@buoy-gg/mcp 7.0.73 → 7.0.74

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,65 +1,65 @@
1
- import{createRequire as jc}from"module";const kw=jc(import.meta.url);import{PACKAGE_NAME as Zr,VERSION as In}from"./chunk-AHIKIVTB.mjs";import{accountRequired as Ii,farmFairness as Lc,hasBuoyAccount as Mi,registerSimFarmTools as qc}from"./chunk-DZRRLTOD.mjs";import{bootEnv as Uc,farmAdbTarget as Qr,farmAdbTargets as zc,simLaunchEnv as Wc}from"./chunk-MJDNVDJI.mjs";import{describeDevices as Hc,describeDiagnosis as Jc,describeInputs as Gc,entitlementHasPro as Mn,proRequired as Kc,readEntitlement as Vc,run as Ne}from"./chunk-OUCB2Y5J.mjs";import{accessFor as Yc,currentAccess as ut,freeToolNames as Xc,hasProAccess as fe,isFreeAppCall as Zc,meetsAccess as An,outsideCalls as Qc,proRequiredMessage as Xt,runWithAccess as eo}from"./chunk-FOMLVT7K.mjs";import{freeComparison as el,freeJsTop as tl,freeRanking as Ai,freeWebBench as nl,nothingLocked as En,withLockedLines as It}from"./chunk-OP23W722.mjs";import{GPU_RENDERER_JS as il,caseUrl as to,closeWithin as Mt,formatWebBenchMarkdown as rl,isSoftwareGpu as no,loadChromium as io,runWebBench as ol,shuffled as al,snapRefreshRate as sl,waitForHostLock as ro,withDeadline as oo}from"./chunk-DVRR7W22.mjs";import{__commonJS as ee,__require as me,__toESM as Cn}from"./chunk-MTTWR4T7.mjs";var ao=ee({"../../node_modules/pngjs/lib/chunkstream.js"(e,n){"use strict";var t=me("util");var i=me("stream");var r=n.exports=function(){i.call(this);this._buffers=[];this._buffered=0;this._reads=[];this._paused=false;this._encoding="utf8";this.writable=true};t.inherits(r,i);r.prototype.read=function(o,a){this._reads.push({length:Math.abs(o),allowLess:o<0,func:a});process.nextTick(function(){this._process();if(this._paused&&this._reads&&this._reads.length>0){this._paused=false;this.emit("drain")}}.bind(this))};r.prototype.write=function(o,a){if(!this.writable){this.emit("error",new Error("Stream not writable"));return false}let s;if(Buffer.isBuffer(o)){s=o}else{s=Buffer.from(o,a||this._encoding)}this._buffers.push(s);this._buffered+=s.length;this._process();if(this._reads&&this._reads.length===0){this._paused=true}return this.writable&&!this._paused};r.prototype.end=function(o,a){if(o){this.write(o,a)}this.writable=false;if(!this._buffers){return}if(this._buffers.length===0){this._end()}else{this._buffers.push(null);this._process()}};r.prototype.destroySoon=r.prototype.end;r.prototype._end=function(){if(this._reads.length>0){this.emit("error",new Error("Unexpected end of input"))}this.destroy()};r.prototype.destroy=function(){if(!this._buffers){return}this.writable=false;this._reads=null;this._buffers=null;this.emit("close")};r.prototype._processReadAllowingLess=function(o){this._reads.shift();let a=this._buffers[0];if(a.length>o.length){this._buffered-=o.length;this._buffers[0]=a.slice(o.length);o.func.call(this,a.slice(0,o.length))}else{this._buffered-=a.length;this._buffers.shift();o.func.call(this,a)}};r.prototype._processRead=function(o){this._reads.shift();let a=0;let s=0;let c=Buffer.alloc(o.length);while(a<o.length){let l=this._buffers[s++];let d=Math.min(l.length,o.length-a);l.copy(c,a,0,d);a+=d;if(d!==l.length){this._buffers[--s]=l.slice(d)}}if(s>0){this._buffers.splice(0,s)}this._buffered-=o.length;o.func.call(this,c)};r.prototype._process=function(){try{while(this._buffered>0&&this._reads&&this._reads.length>0){let o=this._reads[0];if(o.allowLess){this._processReadAllowingLess(o)}else if(this._buffered>=o.length){this._processRead(o)}else{break}}if(this._buffers&&!this.writable){this._end()}}catch(o){this.emit("error",o)}}}});var so=ee({"../../node_modules/pngjs/lib/interlace.js"(e){"use strict";var n=[{x:[0],y:[0]},{x:[4],y:[0]},{x:[0,4],y:[4]},{x:[2,6],y:[0,4]},{x:[0,2,4,6],y:[2,6]},{x:[1,3,5,7],y:[0,2,4,6]},{x:[0,1,2,3,4,5,6,7],y:[1,3,5,7]}];e.getImagePasses=function(t,i){let r=[];let o=t%8;let a=i%8;let s=(t-o)/8;let c=(i-a)/8;for(let l=0;l<n.length;l++){let d=n[l];let u=s*d.x.length;let h=c*d.y.length;for(let b=0;b<d.x.length;b++){if(d.x[b]<o){u++}else{break}}for(let b=0;b<d.y.length;b++){if(d.y[b]<a){h++}else{break}}if(u>0&&h>0){r.push({width:u,height:h,index:l})}}return r};e.getInterlaceIterator=function(t){return function(i,r,o){let a=i%n[o].x.length;let s=(i-a)/n[o].x.length*8+n[o].x[a];let c=r%n[o].y.length;let l=(r-c)/n[o].y.length*8+n[o].y[c];return s*4+l*t*4}}}});var co=ee({"../../node_modules/pngjs/lib/paeth-predictor.js"(e,n){"use strict";n.exports=function t(i,r,o){let a=i+r-o;let s=Math.abs(a-i);let c=Math.abs(a-r);let l=Math.abs(a-o);if(s<=c&&s<=l){return i}if(c<=l){return r}return o}}});var lo=ee({"../../node_modules/pngjs/lib/filter-parse.js"(e,n){"use strict";var t=so();var i=co();function r(a,s,c){let l=a*s;if(c!==8){l=Math.ceil(l/(8/c))}return l}var o=n.exports=function(a,s){let c=a.width;let l=a.height;let d=a.interlace;let u=a.bpp;let h=a.depth;this.read=s.read;this.write=s.write;this.complete=s.complete;this._imageIndex=0;this._images=[];if(d){let b=t.getImagePasses(c,l);for(let f=0;f<b.length;f++){this._images.push({byteWidth:r(b[f].width,u,h),height:b[f].height,lineIndex:0})}}else{this._images.push({byteWidth:r(c,u,h),height:l,lineIndex:0})}if(h===8){this._xComparison=u}else if(h===16){this._xComparison=u*2}else{this._xComparison=1}};o.prototype.start=function(){this.read(this._images[this._imageIndex].byteWidth+1,this._reverseFilterLine.bind(this))};o.prototype._unFilterType1=function(a,s,c){let l=this._xComparison;let d=l-1;for(let u=0;u<c;u++){let h=a[1+u];let b=u>d?s[u-l]:0;s[u]=h+b}};o.prototype._unFilterType2=function(a,s,c){let l=this._lastLine;for(let d=0;d<c;d++){let u=a[1+d];let h=l?l[d]:0;s[d]=u+h}};o.prototype._unFilterType3=function(a,s,c){let l=this._xComparison;let d=l-1;let u=this._lastLine;for(let h=0;h<c;h++){let b=a[1+h];let f=u?u[h]:0;let w=h>d?s[h-l]:0;let g=Math.floor((w+f)/2);s[h]=b+g}};o.prototype._unFilterType4=function(a,s,c){let l=this._xComparison;let d=l-1;let u=this._lastLine;for(let h=0;h<c;h++){let b=a[1+h];let f=u?u[h]:0;let w=h>d?s[h-l]:0;let g=h>d&&u?u[h-l]:0;let m=i(w,f,g);s[h]=b+m}};o.prototype._reverseFilterLine=function(a){let s=a[0];let c;let l=this._images[this._imageIndex];let d=l.byteWidth;if(s===0){c=a.slice(1,d+1)}else{c=Buffer.alloc(d);switch(s){case 1:this._unFilterType1(a,c,d);break;case 2:this._unFilterType2(a,c,d);break;case 3:this._unFilterType3(a,c,d);break;case 4:this._unFilterType4(a,c,d);break;default:throw new Error("Unrecognised filter type - "+s)}}this.write(c);l.lineIndex++;if(l.lineIndex>=l.height){this._lastLine=null;this._imageIndex++;l=this._images[this._imageIndex]}else{this._lastLine=c}if(l){this.read(l.byteWidth+1,this._reverseFilterLine.bind(this))}else{this._lastLine=null;this.complete()}}}});var cl=ee({"../../node_modules/pngjs/lib/filter-parse-async.js"(e,n){"use strict";var t=me("util");var i=ao();var r=lo();var o=n.exports=function(a){i.call(this);let s=[];let c=this;this._filter=new r(a,{read:this.read.bind(this),write:function(l){s.push(l)},complete:function(){c.emit("complete",Buffer.concat(s))}});this._filter.start()};t.inherits(o,i)}});var Zt=ee({"../../node_modules/pngjs/lib/constants.js"(e,n){"use strict";n.exports={PNG_SIGNATURE:[137,80,78,71,13,10,26,10],TYPE_IHDR:1229472850,TYPE_IEND:1229278788,TYPE_IDAT:1229209940,TYPE_PLTE:1347179589,TYPE_tRNS:1951551059,TYPE_gAMA:1732332865,COLORTYPE_GRAYSCALE:0,COLORTYPE_PALETTE:1,COLORTYPE_COLOR:2,COLORTYPE_ALPHA:4,COLORTYPE_PALETTE_COLOR:3,COLORTYPE_COLOR_ALPHA:6,COLORTYPE_TO_BPP_MAP:{0:1,2:3,3:1,4:2,6:4},GAMMA_DIVISION:1e5}}});var uo=ee({"../../node_modules/pngjs/lib/crc.js"(e,n){"use strict";var t=[];(function(){for(let r=0;r<256;r++){let o=r;for(let a=0;a<8;a++){if(o&1){o=3988292384^o>>>1}else{o=o>>>1}}t[r]=o}})();var i=n.exports=function(){this._crc=-1};i.prototype.write=function(r){for(let o=0;o<r.length;o++){this._crc=t[(this._crc^r[o])&255]^this._crc>>>8}return true};i.prototype.crc32=function(){return this._crc^-1};i.crc32=function(r){let o=-1;for(let a=0;a<r.length;a++){o=t[(o^r[a])&255]^o>>>8}return o^-1}}});var ho=ee({"../../node_modules/pngjs/lib/parser.js"(e,n){"use strict";var t=Zt();var i=uo();var r=n.exports=function(o,a){this._options=o;o.checkCRC=o.checkCRC!==false;this._hasIHDR=false;this._hasIEND=false;this._emittedHeadersFinished=false;this._palette=[];this._colorType=0;this._chunks={};this._chunks[t.TYPE_IHDR]=this._handleIHDR.bind(this);this._chunks[t.TYPE_IEND]=this._handleIEND.bind(this);this._chunks[t.TYPE_IDAT]=this._handleIDAT.bind(this);this._chunks[t.TYPE_PLTE]=this._handlePLTE.bind(this);this._chunks[t.TYPE_tRNS]=this._handleTRNS.bind(this);this._chunks[t.TYPE_gAMA]=this._handleGAMA.bind(this);this.read=a.read;this.error=a.error;this.metadata=a.metadata;this.gamma=a.gamma;this.transColor=a.transColor;this.palette=a.palette;this.parsed=a.parsed;this.inflateData=a.inflateData;this.finished=a.finished;this.simpleTransparency=a.simpleTransparency;this.headersFinished=a.headersFinished||function(){}};r.prototype.start=function(){this.read(t.PNG_SIGNATURE.length,this._parseSignature.bind(this))};r.prototype._parseSignature=function(o){let a=t.PNG_SIGNATURE;for(let s=0;s<a.length;s++){if(o[s]!==a[s]){this.error(new Error("Invalid file signature"));return}}this.read(8,this._parseChunkBegin.bind(this))};r.prototype._parseChunkBegin=function(o){let a=o.readUInt32BE(0);let s=o.readUInt32BE(4);let c="";for(let d=4;d<8;d++){c+=String.fromCharCode(o[d])}let l=Boolean(o[4]&32);if(!this._hasIHDR&&s!==t.TYPE_IHDR){this.error(new Error("Expected IHDR on beggining"));return}this._crc=new i;this._crc.write(Buffer.from(c));if(this._chunks[s]){return this._chunks[s](a)}if(!l){this.error(new Error("Unsupported critical chunk type "+c));return}this.read(a+4,this._skipChunk.bind(this))};r.prototype._skipChunk=function(){this.read(8,this._parseChunkBegin.bind(this))};r.prototype._handleChunkEnd=function(){this.read(4,this._parseChunkEnd.bind(this))};r.prototype._parseChunkEnd=function(o){let a=o.readInt32BE(0);let s=this._crc.crc32();if(this._options.checkCRC&&s!==a){this.error(new Error("Crc error - "+a+" - "+s));return}if(!this._hasIEND){this.read(8,this._parseChunkBegin.bind(this))}};r.prototype._handleIHDR=function(o){this.read(o,this._parseIHDR.bind(this))};r.prototype._parseIHDR=function(o){this._crc.write(o);let a=o.readUInt32BE(0);let s=o.readUInt32BE(4);let c=o[8];let l=o[9];let d=o[10];let u=o[11];let h=o[12];if(c!==8&&c!==4&&c!==2&&c!==1&&c!==16){this.error(new Error("Unsupported bit depth "+c));return}if(!(l in t.COLORTYPE_TO_BPP_MAP)){this.error(new Error("Unsupported color type"));return}if(d!==0){this.error(new Error("Unsupported compression method"));return}if(u!==0){this.error(new Error("Unsupported filter method"));return}if(h!==0&&h!==1){this.error(new Error("Unsupported interlace method"));return}this._colorType=l;let b=t.COLORTYPE_TO_BPP_MAP[this._colorType];this._hasIHDR=true;this.metadata({width:a,height:s,depth:c,interlace:Boolean(h),palette:Boolean(l&t.COLORTYPE_PALETTE),color:Boolean(l&t.COLORTYPE_COLOR),alpha:Boolean(l&t.COLORTYPE_ALPHA),bpp:b,colorType:l});this._handleChunkEnd()};r.prototype._handlePLTE=function(o){this.read(o,this._parsePLTE.bind(this))};r.prototype._parsePLTE=function(o){this._crc.write(o);let a=Math.floor(o.length/3);for(let s=0;s<a;s++){this._palette.push([o[s*3],o[s*3+1],o[s*3+2],255])}this.palette(this._palette);this._handleChunkEnd()};r.prototype._handleTRNS=function(o){this.simpleTransparency();this.read(o,this._parseTRNS.bind(this))};r.prototype._parseTRNS=function(o){this._crc.write(o);if(this._colorType===t.COLORTYPE_PALETTE_COLOR){if(this._palette.length===0){this.error(new Error("Transparency chunk must be after palette"));return}if(o.length>this._palette.length){this.error(new Error("More transparent colors than palette size"));return}for(let a=0;a<o.length;a++){this._palette[a][3]=o[a]}this.palette(this._palette)}if(this._colorType===t.COLORTYPE_GRAYSCALE){this.transColor([o.readUInt16BE(0)])}if(this._colorType===t.COLORTYPE_COLOR){this.transColor([o.readUInt16BE(0),o.readUInt16BE(2),o.readUInt16BE(4)])}this._handleChunkEnd()};r.prototype._handleGAMA=function(o){this.read(o,this._parseGAMA.bind(this))};r.prototype._parseGAMA=function(o){this._crc.write(o);this.gamma(o.readUInt32BE(0)/t.GAMMA_DIVISION);this._handleChunkEnd()};r.prototype._handleIDAT=function(o){if(!this._emittedHeadersFinished){this._emittedHeadersFinished=true;this.headersFinished()}this.read(-o,this._parseIDAT.bind(this,o))};r.prototype._parseIDAT=function(o,a){this._crc.write(a);if(this._colorType===t.COLORTYPE_PALETTE_COLOR&&this._palette.length===0){throw new Error("Expected palette not found")}this.inflateData(a);let s=o-a.length;if(s>0){this._handleIDAT(s)}else{this._handleChunkEnd()}};r.prototype._handleIEND=function(o){this.read(o,this._parseIEND.bind(this))};r.prototype._parseIEND=function(o){this._crc.write(o);this._hasIEND=true;this._handleChunkEnd();if(this.finished){this.finished()}}}});var po=ee({"../../node_modules/pngjs/lib/bitmapper.js"(e){"use strict";var n=so();var t=[function(){},function(s,c,l,d){if(d===c.length){throw new Error("Ran out of data")}let u=c[d];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=255},function(s,c,l,d){if(d+1>=c.length){throw new Error("Ran out of data")}let u=c[d];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=c[d+1]},function(s,c,l,d){if(d+2>=c.length){throw new Error("Ran out of data")}s[l]=c[d];s[l+1]=c[d+1];s[l+2]=c[d+2];s[l+3]=255},function(s,c,l,d){if(d+3>=c.length){throw new Error("Ran out of data")}s[l]=c[d];s[l+1]=c[d+1];s[l+2]=c[d+2];s[l+3]=c[d+3]}];var i=[function(){},function(s,c,l,d){let u=c[0];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=d},function(s,c,l){let d=c[0];s[l]=d;s[l+1]=d;s[l+2]=d;s[l+3]=c[1]},function(s,c,l,d){s[l]=c[0];s[l+1]=c[1];s[l+2]=c[2];s[l+3]=d},function(s,c,l){s[l]=c[0];s[l+1]=c[1];s[l+2]=c[2];s[l+3]=c[3]}];function r(s,c){let l=[];let d=0;function u(){if(d===s.length){throw new Error("Ran out of data")}let h=s[d];d++;let b,f,w,g,m,p,y,v;switch(c){default:throw new Error("unrecognised depth");case 16:y=s[d];d++;l.push((h<<8)+y);break;case 4:y=h&15;v=h>>4;l.push(v,y);break;case 2:m=h&3;p=h>>2&3;y=h>>4&3;v=h>>6&3;l.push(v,y,p,m);break;case 1:b=h&1;f=h>>1&1;w=h>>2&1;g=h>>3&1;m=h>>4&1;p=h>>5&1;y=h>>6&1;v=h>>7&1;l.push(v,y,p,m,g,w,f,b);break}}return{get:function(h){while(l.length<h){u()}let b=l.slice(0,h);l=l.slice(h);return b},resetAfterLine:function(){l.length=0},end:function(){if(d!==s.length){throw new Error("extra data found")}}}}function o(s,c,l,d,u,h){let b=s.width;let f=s.height;let w=s.index;for(let g=0;g<f;g++){for(let m=0;m<b;m++){let p=l(m,g,w);t[d](c,u,p,h);h+=d}}return h}function a(s,c,l,d,u,h){let b=s.width;let f=s.height;let w=s.index;for(let g=0;g<f;g++){for(let m=0;m<b;m++){let p=u.get(d);let y=l(m,g,w);i[d](c,p,y,h)}u.resetAfterLine()}}e.dataToBitMap=function(s,c){let l=c.width;let d=c.height;let u=c.depth;let h=c.bpp;let b=c.interlace;let f;if(u!==8){f=r(s,u)}let w;if(u<=8){w=Buffer.alloc(l*d*4)}else{w=new Uint16Array(l*d*4)}let g=Math.pow(2,u)-1;let m=0;let p;let y;if(b){p=n.getImagePasses(l,d);y=n.getInterlaceIterator(l,d)}else{let v=0;y=function(){let $=v;v+=4;return $};p=[{width:l,height:d}]}for(let v=0;v<p.length;v++){if(u===8){m=o(p[v],w,y,h,s,m)}else{a(p[v],w,y,h,f,g)}}if(u===8){if(m!==s.length){throw new Error("extra data found")}}else{f.end()}return w}}});var fo=ee({"../../node_modules/pngjs/lib/format-normaliser.js"(e,n){"use strict";function t(o,a,s,c,l){let d=0;for(let u=0;u<c;u++){for(let h=0;h<s;h++){let b=l[o[d]];if(!b){throw new Error("index "+o[d]+" not in palette")}for(let f=0;f<4;f++){a[d+f]=b[f]}d+=4}}}function i(o,a,s,c,l){let d=0;for(let u=0;u<c;u++){for(let h=0;h<s;h++){let b=false;if(l.length===1){if(l[0]===o[d]){b=true}}else if(l[0]===o[d]&&l[1]===o[d+1]&&l[2]===o[d+2]){b=true}if(b){for(let f=0;f<4;f++){a[d+f]=0}}d+=4}}}function r(o,a,s,c,l){let d=255;let u=Math.pow(2,l)-1;let h=0;for(let b=0;b<c;b++){for(let f=0;f<s;f++){for(let w=0;w<4;w++){a[h+w]=Math.floor(o[h+w]*d/u+.5)}h+=4}}}n.exports=function(o,a,s=false){let c=a.depth;let l=a.width;let d=a.height;let u=a.colorType;let h=a.transColor;let b=a.palette;let f=o;if(u===3){t(o,f,l,d,b)}else{if(h){i(o,f,l,d,h)}if(c!==8&&!s){if(c===16){f=Buffer.alloc(l*d*4)}r(o,f,l,d,c)}}return f}}});var ll=ee({"../../node_modules/pngjs/lib/parser-async.js"(e,n){"use strict";var t=me("util");var i=me("zlib");var r=ao();var o=cl();var a=ho();var s=po();var c=fo();var l=n.exports=function(d){r.call(this);this._parser=new a(d,{read:this.read.bind(this),error:this._handleError.bind(this),metadata:this._handleMetaData.bind(this),gamma:this.emit.bind(this,"gamma"),palette:this._handlePalette.bind(this),transColor:this._handleTransColor.bind(this),finished:this._finished.bind(this),inflateData:this._inflateData.bind(this),simpleTransparency:this._simpleTransparency.bind(this),headersFinished:this._headersFinished.bind(this)});this._options=d;this.writable=true;this._parser.start()};t.inherits(l,r);l.prototype._handleError=function(d){this.emit("error",d);this.writable=false;this.destroy();if(this._inflate&&this._inflate.destroy){this._inflate.destroy()}if(this._filter){this._filter.destroy();this._filter.on("error",function(){})}this.errord=true};l.prototype._inflateData=function(d){if(!this._inflate){if(this._bitmapInfo.interlace){this._inflate=i.createInflate();this._inflate.on("error",this.emit.bind(this,"error"));this._filter.on("complete",this._complete.bind(this));this._inflate.pipe(this._filter)}else{let u=(this._bitmapInfo.width*this._bitmapInfo.bpp*this._bitmapInfo.depth+7>>3)+1;let h=u*this._bitmapInfo.height;let b=Math.max(h,i.Z_MIN_CHUNK);this._inflate=i.createInflate({chunkSize:b});let f=h;let w=this.emit.bind(this,"error");this._inflate.on("error",function(m){if(!f){return}w(m)});this._filter.on("complete",this._complete.bind(this));let g=this._filter.write.bind(this._filter);this._inflate.on("data",function(m){if(!f){return}if(m.length>f){m=m.slice(0,f)}f-=m.length;g(m)});this._inflate.on("end",this._filter.end.bind(this._filter))}}this._inflate.write(d)};l.prototype._handleMetaData=function(d){this._metaData=d;this._bitmapInfo=Object.create(d);this._filter=new o(this._bitmapInfo)};l.prototype._handleTransColor=function(d){this._bitmapInfo.transColor=d};l.prototype._handlePalette=function(d){this._bitmapInfo.palette=d};l.prototype._simpleTransparency=function(){this._metaData.alpha=true};l.prototype._headersFinished=function(){this.emit("metadata",this._metaData)};l.prototype._finished=function(){if(this.errord){return}if(!this._inflate){this.emit("error","No Inflate block")}else{this._inflate.end()}};l.prototype._complete=function(d){if(this.errord){return}let u;try{let h=s.dataToBitMap(d,this._bitmapInfo);u=c(h,this._bitmapInfo,this._options.skipRescale);h=null}catch(h){this._handleError(h);return}this.emit("parsed",u)}}});var dl=ee({"../../node_modules/pngjs/lib/bitpacker.js"(e,n){"use strict";var t=Zt();n.exports=function(i,r,o,a){let s=[t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(a.colorType)!==-1;if(a.colorType===a.inputColorType){let m=(function(){let p=new ArrayBuffer(2);new DataView(p).setInt16(0,256,true);return new Int16Array(p)[0]!==256})();if(a.bitDepth===8||a.bitDepth===16&&m){return i}}let c=a.bitDepth!==16?i:new Uint16Array(i.buffer);let l=255;let d=t.COLORTYPE_TO_BPP_MAP[a.inputColorType];if(d===4&&!a.inputHasAlpha){d=3}let u=t.COLORTYPE_TO_BPP_MAP[a.colorType];if(a.bitDepth===16){l=65535;u*=2}let h=Buffer.alloc(r*o*u);let b=0;let f=0;let w=a.bgColor||{};if(w.red===void 0){w.red=l}if(w.green===void 0){w.green=l}if(w.blue===void 0){w.blue=l}function g(){let m;let p;let y;let v=l;switch(a.inputColorType){case t.COLORTYPE_COLOR_ALPHA:v=c[b+3];m=c[b];p=c[b+1];y=c[b+2];break;case t.COLORTYPE_COLOR:m=c[b];p=c[b+1];y=c[b+2];break;case t.COLORTYPE_ALPHA:v=c[b+1];m=c[b];p=m;y=m;break;case t.COLORTYPE_GRAYSCALE:m=c[b];p=m;y=m;break;default:throw new Error("input color type:"+a.inputColorType+" is not supported at present")}if(a.inputHasAlpha){if(!s){v/=l;m=Math.min(Math.max(Math.round((1-v)*w.red+v*m),0),l);p=Math.min(Math.max(Math.round((1-v)*w.green+v*p),0),l);y=Math.min(Math.max(Math.round((1-v)*w.blue+v*y),0),l)}}return{red:m,green:p,blue:y,alpha:v}}for(let m=0;m<o;m++){for(let p=0;p<r;p++){let y=g(c,b);switch(a.colorType){case t.COLORTYPE_COLOR_ALPHA:case t.COLORTYPE_COLOR:if(a.bitDepth===8){h[f]=y.red;h[f+1]=y.green;h[f+2]=y.blue;if(s){h[f+3]=y.alpha}}else{h.writeUInt16BE(y.red,f);h.writeUInt16BE(y.green,f+2);h.writeUInt16BE(y.blue,f+4);if(s){h.writeUInt16BE(y.alpha,f+6)}}break;case t.COLORTYPE_ALPHA:case t.COLORTYPE_GRAYSCALE:{let v=(y.red+y.green+y.blue)/3;if(a.bitDepth===8){h[f]=v;if(s){h[f+1]=y.alpha}}else{h.writeUInt16BE(v,f);if(s){h.writeUInt16BE(y.alpha,f+2)}}break}default:throw new Error("unrecognised color Type "+a.colorType)}b+=d;f+=u}}return h}}});var ul=ee({"../../node_modules/pngjs/lib/filter-pack.js"(e,n){"use strict";var t=co();function i(w,g,m,p,y){for(let v=0;v<m;v++){p[y+v]=w[g+v]}}function r(w,g,m){let p=0;let y=g+m;for(let v=g;v<y;v++){p+=Math.abs(w[v])}return p}function o(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=w[g+$]-T;p[y+$]=k}}function a(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=w[g+v]-$;y+=Math.abs(T)}return y}function s(w,g,m,p,y){for(let v=0;v<m;v++){let $=g>0?w[g+v-m]:0;let T=w[g+v]-$;p[y+v]=T}}function c(w,g,m){let p=0;let y=g+m;for(let v=g;v<y;v++){let $=g>0?w[v-m]:0;let T=w[v]-$;p+=Math.abs(T)}return p}function l(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=g>0?w[g+$-m]:0;let D=w[g+$]-(T+k>>1);p[y+$]=D}}function d(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=g>0?w[g+v-m]:0;let k=w[g+v]-($+T>>1);y+=Math.abs(k)}return y}function u(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=g>0?w[g+$-m]:0;let D=g>0&&$>=v?w[g+$-(m+v)]:0;let E=w[g+$]-t(T,k,D);p[y+$]=E}}function h(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=g>0?w[g+v-m]:0;let k=g>0&&v>=p?w[g+v-(m+p)]:0;let D=w[g+v]-t($,T,k);y+=Math.abs(D)}return y}var b={0:i,1:o,2:s,3:l,4:u};var f={0:r,1:a,2:c,3:d,4:h};n.exports=function(w,g,m,p,y){let v;if(!("filterType"in p)||p.filterType===-1){v=[0,1,2,3,4]}else if(typeof p.filterType==="number"){v=[p.filterType]}else{throw new Error("unrecognised filter types")}if(p.bitDepth===16){y*=2}let $=g*y;let T=0;let k=0;let D=Buffer.alloc(($+1)*m);let E=v[0];for(let I=0;I<m;I++){if(v.length>1){let R=Infinity;for(let _=0;_<v.length;_++){let S=f[v[_]](w,k,$,y);if(S<R){E=v[_];R=S}}}D[T]=E;T++;b[E](w,k,$,D,T,y);T+=$;k+=$}return D}}});var mo=ee({"../../node_modules/pngjs/lib/packer.js"(e,n){"use strict";var t=Zt();var i=uo();var r=dl();var o=ul();var a=me("zlib");var s=n.exports=function(c){this._options=c;c.deflateChunkSize=c.deflateChunkSize||32*1024;c.deflateLevel=c.deflateLevel!=null?c.deflateLevel:9;c.deflateStrategy=c.deflateStrategy!=null?c.deflateStrategy:3;c.inputHasAlpha=c.inputHasAlpha!=null?c.inputHasAlpha:true;c.deflateFactory=c.deflateFactory||a.createDeflate;c.bitDepth=c.bitDepth||8;c.colorType=typeof c.colorType==="number"?c.colorType:t.COLORTYPE_COLOR_ALPHA;c.inputColorType=typeof c.inputColorType==="number"?c.inputColorType:t.COLORTYPE_COLOR_ALPHA;if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(c.colorType)===-1){throw new Error("option color type:"+c.colorType+" is not supported at present")}if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(c.inputColorType)===-1){throw new Error("option input color type:"+c.inputColorType+" is not supported at present")}if(c.bitDepth!==8&&c.bitDepth!==16){throw new Error("option bit depth:"+c.bitDepth+" is not supported at present")}};s.prototype.getDeflateOptions=function(){return{chunkSize:this._options.deflateChunkSize,level:this._options.deflateLevel,strategy:this._options.deflateStrategy}};s.prototype.createDeflate=function(){return this._options.deflateFactory(this.getDeflateOptions())};s.prototype.filterData=function(c,l,d){let u=r(c,l,d,this._options);let h=t.COLORTYPE_TO_BPP_MAP[this._options.colorType];let b=o(u,l,d,this._options,h);return b};s.prototype._packChunk=function(c,l){let d=l?l.length:0;let u=Buffer.alloc(d+12);u.writeUInt32BE(d,0);u.writeUInt32BE(c,4);if(l){l.copy(u,8)}u.writeInt32BE(i.crc32(u.slice(4,u.length-4)),u.length-4);return u};s.prototype.packGAMA=function(c){let l=Buffer.alloc(4);l.writeUInt32BE(Math.floor(c*t.GAMMA_DIVISION),0);return this._packChunk(t.TYPE_gAMA,l)};s.prototype.packIHDR=function(c,l){let d=Buffer.alloc(13);d.writeUInt32BE(c,0);d.writeUInt32BE(l,4);d[8]=this._options.bitDepth;d[9]=this._options.colorType;d[10]=0;d[11]=0;d[12]=0;return this._packChunk(t.TYPE_IHDR,d)};s.prototype.packIDAT=function(c){return this._packChunk(t.TYPE_IDAT,c)};s.prototype.packIEND=function(){return this._packChunk(t.TYPE_IEND,null)}}});var hl=ee({"../../node_modules/pngjs/lib/packer-async.js"(e,n){"use strict";var t=me("util");var i=me("stream");var r=Zt();var o=mo();var a=n.exports=function(s){i.call(this);let c=s||{};this._packer=new o(c);this._deflate=this._packer.createDeflate();this.readable=true};t.inherits(a,i);a.prototype.pack=function(s,c,l,d){this.emit("data",Buffer.from(r.PNG_SIGNATURE));this.emit("data",this._packer.packIHDR(c,l));if(d){this.emit("data",this._packer.packGAMA(d))}let u=this._packer.filterData(s,c,l);this._deflate.on("error",this.emit.bind(this,"error"));this._deflate.on("data",function(h){this.emit("data",this._packer.packIDAT(h))}.bind(this));this._deflate.on("end",function(){this.emit("data",this._packer.packIEND());this.emit("end")}.bind(this));this._deflate.end(u)}}});var pl=ee({"../../node_modules/pngjs/lib/sync-inflate.js"(e,n){"use strict";var t=me("assert").ok;var i=me("zlib");var r=me("util");var o=me("buffer").kMaxLength;function a(u){if(!(this instanceof a)){return new a(u)}if(u&&u.chunkSize<i.Z_MIN_CHUNK){u.chunkSize=i.Z_MIN_CHUNK}i.Inflate.call(this,u);this._offset=this._offset===void 0?this._outOffset:this._offset;this._buffer=this._buffer||this._outBuffer;if(u&&u.maxLength!=null){this._maxLength=u.maxLength}}function s(u){return new a(u)}function c(u,h){if(h){process.nextTick(h)}if(!u._handle){return}u._handle.close();u._handle=null}a.prototype._processChunk=function(u,h,b){if(typeof b==="function"){return i.Inflate._processChunk.call(this,u,h,b)}let f=this;let w=u&&u.length;let g=this._chunkSize-this._offset;let m=this._maxLength;let p=0;let y=[];let v=0;let $;this.on("error",function(E){$=E});function T(E,I){if(f._hadError){return}let R=g-I;t(R>=0,"have should not go down");if(R>0){let _=f._buffer.slice(f._offset,f._offset+R);f._offset+=R;if(_.length>m){_=_.slice(0,m)}y.push(_);v+=_.length;m-=_.length;if(m===0){return false}}if(I===0||f._offset>=f._chunkSize){g=f._chunkSize;f._offset=0;f._buffer=Buffer.allocUnsafe(f._chunkSize)}if(I===0){p+=w-E;w=E;return true}return false}t(this._handle,"zlib binding closed");let k;do{k=this._handle.writeSync(h,u,p,w,this._buffer,this._offset,g);k=k||this._writeState}while(!this._hadError&&T(k[0],k[1]));if(this._hadError){throw $}if(v>=o){c(this);throw new RangeError("Cannot create final Buffer. It would be larger than 0x"+o.toString(16)+" bytes")}let D=Buffer.concat(y,v);c(this);return D};r.inherits(a,i.Inflate);function l(u,h){if(typeof h==="string"){h=Buffer.from(h)}if(!(h instanceof Buffer)){throw new TypeError("Not a string or buffer")}let b=u._finishFlushFlag;if(b==null){b=i.Z_FINISH}return u._processChunk(h,b)}function d(u,h){return l(new a(h),u)}n.exports=e=d;e.Inflate=a;e.createInflate=s;e.inflateSync=d}});var wo=ee({"../../node_modules/pngjs/lib/sync-reader.js"(e,n){"use strict";var t=n.exports=function(i){this._buffer=i;this._reads=[]};t.prototype.read=function(i,r){this._reads.push({length:Math.abs(i),allowLess:i<0,func:r})};t.prototype.process=function(){while(this._reads.length>0&&this._buffer.length){let i=this._reads[0];if(this._buffer.length&&(this._buffer.length>=i.length||i.allowLess)){this._reads.shift();let r=this._buffer;this._buffer=r.slice(i.length);i.func.call(this,r.slice(0,i.length))}else{break}}if(this._reads.length>0){throw new Error("There are some read requests waitng on finished stream")}if(this._buffer.length>0){throw new Error("unrecognised content at end of stream")}}}});var fl=ee({"../../node_modules/pngjs/lib/filter-parse-sync.js"(e){"use strict";var n=wo();var t=lo();e.process=function(i,r){let o=[];let a=new n(i);let s=new t(r,{read:a.read.bind(a),write:function(c){o.push(c)},complete:function(){}});s.start();a.process();return Buffer.concat(o)}}});var ml=ee({"../../node_modules/pngjs/lib/parser-sync.js"(e,n){"use strict";var t=true;var i=me("zlib");var r=pl();if(!i.deflateSync){t=false}var o=wo();var a=fl();var s=ho();var c=po();var l=fo();n.exports=function(d,u){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let h;function b(A){h=A}let f;function w(A){f=A}function g(A){f.transColor=A}function m(A){f.palette=A}function p(){f.alpha=true}let y;function v(A){y=A}let $=[];function T(A){$.push(A)}let k=new o(d);let D=new s(u,{read:k.read.bind(k),error:b,metadata:w,gamma:v,palette:m,transColor:g,inflateData:T,simpleTransparency:p});D.start();k.process();if(h){throw h}let E=Buffer.concat($);$.length=0;let I;if(f.interlace){I=i.inflateSync(E)}else{let A=(f.width*f.bpp*f.depth+7>>3)+1;let O=A*f.height;I=r(E,{chunkSize:O,maxLength:O})}E=null;if(!I||!I.length){throw new Error("bad png - invalid inflate data response")}let R=a.process(I,f);E=null;let _=c.dataToBitMap(R,f);R=null;let S=l(_,f,u.skipRescale);f.data=S;f.gamma=y||0;return f}}});var wl=ee({"../../node_modules/pngjs/lib/packer-sync.js"(e,n){"use strict";var t=true;var i=me("zlib");if(!i.deflateSync){t=false}var r=Zt();var o=mo();n.exports=function(a,s){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let c=s||{};let l=new o(c);let d=[];d.push(Buffer.from(r.PNG_SIGNATURE));d.push(l.packIHDR(a.width,a.height));if(a.gamma){d.push(l.packGAMA(a.gamma))}let u=l.filterData(a.data,a.width,a.height);let h=i.deflateSync(u,l.getDeflateOptions());u=null;if(!h||!h.length){throw new Error("bad png - invalid compressed data response")}d.push(l.packIDAT(h));d.push(l.packIEND());return Buffer.concat(d)}}});var gl=ee({"../../node_modules/pngjs/lib/png-sync.js"(e){"use strict";var n=ml();var t=wl();e.read=function(i,r){return n(i,r||{})};e.write=function(i,r){return t(i,r)}}});var On=ee({"../../node_modules/pngjs/lib/png.js"(e){"use strict";var n=me("util");var t=me("stream");var i=ll();var r=hl();var o=gl();var a=e.PNG=function(s){t.call(this);s=s||{};this.width=s.width|0;this.height=s.height|0;this.data=this.width>0&&this.height>0?Buffer.alloc(4*this.width*this.height):null;if(s.fill&&this.data){this.data.fill(0)}this.gamma=0;this.readable=this.writable=true;this._parser=new i(s);this._parser.on("error",this.emit.bind(this,"error"));this._parser.on("close",this._handleClose.bind(this));this._parser.on("metadata",this._metadata.bind(this));this._parser.on("gamma",this._gamma.bind(this));this._parser.on("parsed",function(c){this.data=c;this.emit("parsed",c)}.bind(this));this._packer=new r(s);this._packer.on("data",this.emit.bind(this,"data"));this._packer.on("end",this.emit.bind(this,"end"));this._parser.on("close",this._handleClose.bind(this));this._packer.on("error",this.emit.bind(this,"error"))};n.inherits(a,t);a.sync=o;a.prototype.pack=function(){if(!this.data||!this.data.length){this.emit("error","No data provided");return this}process.nextTick(function(){this._packer.pack(this.data,this.width,this.height,this.gamma)}.bind(this));return this};a.prototype.parse=function(s,c){if(c){let l,d;l=function(u){this.removeListener("error",d);this.data=u;c(null,this)}.bind(this);d=function(u){this.removeListener("parsed",l);c(u,null)}.bind(this);this.once("parsed",l);this.once("error",d)}this.end(s);return this};a.prototype.write=function(s){this._parser.write(s);return true};a.prototype.end=function(s){this._parser.end(s)};a.prototype._metadata=function(s){this.width=s.width;this.height=s.height;this.emit("metadata",s)};a.prototype._gamma=function(s){this.gamma=s};a.prototype._handleClose=function(){if(!this._parser.writable&&!this._packer.readable){this.emit("close")}};a.bitblt=function(s,c,l,d,u,h,b,f){l|=0;d|=0;u|=0;h|=0;b|=0;f|=0;if(l>s.width||d>s.height||l+u>s.width||d+h>s.height){throw new Error("bitblt reading outside image")}if(b>c.width||f>c.height||b+u>c.width||f+h>c.height){throw new Error("bitblt writing outside image")}for(let w=0;w<h;w++){s.data.copy(c.data,(f+w)*c.width+b<<2,(d+w)*s.width+l<<2,(d+w)*s.width+l+u<<2)}};a.prototype.bitblt=function(s,c,l,d,u,h,b){a.bitblt(this,s,c,l,d,u,h,b);return this};a.adjustGamma=function(s){if(s.gamma){for(let c=0;c<s.height;c++){for(let l=0;l<s.width;l++){let d=s.width*c+l<<2;for(let u=0;u<3;u++){let h=s.data[d+u]/255;h=Math.pow(h,1/2.2/s.gamma);s.data[d+u]=Math.round(h*255)}}}s.gamma=0}};a.prototype.adjustGamma=function(){a.adjustGamma(this)}}});import{DEFAULT_BROKER_PORT as bl}from"@buoy-gg/sync-broker";var Ei=bl;var go=`http://localhost:${Ei}`;var bo=15e3;var Ci=2500;var vl=1200;var vo=2e4;var yl=3e4;var yo=5e3;var $l=15*6e4;var Oi="Dashboard";var $e="All";import{readFileSync as $o,writeFileSync as kl,mkdirSync as _l,renameSync as Sl,unlinkSync as xl}from"fs";import{dirname as Tl,join as ko,resolve as Dl}from"path";import{homedir as Il}from"os";import{createHash as Ml}from"crypto";import{validateLicense as Al,isAcceptedValidation as El,isTransientLicenseError as Cl,resolveTier as Ol,isRecognizedAccount as Rl,isBuoyToken as Pl,verifyFreeToken as Nl,resolveAccountAccess as Fl,signCache as Rn,verifyCacheSignature as Bl,LICENSE_LAUNCH_VALIDATION_TTL as _o,LICENSE_REVALIDATION_INTERVAL as ht,LICENSE_TRANSIENT_RETRY_DELAYS as So,KEYGEN_CONFIG as jl,createNodeSession as Ll,CLI_AUDIENCE as ql,exchangeServiceKey as Ul,isServiceKey as zl,normalizeLicenseKey as xo,checkDevToken as Wl,isDevToken as Hl}from"@buoy-gg/license/headless";var To=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","NEXT_PUBLIC_BUOY_KEY","REACT_APP_BUOY_KEY"];function Ri(e,n=process.env){for(const t of To)if(n[t]!==void 0)return xo(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let i;try{i=$o(ko(e,t),"utf8")}catch{continue}const r=new Map;for(const o of i.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const s=a[2].trim();const c=s.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);r.set(a[1],c?c[2]:s.split("#")[0].trim())}for(const o of To)if(r.has(o))return xo(r.get(o))||null}return null}var Jl=class{constructor(e,n={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.unsubscribeSession=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>this.hasKeyAccess()||this.session?.getStatus().state==="active";this.lastSessionSync=0;this.projectDir=Dl(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=Ml("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??ko(Il(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:Ll({audience:ql,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return Fl({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted"}start(){if(this.timer)return;if(this.session){this.unsubscribeSession=this.session.subscribe(()=>this.notify());void this.session.load().then(()=>this.notify(),()=>{})}try{const e=JSON.parse($o(this.cacheFile,"utf8"));if(e.valid&&Bl(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;this.unsubscribeSession?.();this.unsubscribeSession=null;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(){if(this.session&&Date.now()-this.lastSessionSync>15e3){this.lastSessionSync=Date.now();void this.session.sync().catch(()=>{})}const e=Ri(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.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<_o&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+_o;return}this.busy=true;const n=this.generation;try{if(Hl(e)){const s=Wl(e);if(!s.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),s.claims.iat*1e3);const l=s.claims.tier==="free"?"free":"pro";this.cache=Rn({licenseKey:e,licenseId:`dev:${s.claims.sid}`,valid:true,tier:l,isPro:l==="pro",expiresAt:new Date(s.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+ht;return}if(zl(e)){const s=await Ul(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||Ri(this.projectDir)!==e)return;if(!s.ok){if(s.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+ht}else{this.nextCheck=Date.now()+(So[this.retry++]??ht)}return}const c=Date.now();const l=s.claims.exp*1e3;this.cache=Rn({licenseKey:e,licenseId:`service:${s.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(l).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,l-60*60*1e3);return}if(Pl(e)){const s=Nl(e);if(!s){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=Rn({licenseKey:e,licenseId:`token:${s.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+ht;return}const t=await Al(e);if(n!==this.generation||Ri(this.projectDir)!==e)return;const i=t.data?.relationships?.product?.data?.id;const r=t.meta.code==="EXPIRED"&&(Rl(t.data)||this.cache?.licenseKey===e);if(i&&i!==jl.productId||!r&&!El(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+ht;return}const o=r?"free":Ol(t.data);const a=Date.now();this.cache=Rn({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:r?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+ht}catch(t){if(n!==this.generation)return;const i=Cl(t)?So[this.retry++]:void 0;this.nextCheck=Date.now()+(i??ht)}finally{this.busy=false;this.notify()}}persistCache(){try{_l(Tl(this.cacheFile),{recursive:true,mode:448});kl(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});Sl(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{xl(this.cacheFile)}catch{}}};import Gl from"http";import{randomUUID as Kl}from"crypto";import{io as Vl}from"socket.io-client";import{createBroker as Yl,brokerTokenPath as Pi,readBrokerToken as Xl,writeBrokerToken as Zl,DASHBOARD_TOKEN_REQUIRED as Ql}from"@buoy-gg/sync-broker";import{execFile as ed}from"child_process";import{promisify as td}from"util";import rt from"path";import nd from"os";var Do=td(ed);async function id(e){let n;try{n=new URL(e)}catch{return null}if(!rd(n.hostname))return null;const t=n.port||(n.protocol==="https:"?"443":"80");try{const{stdout:i}=await Do("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const r=i.split("\n").map(s=>s.trim()).find(Boolean);if(!r)return null;const{stdout:o}=await Do("lsof",["-a","-p",r,"-d","cwd","-Fn"],{timeout:3e3});const a=o.split("\n").find(s=>s.startsWith("n"));return a?rt.resolve(a.slice(1)):null}catch{return null}}function rd(e,n=nd.networkInterfaces()){const t=e.replace(/^\[|\]$/g,"").toLowerCase();if(t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="10.0.2.2"||t==="10.0.3.2")return true;return Object.values(n).flat().some(i=>i?.address?.toLowerCase()===t)}function Ni(e,n){const t=rt.resolve(n);const i=e.map(a=>({deviceId:a.deviceId,score:a.root?od(rt.resolve(a.root),t):-1})).filter(a=>a.score>=0);if(i.length===0)return null;const r=Math.min(...i.map(a=>a.score));const o=i.filter(a=>a.score===r);return o.length===1?o[0].deviceId:null}function od(e,n){if(e===n)return 0;const t=rt.relative(e,n);const i=rt.relative(n,e);const r=o=>o&&!o.startsWith("..")&&!rt.isAbsolute(o);if(r(t))return t.split(rt.sep).length;if(r(i))return i.split(rt.sep).length;return-1}function ad(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function Io(e){return e>=2}function sd(e){if(ad(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!Io(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 cd(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var ld=3e4;var dd=5e3;function ud(e,n,t){if(e!=="highlight-updates"||n!=="waitFor")return 0;const i=Number(t?.timeoutMs);if(!Number.isFinite(i)||i<=0)return 0;return Math.min(i,ld)+dd}var pt="Not ready:";var hd=class{constructor(e={}){this.socket=null;this.admitted=false;this.createdAt=Date.now();this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.sessionDevice=null;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.crashSeen=new Map;this.rootLookups=new Set;this.devServerRoots=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.transportLost=false;this.deviceConnections=new Map;this.port=e.port??Ei;this.url=e.url??(e.port?`http://localhost:${e.port}`:go);this.actionTimeoutMs=e.actionTimeoutMs??bo;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
2
- `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await Fi(this.url)||await Fi(this.url,8e3);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 Jl(()=>this.broker?.refreshAccess());this.hostAccount.start();let t=false;this.broker=Yl({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:i=>{if(i.code==="EADDRINUSE")t=true;this.log(`broker error: ${i.code??i.message}`)}});await pd(this.url).catch(i=>{if(!t)throw i});if(!this.broker.httpServer.listening)t=true;if(t){this.log(`another broker holds :${this.port} \u2014 connecting to it as a dashboard`);await this.broker.close().catch(()=>void 0);this.broker=null;this.hostAccount.stop();this.hostAccount=null;await this.connectSocket();return"connected"}try{Zl(this.broker.dashboardToken,Pi(this.port))}catch(i){this.log(`could not write the broker token file: ${i.message}`)}n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return Qc(()=>new Promise((e,n)=>{const t=Vl(this.url,{transports:["websocket"],query:{deviceName:Oi},auth:s=>{const c=this.broker?.dashboardToken??Xl(Pi(this.port));s(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let i=false;const r=()=>{if(i)return;i=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(i)return;i=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",s=>{const c=!!s&&typeof s==="object"&&s.protocol===1&&s.ready===true;this.setAdmission(c);if(c){clearTimeout(o);this.rearmWatches();r()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const a=s=>s.message===Ql;t.once("connect_error",s=>{if(i)return;i=true;clearTimeout(o);n(new Error(a(s)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${Pi(this.port)} is missing or stale. Update Buoy Desktop, or restart it so it writes the file.`:`failed to connect to broker at ${this.url}: ${s.message}`))});t.on("connect_error",s=>{if(!a(s)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",s=>{if(!this.admitted||this.lockedDevices.has(s.persistentDeviceId))return;this.capabilities.set(s.persistentDeviceId,s);this.trackRootLookup(this.learnDevServerOrigin(s.persistentDeviceId));if(s.tools?.some(c=>c.toolId==="console"))this.ensureWatching("console",s.persistentDeviceId)});t.on("all-devices-update",s=>{if(!this.admitted)return;if(!Array.isArray(s))return;let c=false;for(const d of s){if(d.deviceId&&(d.tier==="locked"||d.tier==="checking"))this.lockDevice(String(d.deviceId),d.tier==="checking");else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){c=this.lockedDevices.delete(String(d.deviceId))||c}}this.devices=s;const l=Date.now();for(const d of s)if(d.deviceId)this.lastSeen.set(d.deviceId,l);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(s)});t.on("devtool-sync",s=>{if(!this.admitted||this.lockedDevices.has(s.persistentDeviceId))return;const c=ze(s.persistentDeviceId,s.toolId);this.snapshots.set(c,s.payload);this.snapshotAt.set(c,Date.now());if(s.toolId==="console")this.noteCrashReport(s.persistentDeviceId,s.payload);const l=this.snapshotWaiters.get(c);if(l&&l.length){this.snapshotWaiters.set(c,[]);for(const d of l)d(s.payload)}});t.on("devtool-action-result",s=>{if(!this.admitted)return;const c=this.pendingActions.get(s.requestId);if(!c)return;this.pendingActions.delete(s.requestId);clearTimeout(c.timer);c.resolve(s)})}))}isAdmitted(){return this.admitted}requireAdmission(){if(this.transportLost)throw new Error("The broker link was lost. Wait for it to reconnect.");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.transportLost=!e&&n;this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.devServerRoots.clear();this.crashSeen.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(n?"The broker link was lost during the call. Its result is unknown.":"Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const i of t)i(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.devices.some(n=>n.deviceId===e&&n.tier==="checking"))throw new Error(`${pt} The new app link is still checking access.`);if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e,n=false){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.devServerRoots.delete(e);this.originProbed.delete(e);const t=`${e}::`;for(const i of this.snapshots.keys())if(i.startsWith(t)){this.snapshots.delete(i);this.snapshotAt.delete(i)}for(const[i,r]of this.snapshotWaiters)if(i.startsWith(t)){this.snapshotWaiters.delete(i);for(const o of r)o(void 0)}for(const[i,r]of this.pendingActions)if(r.target===e||r.target===$e){clearTimeout(r.timer);this.pendingActions.delete(i);r.reject(new Error(n?"The app link changed during the call. Its result is unknown.":"Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-$l;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(r=>r.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.devServerRoots.delete(n);this.originProbed.delete(n);const i=`${n}::`;for(const r of[...this.snapshots.keys()]){if(r.startsWith(i)){this.snapshots.delete(r);this.snapshotAt.delete(r)}}for(const r of[...this.watching.keys()]){if(r.startsWith(i))this.watching.delete(r)}}}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!==Oi&&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==="checking"?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(i=>i.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}peekProjectDevice(){if(!this.admitted)return null;if(this.sessionDevice)return this.sessionDevice;const e=this.connectedDevices().filter(t=>!this.lockedDevices.has(t.deviceId)&&t.tools.length>0);const n=process.env.BUOY_PROJECT_ROOT||process.cwd();return Ni(e.map(t=>({deviceId:t.deviceId,root:this.devServerRoots.get(t.deviceId)})),n)??null}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(l,d)=>new Error(`${pt} "${l}" (${d}) is not connected right now. If it is restarting, it usually takes a few seconds; to use another device, pass deviceId (run list_devices to see them).`);if(this.sessionDevice){const l=this.devices.find(d=>d.deviceId===this.sessionDevice);if(l&&l.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const i=t.filter(l=>!this.lockedDevices.has(l.deviceId));const r=i.filter(l=>l.tools.length>0);const o=process.env.BUOY_PROJECT_ROOT||process.cwd();const a=Ni(r.map(l=>({deviceId:l.deviceId,root:this.devServerRoots.get(l.deviceId)})),o);if(a)return this.sessionDevice=a;const s=Ni(this.devices.filter(l=>l.deviceId&&l.isConnected===false&&!this.lockedDevices.has(l.deviceId)).map(l=>({deviceId:l.deviceId,root:this.devServerRoots.get(l.deviceId)})),o);if(s)throw n(s,"this project's app");if(i.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(l=>l.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(i.length===1)return this.sessionDevice=i[0].deviceId;if(r.length===1)return this.sessionDevice=r[0].deviceId;if(r.length===0){const l=i.map(d=>d.deviceName).join(", ");throw new Error(`Connected devices (${l}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const c=r.map(l=>`${l.deviceName} (${l.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${c}`)}requireProDevice(e){this.requireAdmission();const n=ut();if(e&&e!==$e)this.sessionDevice=e;const t=a=>{if(!a)return"";return a.tier==="locked"?` The broker has not confirmed account access for "${a.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${a.deviceName}" is on the ${a.tier} tier.`};const i=a=>n.level==="pro"?Xt(n,t(a)):`The Buoy MCP needs an app on a Buoy account.${t(a)} Run \`npx buoy login\` in the project (free), restart the dev server with its cache cleared, then run \`list_devices\` to check.`;if(e===$e){const a=this.connectedDevices().filter(c=>c.isPro||c.tier==="free");const s=a.find(c=>!An(c,n.level));if(a.length===0)throw new Error(i());if(s)throw new Error(i(s));return $e}const r=this.resolveDevice(e);const o=this.connectedDevices().find(a=>a.deviceId===r);if(!An(o,n.level)){const a=this.accessState(r,n.level);if(a==="offline")throw new Error(`${pt} "${r}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(a==="pending")throw new Error(`${pt} account access for "${r}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(i(o))}return r}checkAppCall(e,n,t){const i=ut();if(!i.tool)return;const r=t===$e?this.connectedDevices().find(o=>o.tier==="free"&&!fe(o)):this.listDevices().find(o=>o.deviceId===t&&!fe(o));if(!r)return;if(i.level==="free"&&Zc(e,n))return;throw new Error(Xt({...i,level:"pro"},` The app "${r.deviceName}" is on the ${r.tier} tier${i.level==="free"?`, and this part of \`${i.tool}\` reads its ${e} data, which is Pro`:""}.`))}refuseBuoyPanelTap(e,n,t,i){if(e!=="highlight-updates"||n!=="tapElement")return null;const r=ut();if(!r.tool||r.level!=="free")return null;const o=t??{};const a=typeof o.nativeTag==="number"?o.nativeTag:null;const s=typeof o.testID==="string"&&o.testID?o.testID.toLowerCase():null;const c=a!=null||s!=null||!!o.includeBuoy;if(i===$e){if(!c||!this.connectedDevices().some(u=>u.tier==="free"&&!fe(u)))return null;return Promise.reject(new Error(Xt({...r,level:"pro"},' On a free app, tap one app at a time (not "All") when tapping by testID or nativeTag.')))}const l=this.listDevices().find(u=>u.deviceId===i);if(!l||fe(l)||!c)return null;const d=u=>new Error(Xt({...r,level:"pro"},u));if(o.includeBuoy)return Promise.reject(d(" That reaches Buoy's own panel, not the app."));return(async()=>{let u;try{u=await this.callAction("highlight-updates","describeScreen",{includeBuoy:false,includeHidden:true},i,{timeoutMs:4e3,silent:true})}catch(b){throw new Error(`Could not check that target before tapping (${b instanceof Error?b.message:String(b)}). On a free app, tap by query, or retry once the screen settles.`)}const h=(u?.elements??[]).some(b=>a!=null?b.nativeTag===a:b.testID?.toLowerCase()===s);if(!h&&a==null&&typeof o.query==="string"){const{testID:b,...f}=o;return f}if(!h){throw new Error(`"${a??String(o.testID)}" is not on the app's screen right now. On a free app, a tap by testID or nativeTag must hit an element describe_screen lists, and Buoy's own panels are Pro. Swipe it into view first, or tap by query.`)}})()}fullResultsFor(e){if(e===$e){const n=this.connectedDevices();return n.length>0&&n.every(t=>fe(t))}return fe(this.listDevices().find(n=>n.deviceId===e))}deviceWasAllowed(e){return An(this.listDevices().find(n=>n.deviceId===e),ut().level)}accessState(e,n=ut().level){const t=this.devices.find(i=>i.deviceId===e);if(!t||t.isConnected===false)return"offline";if(fe(t)||n==="free"&&t.tier==="free")return"ready";return t.tier===void 0||t.tier==="checking"?"pending":"denied"}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 Io(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const i=this.healthOf(e);const{timeoutMs:r,usedFullBudget:o}=sd({consecutiveTimeouts:i.consecutiveTimeouts,lastFullBudgetAt:i.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:Ci,recoveryProbeMs:vo,toolId:n,action:t});if(o)i.lastFullBudgetAt=Date.now();return r}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const i=cd({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:yl});const r=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,i?"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)r.push(`Last captured error: ${o}`);r.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 r.join(" ")}noteCrashReport(e,n){const t=n?.entries;if(!Array.isArray(t)||t.length===0)return;const i=Math.max(...t.map(a=>typeof a?.timestamp==="number"?a.timestamp:-Infinity));const r=this.crashSeen.get(e)??new Set;let o=false;for(const a of t){const s=a?.fatal||/^\[(RENDER ERROR|FATAL)/.test(a?.message??"");if(!s||typeof a.timestamp!=="number")continue;const c=`${a.id??""}|${a.timestamp}|${(a.message??"").slice(0,120)}`;if(r.has(c))continue;r.add(c);if(i-a.timestamp<3e4)o=true}if(r.size>200)for(const a of[...r].slice(0,r.size-200))r.delete(a);this.crashSeen.set(e,r);if(!o)return;if(![...this.pendingActions.values()].some(a=>a.target===e&&a.expire&&!a.cut))return;void this.callAction("app","ping",{},e,{timeoutMs:1500,silent:true}).then(()=>void 0,()=>{const a=Date.now();for(const s of this.pendingActions.values()){if(s.target!==e||!s.expire||s.cut)continue;s.cut=true;if(s.deadline!==void 0&&s.deadline-a<=Ci)continue;clearTimeout(s.timer);s.timer=setTimeout(s.expire,Ci)}})}lastFatalFor(e){const n=this.snapshots.get(ze(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let i=t.length-1;i>=0;i--){const r=t[i];if(r?.fatal||r?.level==="error"){const o=(r.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(ze(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){const t=this.devServerOrigins.get(e)!==n;this.devServerOrigins.set(e,n);if(t)this.devServerRoots.delete(e);if(t||!this.devServerRoots.has(e)){const i=id(n).then(r=>{if(r&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,r)});this.trackRootLookup(i)}}}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{}}trackRootLookup(e){const n=e.catch(()=>void 0).finally(()=>this.rootLookups.delete(n));this.rootLookups.add(n)}async awaitBroker(){if(Date.now()-this.createdAt>15e3)return;for(let e=0;!this.admitted&&e<4e3;e+=100)await new Promise(n=>setTimeout(n,100));for(let e=0;this.admitted&&this.devices.length===0&&e<1500;e+=100)await new Promise(n=>setTimeout(n,100))}async settleDeviceChoice(e=1500){await this.awaitBroker();if(this.sessionDevice||this.rootLookups.size===0||this.connectedDevices().length<2)return;await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,e))]);if(this.rootLookups.size)await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,Math.min(800,e)))])}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(ze(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=ze(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)}}rearmReconnectedDevices(e){for(const n of e){if(!n.deviceId||n.isConnected===false)continue;const t=String(n.deviceId);const i=`${n.id}|${n.connectionNonce??""}`;const r=this.deviceConnections.get(t);this.deviceConnections.set(t,i);if(r===void 0||r===i)continue;for(const{toolId:o,targetDeviceId:a}of this.watching.values()){if(a===t)this.sendWatch(o,a)}}}unwatch(e,n){const t=ze(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);this.checkAppCall(e,"@snapshot",n);return this.snapshots.get(ze(n,e))}async getSnapshot(e,n,t={}){const i=this.resolveDevice(n);this.checkAppCall(e,"@snapshot",i);const r=ze(i,e);this.ensureWatching(e,i);let o=t.timeoutMs??yo;if(t.timeoutMs===void 0&&this.isUnresponsive(i)){const l=this.healthOf(i);if(Date.now()-l.lastFullBudgetAt>vo){l.lastFullBudgetAt=Date.now()}else{o=vl}}const a=this.waitForSnapshot(r,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:i});const s=await a;this.requireAdmission();this.requireDeviceAccount(i);if(s!==void 0){if(!t.silent)this.noteOk(i);return{deviceId:i,payload:s}}if(!t.silent)this.noteTimeout(i);const c=this.snapshots.get(r);if(c!==void 0){return{deviceId:i,payload:c,stale:this.staleNote(e,i)}}throw new Error(`Timed out waiting for "${e}" state from ${i} (is the tool active?). ${this.unresponsiveNote(i)}`)}lastSync(e,n){this.checkAppCall(n,"@snapshot",e);const t=this.snapshots.get(ze(e,n));return t===void 0?void 0:{payload:t,ageMs:this.snapshotAgeMs(n,e)}}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const i=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${i}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const i=this.snapshotWaiters.get(e)??[];const r=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(s=>s!==o);this.snapshotWaiters.set(e,a);t(void 0)},n);const o=a=>{clearTimeout(r);t(a)};i.push(o);this.snapshotWaiters.set(e,i)})}async callAction(e,n,t,i,r={}){this.requireAdmission();const o=i===$e?$e:this.resolveDevice(i);this.checkAppCall(e,n,o);const a=this.refuseBuoyPanelTap(e,n,t,o);if(a)t=await a??t;if(!this.socket)throw new Error("SyncClient is not connected");const s=Kl();const c=r.timeoutMs??Math.max(this.timeoutFor(o,e,n),ud(e,n,t));const l=await new Promise((d,u)=>{const h=Date.now();const b=()=>{this.pendingActions.delete(s);if(o!==$e&&!r.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-h}ms. `+(o===$e?"No device responded.":this.unresponsiveNote(o))))};const f=setTimeout(b,c);this.pendingActions.set(s,{resolve:d,reject:u,timer:f,target:o,expire:b,deadline:h+c});this.socket.emit("devtool-action",{requestId:s,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==$e&&!r.silent)this.noteOk(o);if(!l.ok){throw new Error(l.error||`Action "${e}.${n}" failed on device`)}return l.result}};var ze=(e,n)=>`${e}::${n}`;function Fi(e,n=800){return new Promise(t=>{const i=Gl.get(e,r=>{r.resume();t((r.statusCode??0)>0)});i.setTimeout(n,()=>{i.destroy();t(false)});i.on("error",()=>t(false))})}async function pd(e,n=20){for(let t=0;t<n;t++){if(await Fi(e,300))return;await fd(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var fd=e=>new Promise(n=>setTimeout(n,e));import{promises as Mo}from"fs";import md from"os";import wd from"path";var gd=24*60*60*1e3;var bd=1500;var vd=`https://registry.npmjs.org/${Zr}/latest`;var Ao=wd.join(md.tmpdir(),"buoy-mcp-update-check.json");function yd(e,n){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const i=t(e);const r=t(n);for(let o=0;o<3;o++){const a=i[o]??0;const s=r[o]??0;if(a>s)return true;if(a<s)return false}return false}async function $d(){try{const e=await Mo.readFile(Ao,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function kd(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),bd);try{const i=await fetch(vd,{signal:n.signal});if(!i.ok)return null;const r=await i.json();const o=typeof r.version==="string"?r.version:null;if(o){await Mo.writeFile(Ao,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function _d(e){const n=await $d();if(n&&e-n.checkedAt<gd)return n.latest;return kd(e)}async function Eo(e=Date.now()){if(In==="0.0.0-dev")return null;try{const n=await _d(e);if(n&&yd(n,In)){return`A newer buoy-mcp is available (${n}; you're on ${In}). Update by re-running: npx -y ${Zr}@latest init \u2014 then restart your editor.`}}catch{}return null}import{z as te}from"zod";import{execFile as Sd}from"child_process";import{promisify as xd}from"util";import{promises as Fe}from"fs";import Co from"os";import Me from"path";import{randomUUID as Oo}from"crypto";var Qt=xd(Sd);var Pn=(e,n,t)=>Math.min(Math.max(e,n),t);function Ro(e){const n=e;if(n?.code==="ENOENT"){return"Xcode command-line tools (xcrun) not found. Install Xcode to capture the iOS Simulator."}const t=String(n?.stderr||n?.message||e);if(/No devices are booted|Unable to find device|current device/i.test(t)){return"No booted iOS Simulator found. Boot the Simulator with your app running, then try again."}return t.trim()||"Failed to capture the iOS Simulator."}function Td(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,i)=>e[i]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}var Dd="@react_buoy_sync_device_id";async function Nn(e){try{return await Fe.readdir(e)}catch{return[]}}async function Id(e){const n=[];for(const t of await Nn(e))n.push(...await Po(Me.join(e,t)));return n}async function Po(e){const n=[];const t=Me.join(e,"Library","WebKit");for(const i of await Nn(t)){const r=Me.join(t,i,"WebsiteData","Default");for(const o of await Nn(r)){for(const a of await Nn(Me.join(r,o))){const s=Me.join(r,o,a,"LocalStorage","localstorage.sqlite3");try{await Fe.access(s);const{stdout:c}=await Qt("sqlite3",["-readonly",s,`SELECT hex(value) FROM ItemTable WHERE key='${Dd}' LIMIT 1;`],{timeout:1500});const l=c.trim();if(l)n.push(Buffer.from(l,"hex").toString("utf16le").trim())}catch{}}}}return n}var No=new Map;var Md=3e4;async function Fo(e){if(process.platform!=="darwin")return{ok:false,error:"Simulator lookup needs macOS."};let n=[];try{const{stdout:i}=await Qt("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const r=JSON.parse(i).devices;n=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch(i){return{ok:false,error:Ro(i)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const i of n){if(!i.dataPath)continue;const r=Me.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await Fe.readdir(r)}catch{continue}for(const a of o){try{const s=(await Fe.readFile(Me.join(r,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(s===t)return{ok:true,udid:i.udid,how:`matched ${e} to "${i.name}"`}}catch{}}}}if(t&&e){const i=n.map(c=>c.udid).sort().join(",");const r=No.get(e);const o=r?.booted===i&&(r.udid!==null||Date.now()-r.at<Md);const a=o&&r?.udid?n.find(c=>c.udid===r.udid):void 0;if(a)return{ok:true,udid:a.udid,how:`matched ${e} to "${a.name}" (WebView storage)`};const s=[];if(!o){const c=await Promise.all(n.map(l=>l.dataPath?Id(Me.join(l.dataPath,"Containers","Data","Application")):Promise.resolve([])));c.forEach((l,d)=>{if(l.includes(t))s.push(n[d])});No.set(e,{booted:i,udid:s.length===1?s[0].udid:null,at:Date.now()})}if(s.length===1){return{ok:true,udid:s[0].udid,how:`matched ${e} to "${s[0].name}" (WebView storage)`}}if(s.length>1){return{ok:false,error:`${s.length} booted simulators (${s.map(c=>c.name).join(", ")}) hold ${e}. Pass the udid, or shut one down.`}}}if(n.length===1)return{ok:true,udid:n[0].udid,how:`only booted simulator "${n[0].name}"`};return{ok:false,error:`${n.length} iOS Simulators are booted (${n.map(i=>i.name).join(", ")}) and none could be matched to ${e??"this device"}. Shut down the others, or capture from a single booted simulator.`}}async function Ad(e){const n=new Map;if(process.platform!=="darwin"||e.length===0)return n;let t=[];try{const{stdout:r}=await Qt("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const o=JSON.parse(r).devices;t=Object.entries(o).filter(([a])=>/iOS/.test(a)).flatMap(([,a])=>a.filter(s=>s.state==="Booted"))}catch{return n}const i=new Map;for(const r of t){if(!r.dataPath)continue;const o=Me.join(r.dataPath,"Containers","Data","Application");let a=[];try{a=await Fe.readdir(o)}catch{continue}for(const s of a){try{const c=(await Fe.readFile(Me.join(o,s,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(c)i.set(c,{name:r.name,udid:r.udid})}catch{}}}for(const r of e){const o=i.get(r.split("-").pop()??"");if(o)n.set(r,o)}return n}async function Bo(e="booted"){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const n=Me.join(Co.tmpdir(),`buoy-mcp-sim-${Oo()}.png`);try{await Qt("xcrun",["simctl","io",e,"screenshot","--type=png",n],{timeout:15e3})}catch(t){return{ok:false,error:Ro(t)}}try{const t=await Fe.readFile(n);const i=Td(t);if(!i)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:i.width,height:i.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function Ed(e,n,t){const i=Pn(Math.round(t.x),0,Math.max(0,n.width-1));const r=Pn(Math.round(t.y),0,Math.max(0,n.height-1));const o=Pn(Math.round(t.width),1,n.width-i);const a=Pn(Math.round(t.height),1,n.height-r);const s=Me.join(Co.tmpdir(),`buoy-mcp-crop-${Oo()}.png`);try{await Qt("sips",["-c",String(a),String(o),"--cropOffset",String(r),String(i),e,"--out",s],{timeout:1e4});const c=await Fe.readFile(s);return{ok:true,buffer:c,width:o,height:a}}catch(c){const l=c;return{ok:false,error:`Crop failed: ${l?.stderr||l?.message||c}`}}finally{await Fe.unlink(s).catch(()=>{});await Fe.unlink(e).catch(()=>{})}}import{execFile as Cd}from"child_process";import{promisify as Od}from"util";var Bi=Cn(On());import{execFile as Rd}from"child_process";import{promisify as Pd}from"util";import{promises as ie}from"fs";import en from"os";import Y from"path";import{randomUUID as Nd}from"crypto";var Fd=Cn(On());import{execFile as Bd}from"child_process";import{promisify as jd}from"util";import{promises as ft}from"fs";import ji from"os";import Li from"path";import{randomUUID as qi}from"crypto";var Ld=jd(Bd);async function Se(e,n=3e4){const t=Li.join(ji.tmpdir(),`buoy-devicectl-${qi()}.json`);try{await Ld("xcrun",["devicectl",...e,"--quiet","--json-output",t],{timeout:n,maxBuffer:16*1024*1024});return JSON.parse(await ft.readFile(t,"utf8"))}catch(i){const r=i;let o=(r.stderr||r.stdout||r.message||"").trim();try{const a=JSON.parse(await ft.readFile(t,"utf8"));o=a.error?.userInfo?.NSLocalizedDescription?.string??o}catch{}if(/locked/i.test(o))throw new Error("The iPhone is locked. Unlock it and try again.");throw new Error(`devicectl ${e.slice(0,2).join(" ")} failed: ${o.split("\n").slice(-3).join(" ").slice(0,300)}`)}finally{await ft.unlink(t).catch(()=>void 0)}}async function qd(){if(process.platform!=="darwin")return[];return Ud(await Se(["list","devices"],15e3).catch(()=>null))}function Ud(e){const n=e;return(n?.result?.devices??[]).filter(t=>t.hardwareProperties?.reality==="physical"&&t.hardwareProperties?.platform==="iOS"&&t.connectionProperties?.pairingState==="paired"&&t.connectionProperties?.tunnelState!=="unavailable").map(t=>({udid:t.hardwareProperties.udid,name:t.deviceProperties?.name??"iPhone"})).filter(t=>!!t.udid)}async function jo(e){const n=await Se(["device","info","apps","--device",e]);return(n.result?.apps??[]).filter(t=>t.bundleIdentifier&&!t.bundleIdentifier.startsWith("com.apple.")&&t.builtByDeveloper!==false).map(t=>({bundleId:t.bundleIdentifier,name:t.name??t.bundleIdentifier,path:t.url?.replace(/^file:\/\//,"")}))}async function Lo(e,n,t){const i=await Se(["device","info","files","--device",e,"--domain-type","appDataContainer","--domain-identifier",n],2e4);const r=new Set((i.result?.files??[]).map(a=>a.relativePath??a.name??""));if(!r.has(t))return null;const o=Li.join(ji.tmpdir(),`buoy-devfile-${qi()}`);try{await Se(["device","copy","from","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",o],2e4);return await ft.readFile(o)}finally{await ft.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function qo(e,n){const t=await Lo(e,n,"Documents/react-buoy/@react_buoy_sync_device_id.json").catch(()=>null);const i=t?.toString("utf8").trim().replace(/^"|"$/g,"")??"";return/^[\w-]{4,64}$/.test(i)?i:null}async function Uo(e){const n=Li.join(ji.tmpdir(),`buoy-phone-shot-${qi()}.png`);try{await Se(["device","capture","screenshot","--device",e,"--destination",n],3e4);return await ft.readFile(n)}finally{await ft.unlink(n).catch(()=>void 0)}}var zd="The picture is all black. If the app isn't really showing black, the iPhone's own screen is off: it is asleep, or the Mac shows it in iPhone Mirroring (the phone stays locked then). Quit iPhone Mirroring, unlock the phone and keep its screen on, then try again.";function Wd(e){try{const n=Fd.PNG.sync.read(e);const t=Math.max(1,Math.floor(n.width*n.height/4e3));for(let i=0;i<n.width*n.height;i+=t){const r=i*4;if(n.data[r]>12||n.data[r+1]>12||n.data[r+2]>12)return false}return true}catch{return false}}async function Ui(e,n,t={}){await Se(["device","process","launch","--device",e,...t.terminateExisting?["--terminate-existing"]:[],...t.url?["--payload-url",t.url]:[],n])}async function Hd(e,n){await Se(["device","process","openURL","--device",e,n])}async function zo(e,n){const t=await jo(e);const i=t.find(a=>a.bundleId===n)?.path;const r=await Se(["device","info","processes","--device",e]);const o=(r.result?.runningProcesses??[]).find(a=>i&&decodeURIComponent(a.executable??"").replace(/^file:\/\//,"").startsWith(i.replace(/\/$/,"")));if(!o?.processIdentifier)return false;await Se(["device","process","terminate","--device",e,"--pid",String(o.processIdentifier)]);return true}async function Jd(e,n){await Se(["device","uninstall","app","--device",e,n],12e4)}async function Gd(e,n){await Se(["device","install","app","--device",e,n],3e5)}async function Kd(e,n,t,i){await Se(["device","copy","to","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",i],3e4)}function Vd(e,n,t){return["devicectl","device","capture","screen-record","--device",e,"--destination",n,"--duration",String(t)]}var Wo=new Map;async function Fn(e){const n=Wo.get(e);if(n)return n;const t=await Se(["list","devices"]);const i=t.result?.devices?.find(r=>r.hardwareProperties?.udid===e)?.identifier;if(!i)throw new Error("CoreDevice doesn't list this iPhone right now. Check that it is connected and unlocked.");Wo.set(e,i);return i}import{execFile as Yd}from"child_process";import{promisify as Xd}from"util";import{promises as zi}from"fs";import At from"path";import{WebSocket as Zd}from"ws";import{execFile as Qd}from"child_process";import{promisify as eu}from"util";var Wi=/\{\{\s*secret:([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;var Et=6;var Hi=new Map;var Ho=()=>false;function tu(e){Ho=e}function nu(e){Wi.lastIndex=0;return Wi.test(e)}function Bn(e,n=process.env){if(!nu(e))return e;if(Ho()){throw new Error("A screen recording is running, and a video can't hide a typed secret. Stop the recording first (screen_recording stop), then type it.")}const t=[];const i=[];const r=e.replace(Wi,(o,a)=>{const s=n[`BUOY_SECRET_${a}`]??n[a];if(s===void 0||s===""){t.push(a);return""}if(s.length<Et){i.push(a);return""}Hi.set(a,s);return s});if(t.length){throw new Error(`No value for ${t.map(o=>`{{secret:${o}}}`).join(", ")}. Set ${t.map(o=>`BUOY_SECRET_${o}`).join(", ")} in the environment the MCP server runs in (for example in your editor's MCP config "env").`)}if(i.length){throw new Error(`${i.map(o=>`{{secret:${o}}}`).join(", ")} is shorter than ${Et} characters, so Buoy can't hide it reliably in what it sends back. Use a longer value, or type it as plain text if it isn't sensitive.`)}return r}function Jo(e=process.env){const n=new Map(Hi);for(const[t,i]of Object.entries(e))if(t.startsWith("BUOY_SECRET_")&&i)n.set(t.slice("BUOY_SECRET_".length),i);return n}var iu=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function xe(e){const n=new Map;for(const[r,o]of Jo()){if(o.length<Et)continue;for(const a of[o,JSON.stringify(o).slice(1,-1),encodeURIComponent(o)])n.set(a,r);for(let a=o.length-1;a>=Et;a--){n.set(`${o.slice(0,a)}\u2026`,r);n.set(`${o.slice(0,a)}...`,r);if(a>=8)n.set(o.slice(0,a),r)}}if(n.size===0)return e;const t=[...n.keys()].sort((r,o)=>o.length-r.length);const i=new RegExp(t.map(iu).join("|"),"g");return e.replace(i,(r,o,a)=>{if(/^\d+$/.test(r)){const s=a.slice(Math.max(0,o-8),o);const c=a.slice(o+r.length,o+r.length+8);if(/(\d|\d\.)$/.test(s)||/^(\d|\.\d|[eE][+-]?\d)/.test(c))return r;if(/[:[,]\s*$/.test(s)&&/^\s*([,\]}]|$)/.test(c)&&!ru(a,o))return`"{{secret:${n.get(r)}}}"`}return`{{secret:${n.get(r)}}}`})}function ru(e,n){let t=false;for(let i=0;i<n;i++){if(e[i]==="\\")i++;else if(e[i]==='"')t=!t}return t}function Ji(){return[...Hi.values()]}function ou(){return[...Jo().values()].filter(e=>e.length>=Et)}var Go=eu(Qd);var Gi="Android control needs adb on this computer (Android SDK platform-tools). Install it, or add it to PATH.";function de(e){return`'${e.replace(/'/g,`'\\''`)}'`}function Ko(e){if(!/^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$/.test(e))throw new Error(`"${e}" is not a valid Android package name.`);return e}function tn(e,n){const t=e?Qr(e):null;if(t)return{bin:t.adb,args:["-P",String(t.port),"-s",e,...n]};return{bin:"adb",args:e?["-s",e,...n]:n}}async function Be(e,n,t=15e3){try{const i=tn(e,n);const{stdout:r}=await Go(i.bin,i.args,{timeout:t,maxBuffer:32*1024*1024});return r}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(Gi);const o=n[0]==="shell"?`shell ${(n[1]??"").split(" ")[0]}`:n.slice(0,2).join(" ");const a=n[0]==="shell"?r.stderr||(r.killed?"timed out":"no error text"):r.stderr||r.message||"";const s=xe(a.replace(/'[^']*'/g,"'\u2026'")).trim().slice(0,300);throw new Error(`adb ${o} failed: ${s}`)}}function J(e,n,t=15e3){return Be(e,["shell",n],t)}async function Vo(e,n,t=15e3){try{const i=tn(e,n);const{stdout:r}=await Go(i.bin,i.args,{timeout:t,encoding:"buffer",maxBuffer:64*1024*1024});return r}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(Gi);throw new Error(`adb ${n.slice(0,2).join(" ")} failed: ${r.message.slice(0,300)}`)}}async function Yo(){const e=[...zc().keys()];try{const n=au(await Be(null,["devices"]));return[...n,...e.filter(t=>!n.includes(t))]}catch(n){if(e.length&&n.message===Gi)return e;throw n}}function au(e){return e.split("\n").slice(1).map(n=>n.trim().split(/\s+/)).filter(n=>n[1]==="device").map(n=>n[0])}async function jn(e){const n=su(await J(e,"wm size"));const t=cu(await J(e,"dumpsys input").catch(()=>""));return t%2===1?{width:n.height,height:n.width}:n}function su(e){const n=/Override size:\s*(\d+)x(\d+)/.exec(e);const t=/Physical size:\s*(\d+)x(\d+)/.exec(e);const i=n??t;if(!i)throw new Error(`Could not read the Android screen size (adb said: ${e.trim().slice(0,120)}).`);return{width:Number(i[1]),height:Number(i[2])}}function cu(e){const n=/SurfaceOrientation:\s*(\d)/.exec(e);return n?Number(n[1]):0}async function Ki(e,n){const t=await J(e,"dumpsys window windows").catch(()=>"");return lu(t,n)}function lu(e,n){const t=e.split(/\n(?=\s*Window #\d+)/);for(const i of t){if(!i.includes(`${n}/`))continue;const r=/mFrame=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(i)??/\bframe=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(i);if(r){const[o,a,s,c]=r.slice(1).map(Number);if(s>o&&c>a)return{x:o,y:a,width:s-o,height:c-a}}}return null}async function du(e){return uu(await J(e,"dumpsys activity activities"))}function uu(e){const n=/(?:topResumedActivity|mResumedActivity|ResumedActivity)[^\n]*?\s([a-zA-Z][\w.]*)\/[\w.$]+/.exec(e);return n?n[1]:null}async function hu(e){const n=await J(e,"pm list packages -3");return n.split("\n").map(t=>t.trim().replace(/^package:/,"")).filter(t=>/^[A-Za-z][\w.]+$/.test(t))}async function Xo(e,n){Ko(n);const t=await J(e,`run-as ${de(n)} cat files/react-buoy/@react_buoy_sync_device_id.json 2>/dev/null`,5e3).catch(()=>"");const i=t.trim().replace(/^"|"$/g,"");if(/^[\w-]{4,64}$/.test(i))return i;const r=await Vo(e,["exec-out",`run-as ${de(n)} sh -c 'cat app_webview/Default/"Local Storage"/leveldb/*.log app_webview/Default/"Local Storage"/leveldb/*.ldb 2>/dev/null'`],5e3).catch(()=>null);return r?pu(r):null}function pu(e){const n=e.toString("latin1");const t=/@react_buoy_sync_device_id([\s\S])\x01([\w-]{4,64})/g;let i=null;for(let r=t.exec(n);r;r=t.exec(n)){const o=r[1].charCodeAt(0)-1;if(o>=4&&o<=r[2].length)i=r[2].slice(0,o)}return i}function fu(e){if(/[^\x20-\x7e]/.test(e))throw new Error("adb can only type plain ASCII text. Use tap_element's text for other characters.");return de(e.replace(/ /g,"%s"))}async function mu(e,n,t,i){const[r,o]=[Math.round(n),Math.round(t)];if(i)await J(e,`input swipe ${r} ${o} ${r} ${o} ${Math.round(i)}`);else await J(e,`input tap ${r} ${o}`)}async function wu(e,n,t,i){const r=[...n,...t].map(o=>Math.round(o)).join(" ");await J(e,`input swipe ${r} ${Math.round(i)}`)}async function gu(e,n){const t=n.replace(/ /g,"\0");for(let i=0;i<t.length;i+=40){const r=t.slice(i,i+40).replace(/\u0000/g," ");await J(e,`input text ${fu(r)}`)}}async function bu(e){const n=await Vo(e,["exec-out","screencap","-p"]);if(n.length<8||n.readUInt32BE(0)!==2303741511)throw new Error("The Android screenshot was not a PNG.");return n}async function Zo(e,n){if(n){await J(e,"settings put global sysui_demo_allowed 1");for(const t of["-e command enter","-e command clock -e hhmm 0941","-e command battery -e level 100 -e plugged false","-e command network -e wifi show -e level 4","-e command notifications -e visible false"]){await J(e,`am broadcast -a com.android.systemui.demo ${t}`).catch(()=>void 0)}}else{await J(e,"am broadcast -a com.android.systemui.demo -e command exit").catch(()=>void 0)}}async function Vi(e){const n=await Be(e,["exec-out","uiautomator","dump","/dev/tty"],2e4);return vu(n)}function vu(e){const n=[];const t=(i,r)=>{const o=new RegExp(`\\s${r}="([^"]*)"`).exec(i);return o?yu(o[1]):""};for(const i of e.matchAll(/<node\b[^>]*>/g)){const r=i[0];const o=/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(t(r,"bounds"));if(!o)continue;const[a,s,c,l]=o.slice(1).map(Number);n.push({text:t(r,"text"),desc:t(r,"content-desc"),id:t(r,"resource-id"),cls:t(r,"class"),clickable:t(r,"clickable")==="true",bounds:{x:a,y:s,width:c-a,height:l-s}})}return n}function yu(e){return e.replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&#(\d+);/g,(n,t)=>String.fromCharCode(Number(t))).replace(/&amp;/g,"&")}function $u(e,n){const t=n.trim().toLowerCase();const i=c=>{let l=0;for(const d of[c.desc,c.text,c.id.split("/").pop()??""]){const u=d.trim().toLowerCase();if(!u)continue;if(u===t)l=Math.max(l,3);else if(u.startsWith(t))l=Math.max(l,2);else if(u.includes(t))l=Math.max(l,1)}return l};const r=e.filter(c=>c.bounds.width>0&&c.bounds.height>0&&i(c)>0);const o=Math.max(0,...r.map(i));const a=r.filter(c=>i(c)===o);let s=a.filter((c,l)=>a.findIndex(d=>JSON.stringify(d.bounds)===JSON.stringify(c.bounds))===l);if(s.length>1){const c=s.filter(l=>[l.desc,l.text,l.id.split("/").pop()??""].some(d=>d.trim()===n.trim()));if(c.length)s=c}if(s.length>1){const c=s.filter(l=>l.clickable||/EditText|Button/.test(l.cls));if(c.length)s=c}return s}var ku=Xd(Yd);var Qo=/^(@buoy\/session:|buoy-gg-license|buoy-gg-device-fingerprint$|@react_buoy_sync_device_id$|@react_buoy_install)/;async function Yi(e){return zi.readdir(e).catch(()=>[])}async function Xi(e){const n=[];const t=At.join(e,"Library","WebKit");for(const i of await Yi(t)){const r=At.join(t,i,"WebsiteData","Default");for(const o of await Yi(r)){for(const a of await Yi(At.join(r,o))){const s=At.join(r,o,a);const c=At.join(s,"LocalStorage","localstorage.sqlite3");if(!await zi.access(c).then(()=>true,()=>false))continue;const l=await zi.readFile(At.join(s,"origin")).then(d=>d.toString("hex"),()=>"");if(l)n.push({db:c,origin:l})}}}return n}var ea=(e,n,t=true)=>ku("sqlite3",[...t?["-readonly"]:[],e,n],{timeout:5e3}).then(i=>i.stdout);async function _u(e){const n=[];for(const{db:t,origin:i}of await Xi(e)){const r=await ea(t,"SELECT key, hex(value) FROM ItemTable;");for(const o of r.split("\n")){const a=o.lastIndexOf("|");if(a<0)continue;const s=o.slice(0,a);if(Qo.test(s))n.push({key:s,value:o.slice(a+1),origin:i})}}return n}async function Su(e,n){if(!n.length)return false;const t=await Xi(e);const i=new Set(n.map(r=>r.origin??""));for(const r of i){const o=r?t.find(s=>s.origin===r):void 0;if(!o)return false;const a=n.filter(s=>(s.origin??"")===r&&/^[0-9A-F]*$/i.test(s.value)).map(s=>`('${s.key.replace(/'/g,"''")}', X'${s.value}')`).join(",");if(a)await ea(o.db,`INSERT OR REPLACE INTO ItemTable (key, value) VALUES ${a};`,false)}return true}async function xu(e,n=15e3){const t=Date.now()+n;while(Date.now()<t){if((await Xi(e)).length)return true;await new Promise(i=>setTimeout(i,500))}return false}async function Tu(e,n,t){const i=(await J(e,`pidof ${n}`).catch(()=>"")).trim().split(/\s+/)[0];if(!i)throw new Error("the app is not running");const r=(await Be(e,["forward","tcp:0",`localabstract:webview_devtools_remote_${i}`])).trim();try{const o=await(await fetch(`http://127.0.0.1:${r}/json`)).json();const a=o.find(s=>s.type==="page"&&s.webSocketDebuggerUrl);if(!a?.webSocketDebuggerUrl)throw new Error("no debuggable WebView page (needs a debug build)");return await new Promise((s,c)=>{const l=new Zd(a.webSocketDebuggerUrl);const d=setTimeout(()=>{l.close();c(new Error("the WebView did not answer"))},5e3);l.on("open",()=>l.send(JSON.stringify({id:1,method:"Runtime.evaluate",params:{expression:t,returnByValue:true}})));l.on("message",u=>{const h=JSON.parse(String(u));if(h.id!==1)return;clearTimeout(d);l.close();if(h.result?.exceptionDetails)c(new Error("the WebView script failed"));else s(h.result?.result?.value)});l.on("error",u=>{clearTimeout(d);c(u)})})}finally{await Be(e,["forward","--remove",`tcp:${r}`]).catch(()=>void 0)}}async function ta(e,n,t,i=15e3){const r=Date.now()+i;let o;while(Date.now()<r){try{return await Tu(e,n,t)}catch(a){o=a;await new Promise(s=>setTimeout(s,700))}}throw o instanceof Error?o:new Error("the WebView did not open")}async function Du(e,n){const t=await J(e,`run-as ${n} sh -c 'ls app_webview/Default/"Local Storage"/leveldb 2>/dev/null'`).catch(()=>"");return/CURRENT|\.log/.test(t)}async function Iu(e,n){if(!(await J(e,`pidof ${n}`).catch(()=>"")).trim()){await J(e,`monkey -p ${n} -c android.intent.category.LAUNCHER 1`)}const t=await ta(e,n,`(() => { const re = ${Qo}; const out = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && re.test(k)) out.push({ key: k, value: localStorage.getItem(k) }); } return out; })()`);return Array.isArray(t)?t.filter(i=>typeof i.key==="string"&&typeof i.value==="string"):[]}async function Mu(e,n,t){await ta(e,n,`(() => { for (const r of ${JSON.stringify(t)}) localStorage.setItem(r.key, r.value); setTimeout(() => location.reload(), 50); return true; })()`)}var na=Pd(Rd);var ot=new Map;function Ln(e){return ot.get(e)}async function We(e,n,t=2e4,i){try{const{stdout:r}=await na(e,n,{timeout:t,maxBuffer:16*1024*1024,env:i?{...process.env,...i}:void 0});return r}catch(r){const o=r;if(o.code==="ENOENT")throw new Error(e==="xcrun"?"This needs macOS with Xcode (xcrun simctl).":`${e} is not installed.`);throw new Error(`${e} ${n.slice(0,2).join(" ")} failed: ${(o.stderr||o.message||"").trim().slice(0,300)}`)}}var we=(e,n)=>We("xcrun",["simctl",...e],n);async function Zi(e,n){const t=await Wc(e,n).catch(()=>({}));return We("xcrun",["simctl","launch",e,n],void 0,t)}var nn=e=>e.split("-").pop()??"";async function Qi(e,n){if(!n){const t=await er(e,["rn"]);return t.length?t:er(e,["webview"])}return er(e,n)}async function er(e,n){let t=[];try{const r=JSON.parse(await we(["list","devices","booted","-j"])).devices;t=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return[]}const i=await Promise.all(t.map(r=>ia(e,r.udid,r.dataPath,n)));return t.flatMap((r,o)=>i[o]?[{udid:r.udid,name:r.name,found:i[o]}]:[])}function tr(e,n){return new Error(`${n.length} booted simulators hold "${e}" (one is probably a clone): ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}. Pass udid to choose, or shut the extra one down.`)}async function ia(e,n,t,i){const r=nn(e);if(!r)return null;if(!t){try{const c=JSON.parse(await we(["list","devices","-j"])).devices;t=Object.values(c).flat().find(l=>l.udid===n)?.dataPath}catch{return null}}if(!t)return null;const o=Y.join(t,"Containers","Data","Application");let a=[];try{a=await ie.readdir(o)}catch{return null}const s=await Promise.all(a.map(c=>nr(Y.join(o,c),r,i)));for(const[c,l]of a.entries()){const d=Y.join(o,l);if(!s[c])continue;let u=null;try{u=(await We("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",Y.join(d,".com.apple.mobile_container_manager.metadata.plist")])).trim()||null}catch{}return{container:d,bundleId:u,kind:s[c]}}return null}async function nr(e,n,t=["rn","webview"]){if(t.includes("rn")){try{if((await ie.readFile(Y.join(e,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim()===n)return"rn"}catch{}}if(!t.includes("webview"))return null;return(await Po(e).catch(()=>[])).includes(n)?"webview":null}async function Au(e,n){const t=nn(e);const i=n.serial?[n.serial]:await Yo();if(i.length===0)throw new Error("No Android device or emulator is connected to adb.");for(const r of i){const o=await du(r).catch(()=>null);const a=await hu(r).catch(()=>[]);const s=[n.bundleId,o,...a].filter((c,l,d)=>!!c&&d.indexOf(c)===l).slice(0,80);for(const c of s){if(await Xo(r,c).catch(()=>null)===t)return{platform:"android",serial:r,packageName:c}}}return null}async function Eu(e){const n=nn(e);for(const t of await qd()){for(const i of await jo(t.udid).catch(()=>[])){if(await qo(t.udid,i.bundleId)===n)return{platform:"ios-device",udid:t.udid,name:t.name,bundleId:i.bundleId}}}return null}var ir=new Map;function rn(e,n,t={}){if(t.udid||t.serial||t.bundleId)return ra(e,n,t);const i=`${e}|${n??""}`;const r=ir.get(i);if(r)return r;const o=ra(e,n,t).finally(()=>ir.delete(i));ir.set(i,o);return o}async function ra(e,n,t={}){if(e==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');const i=ot.get(e);if(i&&!t.udid&&!t.serial&&await aa(e,i)){if(i.platform==="ios"&&!i.picked){const l=await Qi(e,i.idKind?[i.idKind]:void 0);if(l.length>1)throw tr(e,l);if(!l.some(d=>d.udid===i.udid)){ot.delete(e);throw new Error(`The sim no longer holds "${e}". Try the host scan again.`)}}return i}if(i)ot.delete(e);if(n==="android"){const l=await Au(e,t);if(!l){throw new Error(`Could not prove which Android device and app run "${e}". Buoy checks the app's install id with run-as, which needs a debug build of the app on a device adb can reach${t.serial?` (looked on ${t.serial})`:""}.`)}ot.set(e,l);return l}if(n&&n!=="ios")throw new Error(`Device control works on iOS Simulators and Android. This app runs on "${n}".`);if(process.platform!=="darwin")throw new Error("iOS control needs macOS with Xcode.");let r=t.udid;let o=r??"";let a=null;if(!r){const l=await Qi(e);a=l.length===1?l[0]:null;if(l.length>1)throw tr(e,l);const d=l.length===1?{ok:true,udid:l[0].udid,how:`matched ${e} to "${l[0].name}"`}:await Fo(e);if(!d.ok||!/^matched /.test(d.how)){const u=await Eu(e).catch(()=>null);if(u){ot.set(e,u);return u}throw new Error(`Could not find which simulator or iPhone runs "${e}": no booted simulator holds its install id, and no connected iPhone has it (an iPhone must be unlocked, paired, and run a development build).`)}r=d.udid;o=/"([^"]+)"/.exec(d.how)?.[1]??r}const s=a?.udid===r?await nr(a.found.container,nn(e),[a.found.kind])?a.found:null:await ia(e,r);if(!s)throw new Error(`Simulator ${r} does not hold "${e}".`);const c={platform:"ios",udid:r,name:o,bundleId:t.bundleId??s?.bundleId??null,container:s?.container??null,picked:!!t.udid,idKind:s.kind};ot.set(e,c);return c}async function oa(e,n){if(n.platform==="ios"&&!n.picked){const t=await Qi(e);if(t.length>1)throw tr(e,t)}if(await aa(e,n))return n;ot.delete(e);return rn(e,n.platform==="android"?"android":"ios",n.platform==="ios"&&n.picked?{udid:n.udid}:{})}async function aa(e,n){const t=nn(e);if(n.platform==="android")return await Xo(n.serial,n.packageName).catch(()=>null)===t;if(n.platform==="ios-device")return await qo(n.udid,n.bundleId).catch(()=>null)===t;if(n.container)return!!await nr(n.container,t);const i=await Fo(e);return i.ok&&i.udid===n.udid&&/^matched /.test(i.how)}function rr(e){if(e.platform==="android")return Ko(e.packageName);if(e.platform==="ios-device")return e.bundleId;if(!e.bundleId)throw new Error("Could not tell which app this is on the simulator (its container has no bundle metadata).");return e.bundleId}async function on(e){if(e.platform==="android")return bu(e.serial);if(e.platform==="ios-device")return Uo(e.udid);const n=await Bo(e.udid);if(!n.ok||!n.srcPath)throw new Error(n.error??"Screenshot failed.");try{return await ie.readFile(n.srcPath)}finally{await ie.unlink(n.srcPath).catch(()=>{})}}async function Ct(e,n=1280){if(process.platform==="darwin"){const r=Y.join(en.tmpdir(),`buoy-mcp-img-${Nd()}.png`);const o=r.replace(/\.png$/,".jpg");await ie.writeFile(r,e);try{await We("sips",["-Z",String(n),"-s","format","jpeg","-s","formatOptions","70",r,"--out",o],15e3);const a=await We("sips",["-g","pixelWidth","-g","pixelHeight",o]);return{data:(await ie.readFile(o)).toString("base64"),mimeType:"image/jpeg",width:Number(/pixelWidth:\s*(\d+)/.exec(a)?.[1]??0),height:Number(/pixelHeight:\s*(\d+)/.exec(a)?.[1]??0)}}finally{await ie.unlink(r).catch(()=>{});await ie.unlink(o).catch(()=>{})}}const t=Cu(Bi.PNG.sync.read(e),Math.min(n,1024));const i=Bi.PNG.sync.write(t,{colorType:2,deflateLevel:9});return{data:i.toString("base64"),mimeType:"image/png",width:t.width,height:t.height}}function Cu(e,n){const t=Math.min(1,n/Math.max(e.width,e.height));if(t===1)return e;const i=Math.max(1,Math.round(e.width*t));const r=Math.max(1,Math.round(e.height*t));const o=new Bi.PNG({width:i,height:r});const a=e.width/i;const s=e.height/r;for(let c=0;c<r;c++){const l=Math.floor(c*s);const d=Math.max(l+1,Math.floor((c+1)*s));for(let u=0;u<i;u++){const h=Math.floor(u*a);const b=Math.max(h+1,Math.floor((u+1)*a));const f=[0,0,0,0];let w=0;for(let m=l;m<d;m++){for(let p=h;p<b;p++){const y=(m*e.width+p)*4;for(let v=0;v<4;v++)f[v]+=e.data[y+v];w++}}const g=(c*i+u)*4;for(let m=0;m<4;m++)o.data[g+m]=Math.round(f[m]/w)}}return o}var or=new Set;async function Ou(e,n){let t=false;try{if(e.platform==="ios"){const i=await we(["status_bar",e.udid,"list"]).catch(()=>"");const r=i.split(/^=+$/m).slice(1).join("").trim();if(!r){await we(["status_bar",e.udid,"override","--time","9:41","--batteryState","charged","--batteryLevel","100","--wifiBars","3","--cellularBars","4"]);t=true}}else if(e.platform==="android"){const i=(await J(e.serial,"settings get global sysui_demo_allowed").catch(()=>"")).trim();if(i!=="1"&&!or.has(e.serial)){or.add(e.serial);await Zo(e.serial,true);t=true}}if(t)await new Promise(i=>setTimeout(i,400))}catch{t=false}try{return{result:await n(),steadied:t}}finally{if(t){if(e.platform==="ios")await we(["status_bar",e.udid,"clear"]).catch(()=>void 0);else if(e.platform==="android"){await Zo(e.serial,false);await J(e.serial,"settings put global sysui_demo_allowed 0").catch(()=>void 0);or.delete(e.serial)}}}}async function ar(e){const n=rr(e);if(e.platform==="ios-device"){const t=await zo(e.udid,n);return t?`Quit ${n}.`:`${n} was not running.`}if(e.platform==="ios"){await we(["terminate",e.udid,n]).catch(t=>{if(!/found nothing to terminate|not running/i.test(String(t)))throw t})}else{await J(e.serial,`am force-stop ${de(n)}`)}return`Quit ${n}.`}async function Ot(e){const n=rr(e);if(e.platform==="ios")await Zi(e.udid,n);else if(e.platform==="ios-device")await Ui(e.udid,n);else await J(e.serial,`monkey -p ${de(n)} -c android.intent.category.LAUNCHER 1`);return`Launched ${n}.`}async function Ru(e){if(e.platform==="ios-device"){await Ui(e.udid,"com.apple.Preferences");return"Sent to the background for real (Settings opened on top)."}if(e.platform==="ios"){await we(["launch",e.udid,"com.apple.Preferences"]);return"Sent to the background for real (Settings opened on top)."}await J(e.serial,"input keyevent 3");return"Sent to the background for real (pressed Home)."}async function Pu(e,n){if(!/^[a-zA-Z][\w+.-]*:/.test(n))throw new Error(`"${n}" is not a link with a scheme (like https://\u2026 or myapp://\u2026).`);if(e.platform==="ios-device"){await Hd(e.udid,n);return"Opened through the OS on the iPhone."}if(e.platform==="ios"){await we(["openurl",e.udid,n]);return'Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.'}await J(e.serial,`am start -a android.intent.action.VIEW -d ${de(n)}`);return"Opened through the OS (an Android VIEW intent)."}var Nu=["buoy-license","react-buoy/@react_buoy_sync_device_id.json","react-buoy/@react_buoy_install.json"];async function sa(e){const n=Y.join(e,"Info.plist");const t=a=>We("plutil",["-extract",a,"raw","-o","-",n]).then(s=>s.trim()).catch(()=>"");const i=await t("CFBundleIdentifier");if(!i)throw new Error(`${e} has no Info.plist with a bundle id. Pass a built .app folder.`);const r=(await We("plutil",["-extract","CFBundleSupportedPlatforms","json","-o","-",n]).catch(()=>"[]")).toLowerCase();const o=(await t("DTPlatformName")).toLowerCase();return{bundleId:i,simulator:r.includes("iphonesimulator")||o==="iphonesimulator"}}async function Fu(e){const n=[process.env.ANDROID_HOME,process.env.ANDROID_SDK_ROOT,Y.join(en.homedir(),"Library","Android","sdk"),Y.join(en.homedir(),"Android","Sdk")].filter(t=>!!t);for(const t of n){const i=Y.join(t,"build-tools");const r=(await ie.readdir(i).catch(()=>[])).sort().reverse();for(const o of r){for(const a of["aapt2","aapt"]){const s=Y.join(i,o,a);try{await ie.access(s)}catch{continue}const c=await We(s,a==="aapt2"?["dump","packagename",e]:["dump","badging",e]).catch(()=>"");const l=a==="aapt2"?c.trim().split("\n")[0]:/package: name='([^']+)'/.exec(c)?.[1]??"";if(l)return l}}}throw new Error("Could not read the APK's package name (needs aapt2 from the Android SDK build-tools). Nothing was changed.")}async function Bu(e,n,t=true){const i=rr(e);const r=Y.resolve(n);try{await ie.access(r)}catch{throw new Error(`No build at ${r}. Pass appPath: the built .app folder (iOS Simulator) or .apk file (Android). Nothing was changed.`)}if(e.platform==="android")return ju(e,i,r,t);if(e.platform==="ios-device")return Lu(e,i,r,t);if(!/\.app\/?$/i.test(r))throw new Error("On the iOS Simulator, appPath must be a built .app folder. Nothing was changed.");const o=await sa(r);if(o.bundleId!==i)throw new Error(`${Y.basename(r)} is ${o.bundleId}, but this app is ${i}. Nothing was changed.`);if(!o.simulator)throw new Error(`${Y.basename(r)} is a device build, not a simulator build. Nothing was changed.`);const a=t?await ie.mkdtemp(Y.join(en.tmpdir(),"buoy-mcp-keep-")):null;try{let s=false;let c=[];if(a){await ie.chmod(a,448);const d=(await we(["get_app_container",e.udid,i,"data"]).catch(()=>"")).trim();if(d)s=await ca(Y.join(d,"Documents"),a);if(d&&!s){try{c=await _u(d)}catch(u){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView storage (${u.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}}await we(["terminate",e.udid,i]).catch(()=>void 0);await we(["uninstall",e.udid,i]);try{await we(["install",e.udid,r],18e4)}catch(d){throw new Error(`The app was removed, but installing ${Y.basename(r)} failed: ${d.message}. Install it again by hand.`)}let l="";if(a&&s){try{const d=(await we(["get_app_container",e.udid,i,"data"])).trim();await ca(a,Y.join(d,"Documents"));e.container=d;l=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(d){l=` Buoy's sign-in could not be restored (${d.message}): sign in to Buoy again in the app.`}}else if(c.length){try{const d=(await we(["get_app_container",e.udid,i,"data"])).trim();await Zi(e.udid,i);const u=await xu(d);await we(["terminate",e.udid,i]).catch(()=>void 0);if(!u||!await Su(d,c))throw new Error("the app made no WebView storage");e.container=d;l=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(d){l=` Buoy's sign-in could not be restored (${d.message}): sign in to Buoy again in the app.`}}else if(t){l=" The old install had no Buoy sign-in to keep."}await Zi(e.udid,i);return`Reinstalled ${i} from ${r} and launched it. The app's own data is gone, like a new user.${l}`}finally{if(a)await ie.rm(a,{recursive:true,force:true}).catch(()=>void 0)}}async function ca(e,n){let t=false;for(const i of Nu){const r=Y.join(e,i);try{await ie.access(r)}catch{continue}const o=Y.join(n,i);await ie.mkdir(Y.dirname(o),{recursive:true});await ie.cp(r,o,{recursive:true,force:true});t=true}return t}async function ju(e,n,t,i){if(!/\.apk$/i.test(t))throw new Error("On Android, appPath must be an .apk file. Nothing was changed.");const r=await Fu(t);if(r!==n)throw new Error(`${Y.basename(t)} is ${r}, but this app is ${n}. Nothing was changed.`);const o=[];if(i){const c=await J(e.serial,`run-as ${de(n)} find files/buoy-license files/react-buoy -maxdepth 1 -type f 2>/dev/null`).catch(()=>"");const l=c.split("\n").map(d=>d.trim()).filter(d=>d.startsWith("files/buoy-license/")||/react-buoy\/@react_buoy_(sync_device_id|install)\.json$/.test(d));for(const d of l){const u=await J(e.serial,`run-as ${de(n)} base64 ${de(d)}`).catch(()=>"");if(u.trim())o.push({rel:d,data:u.replace(/\s+/g,"")})}}let a=[];if(i&&!o.length&&await Du(e.serial,n)){try{a=await Iu(e.serial,n)}catch(c){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView (${c.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}await Be(e.serial,["install","-r","-d",t],18e4);await J(e.serial,`pm clear ${de(n)}`);let s="";if(a.length){await Ot(e);try{await Mu(e.serial,n,a);s=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){s=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${s}`}if(o.length){try{for(const c of o){const l=Y.posix.dirname(c.rel);await J(e.serial,`echo ${de(c.data)} | run-as ${de(n)} sh -c ${de(`mkdir -p ${de(l)} && base64 -d > ${de(c.rel)}`)}`)}s=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){s=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}}else if(i){s=" No Buoy sign-in could be read to keep (needs a debug build): sign in to Buoy again in the app."}await Ot(e);return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${s}`}async function Lu(e,n,t,i){if(!/\.app\/?$/i.test(t))throw new Error("On an iPhone, appPath must be a built .app folder (a device build). Nothing was changed.");const r=await sa(t);if(r.bundleId!==n)throw new Error(`${Y.basename(t)} is ${r.bundleId}, but this app is ${n}. Nothing was changed.`);if(r.simulator)throw new Error(`${Y.basename(t)} is a simulator build, not a device build. Nothing was changed.`);const o=["Documents/buoy-license/buoy-gg-license.json","Documents/react-buoy/@react_buoy_sync_device_id.json","Documents/react-buoy/@react_buoy_install.json"];const a=i?await ie.mkdtemp(Y.join(en.tmpdir(),"buoy-mcp-keep-")):null;try{const s=[];if(a){await ie.chmod(a,448);for(const l of o){let d;try{d=await Lo(e.udid,n,l)}catch(h){throw new Error(`Could not back up Buoy's ${Y.basename(l)} from the iPhone (${h.message}). Nothing was changed.`)}if(!d)continue;const u=Y.join(a,l.replace(/\//g,"__"));await ie.writeFile(u,d,{mode:384});s.push({rel:l,local:u})}}await zo(e.udid,n).catch(()=>false);await Jd(e.udid,n);try{await Gd(e.udid,t)}catch(l){throw new Error(`The app was removed, but installing ${Y.basename(t)} failed: ${l.message}. Install it again from Xcode.`)}let c="";if(s.length){try{for(const u of s)await Kd(e.udid,n,u.local,u.rel);const l=s.some(u=>u.rel.includes("buoy-license"));const d=s.some(u=>u.rel.includes("sync_device_id"));c=l?` Kept Buoy's sign-in${d?" and device id, so the same deviceId still works":""}.`:` Kept the device id; there was no Buoy sign-in to keep, so sign in to Buoy in the app.`}catch(l){c=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(i)c=" The old install had no Buoy sign-in to keep.";await Ui(e.udid,n);return`Reinstalled ${n} on the iPhone and launched it. The app's own data is gone, like a new user.${c}`}finally{if(a)await ie.rm(a,{recursive:true,force:true}).catch(()=>void 0)}}async function qu(e){const n=JSON.parse(await we(["list","devices","available","-j"])).devices;const t=Object.entries(n).filter(([s])=>/iOS/.test(s)).flatMap(([,s])=>s);const i=e.trim().toLowerCase();const r=t.filter(s=>s.udid.toLowerCase()===i||s.name.toLowerCase()===i);const o=r.length?r:t.filter(s=>s.name.toLowerCase().includes(i));if(o.length===0)throw new Error(`No iOS Simulator matches "${e}". Run \`xcrun simctl list devices available\` to see them.`);if(o.length>1)throw new Error(`${o.length} simulators match "${e}": ${o.map(s=>`${s.name} (${s.udid})`).join(", ")}. Pass the udid.`);const a=o[0];if(a.state!=="Booted"){try{await na("xcrun",["simctl","boot",a.udid],{timeout:12e4,env:Uc()})}catch(s){const c=s;throw new Error(`xcrun simctl boot failed: ${(c.stderr||c.message||"").trim().slice(0,300)}`)}}await We("open",["-a","Simulator","--args","-CurrentDeviceUDID",a.udid]).catch(()=>void 0);return a.state==="Booted"?`${a.name} (${a.udid}) was already booted.`:`Booted ${a.name} (${a.udid}).`}var Uu=Od(Cd);async function ge(e,n,t={},i={}){if(n==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');let r;let o=true;try{r=e.requireProDevice(n)}catch(c){const l=c instanceof Error?c.message:String(c);const d=l.startsWith(pt)&&/is not connected right now/.test(l)?/"([^"]+)"/.exec(l)?.[1]:void 0;const u=d?e.deviceWasAllowed(d):false;if(!i.allowOffline||!d||!u)throw c;r=d;o=false}const a=e.listDevices().find(c=>c.deviceId===r)?.platform;let s=await rn(r,a,t);if(i.changesApp&&Ln(r)===s)s=await oa(r,s);return{id:r,host:s,online:o}}function Rt(e){if(e.platform==="ios")return`simulator "${e.name}"`;if(e.platform==="ios-device")return`iPhone "${e.name}"`;return`Android device ${e.serial}`}var sr=new Map;function la(e){if(!sr.has(e))sr.set(e,Uu("which",[e],{timeout:3e3}).then(()=>true,()=>false));return sr.get(e)}async function zu(e,n){let t=n;if(e.platform==="android"){const r=Ln(e.deviceId);const o=r??(e.isConnected?await Promise.race([rn(e.deviceId,"android").catch(()=>void 0),new Promise(s=>setTimeout(()=>s(void 0),3e3))]):void 0);const a=o?.platform==="android"&&!!Qr(o.serial)||await la("adb");return{kind:"android",...o&&o.platform==="android"?{id:o.serial,name:o.packageName}:{},can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall","real lifecycle events","native_tap/type/swipe","native_inspect"],...!a?{missing:["adb (Android SDK platform-tools)"]}:!o?{missing:["proof of which phone runs it (needs a debug build reachable by adb)"]}:{}}}if(e.platform!=="ios")return{kind:e.platform??"unknown",can:[],missing:["host control works on iOS and Android apps"]};let i=Ln(e.deviceId);if(!i&&!t&&e.isConnected){i=await Promise.race([rn(e.deviceId,"ios").catch(()=>void 0),new Promise(r=>setTimeout(()=>r(void 0),4e3))])}if(!t&&i?.platform==="ios")t={name:i.name,udid:i.udid};if(i?.platform==="ios-device"){return{kind:"physical iPhone",id:i.udid,name:i.name,can:["screenshot","screen_recording (if this iPhone supports it)","baseline diffs","quit/launch/reinstall","open_url","native_tap/type/swipe/pinch (by query, testID, nativeTag or x/y; not label)","native_inspect (the app's side only)"]}}if(t){const r=await la("idb");return{kind:"ios simulator",id:t.udid,name:t.name,can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall/boot","real lifecycle events",...r?["native_tap/type/swipe/pinch","native_inspect"]:[]],...r?{}:{missing:["idb for real touches (brew install facebook/fb/idb-companion && pipx install fb-idb)"]}}}return void 0}function Pt(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function Nt(e){return{isError:true,content:[{type:"text",text:e}]}}function da(e,n){e.registerTool("list_devices",{description:"List the apps connected to Buoy: id, name, platform, whether it is connected and has MCP access, the simulator or phone it runs on with the host controls that work there (screenshot, recording, app control, real touches) and anything missing, and the Buoy tools it exposes. Start here. Pass details:true for every tool's action names.",inputSchema:{details:te.boolean().optional().describe("Also list every action of every tool. Default false (tool names only); list_tool_actions gives one tool's actions."),includeDisconnected:te.boolean().optional().describe("Also list apps that were seen before but are not connected now. Default false.")}},async(t={})=>{const i=await Eo();let r=n.listDevices();for(let b=0;r.length===0&&b<10;b++){await new Promise(f=>setTimeout(f,200));r=n.listDevices()}if(r.length===0){return Pt({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...i?{updateAvailable:i}:{}})}const o=t.includeDisconnected?0:r.filter(b=>!b.isConnected).length;if(!t.includeDisconnected)r=r.filter(b=>b.isConnected);if(r.length===0){return Pt({devices:[],hint:`No app is connected right now${o?` (${o} seen before; pass includeDisconnected:true to list them)`:""}. Open the app with Buoy devtools running, then retry.`,...i?{updateAvailable:i}:{}})}const a=await Ad(r.filter(b=>b.platform==="ios").map(b=>b.deviceId)).catch(()=>new Map);const s=r.filter(b=>b.isConnected);const c=s.some(b=>fe(b));const l=s.some(b=>b.tier!=="locked");const d=s.filter(b=>b.collision);const u=await Promise.all(r.map(b=>b.isConnected?zu(b,a.get(b.deviceId)).catch(()=>void 0):void 0));const h=r.map((b,f)=>{const w=a.get(b.deviceId);const g=u[f];return{deviceId:b.deviceId,deviceName:b.deviceName,platform:b.platform,isConnected:b.isConnected,isPro:b.isPro,tier:b.tier,...!b.isPro&&fe(b)?{weekendPass:true}:{},access:n.accessState(b.deviceId,"free"),...w?{simulator:w}:{},...g?{host:g}:{},...b.collision?{collision:true}:{},tools:t.details?b.tools:b.tools.map(m=>m.toolId)}});return Pt({devices:h,...t.details?{}:{note:`Tool names only. Pass details:true, or list_tool_actions({toolId}), for action names.${o?` ${o} apps seen before but not connected now are left out (includeDisconnected:true lists them).`:""}`},...d.length>0?{warning:`${d.map(b=>`"${b.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.`}:{},...c?{}:{hint:l?`The connected app is on the free tier, so only the free MCP tools work: ${Xc().join(", ")} (get_network_requests without includeBodies; get_triage without storage values). Every other tool needs Buoy Pro (https://buoy.gg/pricing). To add a license, enter it 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."},...i?{updateAvailable:i}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a tool exposes, and which connected devices offer it. Leave toolId out to list every tool's actions. Use the action names with call_action.",inputSchema:{toolId:te.string().optional().describe('The tool id, e.g. "perf-monitor", "network", "react-query". Leave out for every tool.'),tool:te.string().optional().describe("Same as toolId."),deviceId:te.string().optional().describe("Only list what this device offers.")}},async({toolId:t,tool:i,deviceId:r})=>{const o=t??i;if(!o){const c=n.connectedDevices().filter(d=>!r||d.deviceId===r);if(c.length===0)return Nt(r?`No connected device "${r}". Call list_devices.`:"No devices are connected. Call list_devices.");const l=c.map(d=>[`**${d.deviceName}** (${d.deviceId})`,...d.tools.map(u=>`- ${u.toolId}: ${u.actions.join(", ")}`)].join("\n"));return{content:[{type:"text",text:`${l.join("\n\n")}
1
+ import{createRequire as Yc}from"module";const qw=Yc(import.meta.url);import{PACKAGE_NAME as so,VERSION as Nn}from"./chunk-ZHEXLBAX.mjs";import{accountRequired as Fn,farmFairness as Xc,hasBuoyAccount as Bn,registerSimFarmTools as Zc}from"./chunk-YBJGCX7S.mjs";import{bootEnv as Qc,coreInfo as el,ensureKeeper as tl,farmAdbTarget as co,farmAdbTargets as nl,farmHome as il,findShell as rl,installShell as ol,keeperAlive as Ct,keeperCall as pt,simLaunchEnv as al,suggestCounts as sl}from"./chunk-LOGSZBZZ.mjs";import{describeDevices as cl,describeDiagnosis as ll,describeInputs as dl,entitlementHasPro as jn,proRequired as ul,readEntitlement as hl,run as Be}from"./chunk-FRGTROJC.mjs";import{accessFor as pl,currentAccess as ft,freeToolNames as fl,hasProAccess as fe,isFreeAppCall as ml,meetsAccess as Ln,outsideCalls as wl,proRequiredMessage as rn,runWithAccess as lo}from"./chunk-PAOWVPJM.mjs";import{freeComparison as gl,freeJsTop as bl,freeRanking as Bi,freeWebBench as vl,nothingLocked as qn,withLockedLines as Et}from"./chunk-OP23W722.mjs";import{GPU_RENDERER_JS as yl,caseUrl as uo,closeWithin as Ot,formatWebBenchMarkdown as $l,isSoftwareGpu as ho,loadChromium as po,runWebBench as kl,shuffled as _l,snapRefreshRate as Sl,waitForHostLock as fo,withDeadline as mo}from"./chunk-DVRR7W22.mjs";import{__commonJS as ee,__require as me,__toESM as Un}from"./chunk-MTTWR4T7.mjs";var wo=ee({"../../node_modules/pngjs/lib/chunkstream.js"(e,n){"use strict";var t=me("util");var i=me("stream");var r=n.exports=function(){i.call(this);this._buffers=[];this._buffered=0;this._reads=[];this._paused=false;this._encoding="utf8";this.writable=true};t.inherits(r,i);r.prototype.read=function(o,a){this._reads.push({length:Math.abs(o),allowLess:o<0,func:a});process.nextTick(function(){this._process();if(this._paused&&this._reads&&this._reads.length>0){this._paused=false;this.emit("drain")}}.bind(this))};r.prototype.write=function(o,a){if(!this.writable){this.emit("error",new Error("Stream not writable"));return false}let s;if(Buffer.isBuffer(o)){s=o}else{s=Buffer.from(o,a||this._encoding)}this._buffers.push(s);this._buffered+=s.length;this._process();if(this._reads&&this._reads.length===0){this._paused=true}return this.writable&&!this._paused};r.prototype.end=function(o,a){if(o){this.write(o,a)}this.writable=false;if(!this._buffers){return}if(this._buffers.length===0){this._end()}else{this._buffers.push(null);this._process()}};r.prototype.destroySoon=r.prototype.end;r.prototype._end=function(){if(this._reads.length>0){this.emit("error",new Error("Unexpected end of input"))}this.destroy()};r.prototype.destroy=function(){if(!this._buffers){return}this.writable=false;this._reads=null;this._buffers=null;this.emit("close")};r.prototype._processReadAllowingLess=function(o){this._reads.shift();let a=this._buffers[0];if(a.length>o.length){this._buffered-=o.length;this._buffers[0]=a.slice(o.length);o.func.call(this,a.slice(0,o.length))}else{this._buffered-=a.length;this._buffers.shift();o.func.call(this,a)}};r.prototype._processRead=function(o){this._reads.shift();let a=0;let s=0;let c=Buffer.alloc(o.length);while(a<o.length){let l=this._buffers[s++];let d=Math.min(l.length,o.length-a);l.copy(c,a,0,d);a+=d;if(d!==l.length){this._buffers[--s]=l.slice(d)}}if(s>0){this._buffers.splice(0,s)}this._buffered-=o.length;o.func.call(this,c)};r.prototype._process=function(){try{while(this._buffered>0&&this._reads&&this._reads.length>0){let o=this._reads[0];if(o.allowLess){this._processReadAllowingLess(o)}else if(this._buffered>=o.length){this._processRead(o)}else{break}}if(this._buffers&&!this.writable){this._end()}}catch(o){this.emit("error",o)}}}});var go=ee({"../../node_modules/pngjs/lib/interlace.js"(e){"use strict";var n=[{x:[0],y:[0]},{x:[4],y:[0]},{x:[0,4],y:[4]},{x:[2,6],y:[0,4]},{x:[0,2,4,6],y:[2,6]},{x:[1,3,5,7],y:[0,2,4,6]},{x:[0,1,2,3,4,5,6,7],y:[1,3,5,7]}];e.getImagePasses=function(t,i){let r=[];let o=t%8;let a=i%8;let s=(t-o)/8;let c=(i-a)/8;for(let l=0;l<n.length;l++){let d=n[l];let u=s*d.x.length;let h=c*d.y.length;for(let b=0;b<d.x.length;b++){if(d.x[b]<o){u++}else{break}}for(let b=0;b<d.y.length;b++){if(d.y[b]<a){h++}else{break}}if(u>0&&h>0){r.push({width:u,height:h,index:l})}}return r};e.getInterlaceIterator=function(t){return function(i,r,o){let a=i%n[o].x.length;let s=(i-a)/n[o].x.length*8+n[o].x[a];let c=r%n[o].y.length;let l=(r-c)/n[o].y.length*8+n[o].y[c];return s*4+l*t*4}}}});var bo=ee({"../../node_modules/pngjs/lib/paeth-predictor.js"(e,n){"use strict";n.exports=function t(i,r,o){let a=i+r-o;let s=Math.abs(a-i);let c=Math.abs(a-r);let l=Math.abs(a-o);if(s<=c&&s<=l){return i}if(c<=l){return r}return o}}});var vo=ee({"../../node_modules/pngjs/lib/filter-parse.js"(e,n){"use strict";var t=go();var i=bo();function r(a,s,c){let l=a*s;if(c!==8){l=Math.ceil(l/(8/c))}return l}var o=n.exports=function(a,s){let c=a.width;let l=a.height;let d=a.interlace;let u=a.bpp;let h=a.depth;this.read=s.read;this.write=s.write;this.complete=s.complete;this._imageIndex=0;this._images=[];if(d){let b=t.getImagePasses(c,l);for(let f=0;f<b.length;f++){this._images.push({byteWidth:r(b[f].width,u,h),height:b[f].height,lineIndex:0})}}else{this._images.push({byteWidth:r(c,u,h),height:l,lineIndex:0})}if(h===8){this._xComparison=u}else if(h===16){this._xComparison=u*2}else{this._xComparison=1}};o.prototype.start=function(){this.read(this._images[this._imageIndex].byteWidth+1,this._reverseFilterLine.bind(this))};o.prototype._unFilterType1=function(a,s,c){let l=this._xComparison;let d=l-1;for(let u=0;u<c;u++){let h=a[1+u];let b=u>d?s[u-l]:0;s[u]=h+b}};o.prototype._unFilterType2=function(a,s,c){let l=this._lastLine;for(let d=0;d<c;d++){let u=a[1+d];let h=l?l[d]:0;s[d]=u+h}};o.prototype._unFilterType3=function(a,s,c){let l=this._xComparison;let d=l-1;let u=this._lastLine;for(let h=0;h<c;h++){let b=a[1+h];let f=u?u[h]:0;let w=h>d?s[h-l]:0;let g=Math.floor((w+f)/2);s[h]=b+g}};o.prototype._unFilterType4=function(a,s,c){let l=this._xComparison;let d=l-1;let u=this._lastLine;for(let h=0;h<c;h++){let b=a[1+h];let f=u?u[h]:0;let w=h>d?s[h-l]:0;let g=h>d&&u?u[h-l]:0;let m=i(w,f,g);s[h]=b+m}};o.prototype._reverseFilterLine=function(a){let s=a[0];let c;let l=this._images[this._imageIndex];let d=l.byteWidth;if(s===0){c=a.slice(1,d+1)}else{c=Buffer.alloc(d);switch(s){case 1:this._unFilterType1(a,c,d);break;case 2:this._unFilterType2(a,c,d);break;case 3:this._unFilterType3(a,c,d);break;case 4:this._unFilterType4(a,c,d);break;default:throw new Error("Unrecognised filter type - "+s)}}this.write(c);l.lineIndex++;if(l.lineIndex>=l.height){this._lastLine=null;this._imageIndex++;l=this._images[this._imageIndex]}else{this._lastLine=c}if(l){this.read(l.byteWidth+1,this._reverseFilterLine.bind(this))}else{this._lastLine=null;this.complete()}}}});var xl=ee({"../../node_modules/pngjs/lib/filter-parse-async.js"(e,n){"use strict";var t=me("util");var i=wo();var r=vo();var o=n.exports=function(a){i.call(this);let s=[];let c=this;this._filter=new r(a,{read:this.read.bind(this),write:function(l){s.push(l)},complete:function(){c.emit("complete",Buffer.concat(s))}});this._filter.start()};t.inherits(o,i)}});var on=ee({"../../node_modules/pngjs/lib/constants.js"(e,n){"use strict";n.exports={PNG_SIGNATURE:[137,80,78,71,13,10,26,10],TYPE_IHDR:1229472850,TYPE_IEND:1229278788,TYPE_IDAT:1229209940,TYPE_PLTE:1347179589,TYPE_tRNS:1951551059,TYPE_gAMA:1732332865,COLORTYPE_GRAYSCALE:0,COLORTYPE_PALETTE:1,COLORTYPE_COLOR:2,COLORTYPE_ALPHA:4,COLORTYPE_PALETTE_COLOR:3,COLORTYPE_COLOR_ALPHA:6,COLORTYPE_TO_BPP_MAP:{0:1,2:3,3:1,4:2,6:4},GAMMA_DIVISION:1e5}}});var yo=ee({"../../node_modules/pngjs/lib/crc.js"(e,n){"use strict";var t=[];(function(){for(let r=0;r<256;r++){let o=r;for(let a=0;a<8;a++){if(o&1){o=3988292384^o>>>1}else{o=o>>>1}}t[r]=o}})();var i=n.exports=function(){this._crc=-1};i.prototype.write=function(r){for(let o=0;o<r.length;o++){this._crc=t[(this._crc^r[o])&255]^this._crc>>>8}return true};i.prototype.crc32=function(){return this._crc^-1};i.crc32=function(r){let o=-1;for(let a=0;a<r.length;a++){o=t[(o^r[a])&255]^o>>>8}return o^-1}}});var $o=ee({"../../node_modules/pngjs/lib/parser.js"(e,n){"use strict";var t=on();var i=yo();var r=n.exports=function(o,a){this._options=o;o.checkCRC=o.checkCRC!==false;this._hasIHDR=false;this._hasIEND=false;this._emittedHeadersFinished=false;this._palette=[];this._colorType=0;this._chunks={};this._chunks[t.TYPE_IHDR]=this._handleIHDR.bind(this);this._chunks[t.TYPE_IEND]=this._handleIEND.bind(this);this._chunks[t.TYPE_IDAT]=this._handleIDAT.bind(this);this._chunks[t.TYPE_PLTE]=this._handlePLTE.bind(this);this._chunks[t.TYPE_tRNS]=this._handleTRNS.bind(this);this._chunks[t.TYPE_gAMA]=this._handleGAMA.bind(this);this.read=a.read;this.error=a.error;this.metadata=a.metadata;this.gamma=a.gamma;this.transColor=a.transColor;this.palette=a.palette;this.parsed=a.parsed;this.inflateData=a.inflateData;this.finished=a.finished;this.simpleTransparency=a.simpleTransparency;this.headersFinished=a.headersFinished||function(){}};r.prototype.start=function(){this.read(t.PNG_SIGNATURE.length,this._parseSignature.bind(this))};r.prototype._parseSignature=function(o){let a=t.PNG_SIGNATURE;for(let s=0;s<a.length;s++){if(o[s]!==a[s]){this.error(new Error("Invalid file signature"));return}}this.read(8,this._parseChunkBegin.bind(this))};r.prototype._parseChunkBegin=function(o){let a=o.readUInt32BE(0);let s=o.readUInt32BE(4);let c="";for(let d=4;d<8;d++){c+=String.fromCharCode(o[d])}let l=Boolean(o[4]&32);if(!this._hasIHDR&&s!==t.TYPE_IHDR){this.error(new Error("Expected IHDR on beggining"));return}this._crc=new i;this._crc.write(Buffer.from(c));if(this._chunks[s]){return this._chunks[s](a)}if(!l){this.error(new Error("Unsupported critical chunk type "+c));return}this.read(a+4,this._skipChunk.bind(this))};r.prototype._skipChunk=function(){this.read(8,this._parseChunkBegin.bind(this))};r.prototype._handleChunkEnd=function(){this.read(4,this._parseChunkEnd.bind(this))};r.prototype._parseChunkEnd=function(o){let a=o.readInt32BE(0);let s=this._crc.crc32();if(this._options.checkCRC&&s!==a){this.error(new Error("Crc error - "+a+" - "+s));return}if(!this._hasIEND){this.read(8,this._parseChunkBegin.bind(this))}};r.prototype._handleIHDR=function(o){this.read(o,this._parseIHDR.bind(this))};r.prototype._parseIHDR=function(o){this._crc.write(o);let a=o.readUInt32BE(0);let s=o.readUInt32BE(4);let c=o[8];let l=o[9];let d=o[10];let u=o[11];let h=o[12];if(c!==8&&c!==4&&c!==2&&c!==1&&c!==16){this.error(new Error("Unsupported bit depth "+c));return}if(!(l in t.COLORTYPE_TO_BPP_MAP)){this.error(new Error("Unsupported color type"));return}if(d!==0){this.error(new Error("Unsupported compression method"));return}if(u!==0){this.error(new Error("Unsupported filter method"));return}if(h!==0&&h!==1){this.error(new Error("Unsupported interlace method"));return}this._colorType=l;let b=t.COLORTYPE_TO_BPP_MAP[this._colorType];this._hasIHDR=true;this.metadata({width:a,height:s,depth:c,interlace:Boolean(h),palette:Boolean(l&t.COLORTYPE_PALETTE),color:Boolean(l&t.COLORTYPE_COLOR),alpha:Boolean(l&t.COLORTYPE_ALPHA),bpp:b,colorType:l});this._handleChunkEnd()};r.prototype._handlePLTE=function(o){this.read(o,this._parsePLTE.bind(this))};r.prototype._parsePLTE=function(o){this._crc.write(o);let a=Math.floor(o.length/3);for(let s=0;s<a;s++){this._palette.push([o[s*3],o[s*3+1],o[s*3+2],255])}this.palette(this._palette);this._handleChunkEnd()};r.prototype._handleTRNS=function(o){this.simpleTransparency();this.read(o,this._parseTRNS.bind(this))};r.prototype._parseTRNS=function(o){this._crc.write(o);if(this._colorType===t.COLORTYPE_PALETTE_COLOR){if(this._palette.length===0){this.error(new Error("Transparency chunk must be after palette"));return}if(o.length>this._palette.length){this.error(new Error("More transparent colors than palette size"));return}for(let a=0;a<o.length;a++){this._palette[a][3]=o[a]}this.palette(this._palette)}if(this._colorType===t.COLORTYPE_GRAYSCALE){this.transColor([o.readUInt16BE(0)])}if(this._colorType===t.COLORTYPE_COLOR){this.transColor([o.readUInt16BE(0),o.readUInt16BE(2),o.readUInt16BE(4)])}this._handleChunkEnd()};r.prototype._handleGAMA=function(o){this.read(o,this._parseGAMA.bind(this))};r.prototype._parseGAMA=function(o){this._crc.write(o);this.gamma(o.readUInt32BE(0)/t.GAMMA_DIVISION);this._handleChunkEnd()};r.prototype._handleIDAT=function(o){if(!this._emittedHeadersFinished){this._emittedHeadersFinished=true;this.headersFinished()}this.read(-o,this._parseIDAT.bind(this,o))};r.prototype._parseIDAT=function(o,a){this._crc.write(a);if(this._colorType===t.COLORTYPE_PALETTE_COLOR&&this._palette.length===0){throw new Error("Expected palette not found")}this.inflateData(a);let s=o-a.length;if(s>0){this._handleIDAT(s)}else{this._handleChunkEnd()}};r.prototype._handleIEND=function(o){this.read(o,this._parseIEND.bind(this))};r.prototype._parseIEND=function(o){this._crc.write(o);this._hasIEND=true;this._handleChunkEnd();if(this.finished){this.finished()}}}});var ko=ee({"../../node_modules/pngjs/lib/bitmapper.js"(e){"use strict";var n=go();var t=[function(){},function(s,c,l,d){if(d===c.length){throw new Error("Ran out of data")}let u=c[d];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=255},function(s,c,l,d){if(d+1>=c.length){throw new Error("Ran out of data")}let u=c[d];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=c[d+1]},function(s,c,l,d){if(d+2>=c.length){throw new Error("Ran out of data")}s[l]=c[d];s[l+1]=c[d+1];s[l+2]=c[d+2];s[l+3]=255},function(s,c,l,d){if(d+3>=c.length){throw new Error("Ran out of data")}s[l]=c[d];s[l+1]=c[d+1];s[l+2]=c[d+2];s[l+3]=c[d+3]}];var i=[function(){},function(s,c,l,d){let u=c[0];s[l]=u;s[l+1]=u;s[l+2]=u;s[l+3]=d},function(s,c,l){let d=c[0];s[l]=d;s[l+1]=d;s[l+2]=d;s[l+3]=c[1]},function(s,c,l,d){s[l]=c[0];s[l+1]=c[1];s[l+2]=c[2];s[l+3]=d},function(s,c,l){s[l]=c[0];s[l+1]=c[1];s[l+2]=c[2];s[l+3]=c[3]}];function r(s,c){let l=[];let d=0;function u(){if(d===s.length){throw new Error("Ran out of data")}let h=s[d];d++;let b,f,w,g,m,p,y,v;switch(c){default:throw new Error("unrecognised depth");case 16:y=s[d];d++;l.push((h<<8)+y);break;case 4:y=h&15;v=h>>4;l.push(v,y);break;case 2:m=h&3;p=h>>2&3;y=h>>4&3;v=h>>6&3;l.push(v,y,p,m);break;case 1:b=h&1;f=h>>1&1;w=h>>2&1;g=h>>3&1;m=h>>4&1;p=h>>5&1;y=h>>6&1;v=h>>7&1;l.push(v,y,p,m,g,w,f,b);break}}return{get:function(h){while(l.length<h){u()}let b=l.slice(0,h);l=l.slice(h);return b},resetAfterLine:function(){l.length=0},end:function(){if(d!==s.length){throw new Error("extra data found")}}}}function o(s,c,l,d,u,h){let b=s.width;let f=s.height;let w=s.index;for(let g=0;g<f;g++){for(let m=0;m<b;m++){let p=l(m,g,w);t[d](c,u,p,h);h+=d}}return h}function a(s,c,l,d,u,h){let b=s.width;let f=s.height;let w=s.index;for(let g=0;g<f;g++){for(let m=0;m<b;m++){let p=u.get(d);let y=l(m,g,w);i[d](c,p,y,h)}u.resetAfterLine()}}e.dataToBitMap=function(s,c){let l=c.width;let d=c.height;let u=c.depth;let h=c.bpp;let b=c.interlace;let f;if(u!==8){f=r(s,u)}let w;if(u<=8){w=Buffer.alloc(l*d*4)}else{w=new Uint16Array(l*d*4)}let g=Math.pow(2,u)-1;let m=0;let p;let y;if(b){p=n.getImagePasses(l,d);y=n.getInterlaceIterator(l,d)}else{let v=0;y=function(){let $=v;v+=4;return $};p=[{width:l,height:d}]}for(let v=0;v<p.length;v++){if(u===8){m=o(p[v],w,y,h,s,m)}else{a(p[v],w,y,h,f,g)}}if(u===8){if(m!==s.length){throw new Error("extra data found")}}else{f.end()}return w}}});var _o=ee({"../../node_modules/pngjs/lib/format-normaliser.js"(e,n){"use strict";function t(o,a,s,c,l){let d=0;for(let u=0;u<c;u++){for(let h=0;h<s;h++){let b=l[o[d]];if(!b){throw new Error("index "+o[d]+" not in palette")}for(let f=0;f<4;f++){a[d+f]=b[f]}d+=4}}}function i(o,a,s,c,l){let d=0;for(let u=0;u<c;u++){for(let h=0;h<s;h++){let b=false;if(l.length===1){if(l[0]===o[d]){b=true}}else if(l[0]===o[d]&&l[1]===o[d+1]&&l[2]===o[d+2]){b=true}if(b){for(let f=0;f<4;f++){a[d+f]=0}}d+=4}}}function r(o,a,s,c,l){let d=255;let u=Math.pow(2,l)-1;let h=0;for(let b=0;b<c;b++){for(let f=0;f<s;f++){for(let w=0;w<4;w++){a[h+w]=Math.floor(o[h+w]*d/u+.5)}h+=4}}}n.exports=function(o,a,s=false){let c=a.depth;let l=a.width;let d=a.height;let u=a.colorType;let h=a.transColor;let b=a.palette;let f=o;if(u===3){t(o,f,l,d,b)}else{if(h){i(o,f,l,d,h)}if(c!==8&&!s){if(c===16){f=Buffer.alloc(l*d*4)}r(o,f,l,d,c)}}return f}}});var Tl=ee({"../../node_modules/pngjs/lib/parser-async.js"(e,n){"use strict";var t=me("util");var i=me("zlib");var r=wo();var o=xl();var a=$o();var s=ko();var c=_o();var l=n.exports=function(d){r.call(this);this._parser=new a(d,{read:this.read.bind(this),error:this._handleError.bind(this),metadata:this._handleMetaData.bind(this),gamma:this.emit.bind(this,"gamma"),palette:this._handlePalette.bind(this),transColor:this._handleTransColor.bind(this),finished:this._finished.bind(this),inflateData:this._inflateData.bind(this),simpleTransparency:this._simpleTransparency.bind(this),headersFinished:this._headersFinished.bind(this)});this._options=d;this.writable=true;this._parser.start()};t.inherits(l,r);l.prototype._handleError=function(d){this.emit("error",d);this.writable=false;this.destroy();if(this._inflate&&this._inflate.destroy){this._inflate.destroy()}if(this._filter){this._filter.destroy();this._filter.on("error",function(){})}this.errord=true};l.prototype._inflateData=function(d){if(!this._inflate){if(this._bitmapInfo.interlace){this._inflate=i.createInflate();this._inflate.on("error",this.emit.bind(this,"error"));this._filter.on("complete",this._complete.bind(this));this._inflate.pipe(this._filter)}else{let u=(this._bitmapInfo.width*this._bitmapInfo.bpp*this._bitmapInfo.depth+7>>3)+1;let h=u*this._bitmapInfo.height;let b=Math.max(h,i.Z_MIN_CHUNK);this._inflate=i.createInflate({chunkSize:b});let f=h;let w=this.emit.bind(this,"error");this._inflate.on("error",function(m){if(!f){return}w(m)});this._filter.on("complete",this._complete.bind(this));let g=this._filter.write.bind(this._filter);this._inflate.on("data",function(m){if(!f){return}if(m.length>f){m=m.slice(0,f)}f-=m.length;g(m)});this._inflate.on("end",this._filter.end.bind(this._filter))}}this._inflate.write(d)};l.prototype._handleMetaData=function(d){this._metaData=d;this._bitmapInfo=Object.create(d);this._filter=new o(this._bitmapInfo)};l.prototype._handleTransColor=function(d){this._bitmapInfo.transColor=d};l.prototype._handlePalette=function(d){this._bitmapInfo.palette=d};l.prototype._simpleTransparency=function(){this._metaData.alpha=true};l.prototype._headersFinished=function(){this.emit("metadata",this._metaData)};l.prototype._finished=function(){if(this.errord){return}if(!this._inflate){this.emit("error","No Inflate block")}else{this._inflate.end()}};l.prototype._complete=function(d){if(this.errord){return}let u;try{let h=s.dataToBitMap(d,this._bitmapInfo);u=c(h,this._bitmapInfo,this._options.skipRescale);h=null}catch(h){this._handleError(h);return}this.emit("parsed",u)}}});var Dl=ee({"../../node_modules/pngjs/lib/bitpacker.js"(e,n){"use strict";var t=on();n.exports=function(i,r,o,a){let s=[t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(a.colorType)!==-1;if(a.colorType===a.inputColorType){let m=(function(){let p=new ArrayBuffer(2);new DataView(p).setInt16(0,256,true);return new Int16Array(p)[0]!==256})();if(a.bitDepth===8||a.bitDepth===16&&m){return i}}let c=a.bitDepth!==16?i:new Uint16Array(i.buffer);let l=255;let d=t.COLORTYPE_TO_BPP_MAP[a.inputColorType];if(d===4&&!a.inputHasAlpha){d=3}let u=t.COLORTYPE_TO_BPP_MAP[a.colorType];if(a.bitDepth===16){l=65535;u*=2}let h=Buffer.alloc(r*o*u);let b=0;let f=0;let w=a.bgColor||{};if(w.red===void 0){w.red=l}if(w.green===void 0){w.green=l}if(w.blue===void 0){w.blue=l}function g(){let m;let p;let y;let v=l;switch(a.inputColorType){case t.COLORTYPE_COLOR_ALPHA:v=c[b+3];m=c[b];p=c[b+1];y=c[b+2];break;case t.COLORTYPE_COLOR:m=c[b];p=c[b+1];y=c[b+2];break;case t.COLORTYPE_ALPHA:v=c[b+1];m=c[b];p=m;y=m;break;case t.COLORTYPE_GRAYSCALE:m=c[b];p=m;y=m;break;default:throw new Error("input color type:"+a.inputColorType+" is not supported at present")}if(a.inputHasAlpha){if(!s){v/=l;m=Math.min(Math.max(Math.round((1-v)*w.red+v*m),0),l);p=Math.min(Math.max(Math.round((1-v)*w.green+v*p),0),l);y=Math.min(Math.max(Math.round((1-v)*w.blue+v*y),0),l)}}return{red:m,green:p,blue:y,alpha:v}}for(let m=0;m<o;m++){for(let p=0;p<r;p++){let y=g(c,b);switch(a.colorType){case t.COLORTYPE_COLOR_ALPHA:case t.COLORTYPE_COLOR:if(a.bitDepth===8){h[f]=y.red;h[f+1]=y.green;h[f+2]=y.blue;if(s){h[f+3]=y.alpha}}else{h.writeUInt16BE(y.red,f);h.writeUInt16BE(y.green,f+2);h.writeUInt16BE(y.blue,f+4);if(s){h.writeUInt16BE(y.alpha,f+6)}}break;case t.COLORTYPE_ALPHA:case t.COLORTYPE_GRAYSCALE:{let v=(y.red+y.green+y.blue)/3;if(a.bitDepth===8){h[f]=v;if(s){h[f+1]=y.alpha}}else{h.writeUInt16BE(v,f);if(s){h.writeUInt16BE(y.alpha,f+2)}}break}default:throw new Error("unrecognised color Type "+a.colorType)}b+=d;f+=u}}return h}}});var Il=ee({"../../node_modules/pngjs/lib/filter-pack.js"(e,n){"use strict";var t=bo();function i(w,g,m,p,y){for(let v=0;v<m;v++){p[y+v]=w[g+v]}}function r(w,g,m){let p=0;let y=g+m;for(let v=g;v<y;v++){p+=Math.abs(w[v])}return p}function o(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=w[g+$]-T;p[y+$]=k}}function a(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=w[g+v]-$;y+=Math.abs(T)}return y}function s(w,g,m,p,y){for(let v=0;v<m;v++){let $=g>0?w[g+v-m]:0;let T=w[g+v]-$;p[y+v]=T}}function c(w,g,m){let p=0;let y=g+m;for(let v=g;v<y;v++){let $=g>0?w[v-m]:0;let T=w[v]-$;p+=Math.abs(T)}return p}function l(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=g>0?w[g+$-m]:0;let D=w[g+$]-(T+k>>1);p[y+$]=D}}function d(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=g>0?w[g+v-m]:0;let k=w[g+v]-($+T>>1);y+=Math.abs(k)}return y}function u(w,g,m,p,y,v){for(let $=0;$<m;$++){let T=$>=v?w[g+$-v]:0;let k=g>0?w[g+$-m]:0;let D=g>0&&$>=v?w[g+$-(m+v)]:0;let C=w[g+$]-t(T,k,D);p[y+$]=C}}function h(w,g,m,p){let y=0;for(let v=0;v<m;v++){let $=v>=p?w[g+v-p]:0;let T=g>0?w[g+v-m]:0;let k=g>0&&v>=p?w[g+v-(m+p)]:0;let D=w[g+v]-t($,T,k);y+=Math.abs(D)}return y}var b={0:i,1:o,2:s,3:l,4:u};var f={0:r,1:a,2:c,3:d,4:h};n.exports=function(w,g,m,p,y){let v;if(!("filterType"in p)||p.filterType===-1){v=[0,1,2,3,4]}else if(typeof p.filterType==="number"){v=[p.filterType]}else{throw new Error("unrecognised filter types")}if(p.bitDepth===16){y*=2}let $=g*y;let T=0;let k=0;let D=Buffer.alloc(($+1)*m);let C=v[0];for(let I=0;I<m;I++){if(v.length>1){let R=Infinity;for(let _=0;_<v.length;_++){let S=f[v[_]](w,k,$,y);if(S<R){C=v[_];R=S}}}D[T]=C;T++;b[C](w,k,$,D,T,y);T+=$;k+=$}return D}}});var So=ee({"../../node_modules/pngjs/lib/packer.js"(e,n){"use strict";var t=on();var i=yo();var r=Dl();var o=Il();var a=me("zlib");var s=n.exports=function(c){this._options=c;c.deflateChunkSize=c.deflateChunkSize||32*1024;c.deflateLevel=c.deflateLevel!=null?c.deflateLevel:9;c.deflateStrategy=c.deflateStrategy!=null?c.deflateStrategy:3;c.inputHasAlpha=c.inputHasAlpha!=null?c.inputHasAlpha:true;c.deflateFactory=c.deflateFactory||a.createDeflate;c.bitDepth=c.bitDepth||8;c.colorType=typeof c.colorType==="number"?c.colorType:t.COLORTYPE_COLOR_ALPHA;c.inputColorType=typeof c.inputColorType==="number"?c.inputColorType:t.COLORTYPE_COLOR_ALPHA;if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(c.colorType)===-1){throw new Error("option color type:"+c.colorType+" is not supported at present")}if([t.COLORTYPE_GRAYSCALE,t.COLORTYPE_COLOR,t.COLORTYPE_COLOR_ALPHA,t.COLORTYPE_ALPHA].indexOf(c.inputColorType)===-1){throw new Error("option input color type:"+c.inputColorType+" is not supported at present")}if(c.bitDepth!==8&&c.bitDepth!==16){throw new Error("option bit depth:"+c.bitDepth+" is not supported at present")}};s.prototype.getDeflateOptions=function(){return{chunkSize:this._options.deflateChunkSize,level:this._options.deflateLevel,strategy:this._options.deflateStrategy}};s.prototype.createDeflate=function(){return this._options.deflateFactory(this.getDeflateOptions())};s.prototype.filterData=function(c,l,d){let u=r(c,l,d,this._options);let h=t.COLORTYPE_TO_BPP_MAP[this._options.colorType];let b=o(u,l,d,this._options,h);return b};s.prototype._packChunk=function(c,l){let d=l?l.length:0;let u=Buffer.alloc(d+12);u.writeUInt32BE(d,0);u.writeUInt32BE(c,4);if(l){l.copy(u,8)}u.writeInt32BE(i.crc32(u.slice(4,u.length-4)),u.length-4);return u};s.prototype.packGAMA=function(c){let l=Buffer.alloc(4);l.writeUInt32BE(Math.floor(c*t.GAMMA_DIVISION),0);return this._packChunk(t.TYPE_gAMA,l)};s.prototype.packIHDR=function(c,l){let d=Buffer.alloc(13);d.writeUInt32BE(c,0);d.writeUInt32BE(l,4);d[8]=this._options.bitDepth;d[9]=this._options.colorType;d[10]=0;d[11]=0;d[12]=0;return this._packChunk(t.TYPE_IHDR,d)};s.prototype.packIDAT=function(c){return this._packChunk(t.TYPE_IDAT,c)};s.prototype.packIEND=function(){return this._packChunk(t.TYPE_IEND,null)}}});var Ml=ee({"../../node_modules/pngjs/lib/packer-async.js"(e,n){"use strict";var t=me("util");var i=me("stream");var r=on();var o=So();var a=n.exports=function(s){i.call(this);let c=s||{};this._packer=new o(c);this._deflate=this._packer.createDeflate();this.readable=true};t.inherits(a,i);a.prototype.pack=function(s,c,l,d){this.emit("data",Buffer.from(r.PNG_SIGNATURE));this.emit("data",this._packer.packIHDR(c,l));if(d){this.emit("data",this._packer.packGAMA(d))}let u=this._packer.filterData(s,c,l);this._deflate.on("error",this.emit.bind(this,"error"));this._deflate.on("data",function(h){this.emit("data",this._packer.packIDAT(h))}.bind(this));this._deflate.on("end",function(){this.emit("data",this._packer.packIEND());this.emit("end")}.bind(this));this._deflate.end(u)}}});var Al=ee({"../../node_modules/pngjs/lib/sync-inflate.js"(e,n){"use strict";var t=me("assert").ok;var i=me("zlib");var r=me("util");var o=me("buffer").kMaxLength;function a(u){if(!(this instanceof a)){return new a(u)}if(u&&u.chunkSize<i.Z_MIN_CHUNK){u.chunkSize=i.Z_MIN_CHUNK}i.Inflate.call(this,u);this._offset=this._offset===void 0?this._outOffset:this._offset;this._buffer=this._buffer||this._outBuffer;if(u&&u.maxLength!=null){this._maxLength=u.maxLength}}function s(u){return new a(u)}function c(u,h){if(h){process.nextTick(h)}if(!u._handle){return}u._handle.close();u._handle=null}a.prototype._processChunk=function(u,h,b){if(typeof b==="function"){return i.Inflate._processChunk.call(this,u,h,b)}let f=this;let w=u&&u.length;let g=this._chunkSize-this._offset;let m=this._maxLength;let p=0;let y=[];let v=0;let $;this.on("error",function(C){$=C});function T(C,I){if(f._hadError){return}let R=g-I;t(R>=0,"have should not go down");if(R>0){let _=f._buffer.slice(f._offset,f._offset+R);f._offset+=R;if(_.length>m){_=_.slice(0,m)}y.push(_);v+=_.length;m-=_.length;if(m===0){return false}}if(I===0||f._offset>=f._chunkSize){g=f._chunkSize;f._offset=0;f._buffer=Buffer.allocUnsafe(f._chunkSize)}if(I===0){p+=w-C;w=C;return true}return false}t(this._handle,"zlib binding closed");let k;do{k=this._handle.writeSync(h,u,p,w,this._buffer,this._offset,g);k=k||this._writeState}while(!this._hadError&&T(k[0],k[1]));if(this._hadError){throw $}if(v>=o){c(this);throw new RangeError("Cannot create final Buffer. It would be larger than 0x"+o.toString(16)+" bytes")}let D=Buffer.concat(y,v);c(this);return D};r.inherits(a,i.Inflate);function l(u,h){if(typeof h==="string"){h=Buffer.from(h)}if(!(h instanceof Buffer)){throw new TypeError("Not a string or buffer")}let b=u._finishFlushFlag;if(b==null){b=i.Z_FINISH}return u._processChunk(h,b)}function d(u,h){return l(new a(h),u)}n.exports=e=d;e.Inflate=a;e.createInflate=s;e.inflateSync=d}});var xo=ee({"../../node_modules/pngjs/lib/sync-reader.js"(e,n){"use strict";var t=n.exports=function(i){this._buffer=i;this._reads=[]};t.prototype.read=function(i,r){this._reads.push({length:Math.abs(i),allowLess:i<0,func:r})};t.prototype.process=function(){while(this._reads.length>0&&this._buffer.length){let i=this._reads[0];if(this._buffer.length&&(this._buffer.length>=i.length||i.allowLess)){this._reads.shift();let r=this._buffer;this._buffer=r.slice(i.length);i.func.call(this,r.slice(0,i.length))}else{break}}if(this._reads.length>0){throw new Error("There are some read requests waitng on finished stream")}if(this._buffer.length>0){throw new Error("unrecognised content at end of stream")}}}});var Cl=ee({"../../node_modules/pngjs/lib/filter-parse-sync.js"(e){"use strict";var n=xo();var t=vo();e.process=function(i,r){let o=[];let a=new n(i);let s=new t(r,{read:a.read.bind(a),write:function(c){o.push(c)},complete:function(){}});s.start();a.process();return Buffer.concat(o)}}});var El=ee({"../../node_modules/pngjs/lib/parser-sync.js"(e,n){"use strict";var t=true;var i=me("zlib");var r=Al();if(!i.deflateSync){t=false}var o=xo();var a=Cl();var s=$o();var c=ko();var l=_o();n.exports=function(d,u){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let h;function b(A){h=A}let f;function w(A){f=A}function g(A){f.transColor=A}function m(A){f.palette=A}function p(){f.alpha=true}let y;function v(A){y=A}let $=[];function T(A){$.push(A)}let k=new o(d);let D=new s(u,{read:k.read.bind(k),error:b,metadata:w,gamma:v,palette:m,transColor:g,inflateData:T,simpleTransparency:p});D.start();k.process();if(h){throw h}let C=Buffer.concat($);$.length=0;let I;if(f.interlace){I=i.inflateSync(C)}else{let A=(f.width*f.bpp*f.depth+7>>3)+1;let O=A*f.height;I=r(C,{chunkSize:O,maxLength:O})}C=null;if(!I||!I.length){throw new Error("bad png - invalid inflate data response")}let R=a.process(I,f);C=null;let _=c.dataToBitMap(R,f);R=null;let S=l(_,f,u.skipRescale);f.data=S;f.gamma=y||0;return f}}});var Ol=ee({"../../node_modules/pngjs/lib/packer-sync.js"(e,n){"use strict";var t=true;var i=me("zlib");if(!i.deflateSync){t=false}var r=on();var o=So();n.exports=function(a,s){if(!t){throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0")}let c=s||{};let l=new o(c);let d=[];d.push(Buffer.from(r.PNG_SIGNATURE));d.push(l.packIHDR(a.width,a.height));if(a.gamma){d.push(l.packGAMA(a.gamma))}let u=l.filterData(a.data,a.width,a.height);let h=i.deflateSync(u,l.getDeflateOptions());u=null;if(!h||!h.length){throw new Error("bad png - invalid compressed data response")}d.push(l.packIDAT(h));d.push(l.packIEND());return Buffer.concat(d)}}});var Rl=ee({"../../node_modules/pngjs/lib/png-sync.js"(e){"use strict";var n=El();var t=Ol();e.read=function(i,r){return n(i,r||{})};e.write=function(i,r){return t(i,r)}}});var zn=ee({"../../node_modules/pngjs/lib/png.js"(e){"use strict";var n=me("util");var t=me("stream");var i=Tl();var r=Ml();var o=Rl();var a=e.PNG=function(s){t.call(this);s=s||{};this.width=s.width|0;this.height=s.height|0;this.data=this.width>0&&this.height>0?Buffer.alloc(4*this.width*this.height):null;if(s.fill&&this.data){this.data.fill(0)}this.gamma=0;this.readable=this.writable=true;this._parser=new i(s);this._parser.on("error",this.emit.bind(this,"error"));this._parser.on("close",this._handleClose.bind(this));this._parser.on("metadata",this._metadata.bind(this));this._parser.on("gamma",this._gamma.bind(this));this._parser.on("parsed",function(c){this.data=c;this.emit("parsed",c)}.bind(this));this._packer=new r(s);this._packer.on("data",this.emit.bind(this,"data"));this._packer.on("end",this.emit.bind(this,"end"));this._parser.on("close",this._handleClose.bind(this));this._packer.on("error",this.emit.bind(this,"error"))};n.inherits(a,t);a.sync=o;a.prototype.pack=function(){if(!this.data||!this.data.length){this.emit("error","No data provided");return this}process.nextTick(function(){this._packer.pack(this.data,this.width,this.height,this.gamma)}.bind(this));return this};a.prototype.parse=function(s,c){if(c){let l,d;l=function(u){this.removeListener("error",d);this.data=u;c(null,this)}.bind(this);d=function(u){this.removeListener("parsed",l);c(u,null)}.bind(this);this.once("parsed",l);this.once("error",d)}this.end(s);return this};a.prototype.write=function(s){this._parser.write(s);return true};a.prototype.end=function(s){this._parser.end(s)};a.prototype._metadata=function(s){this.width=s.width;this.height=s.height;this.emit("metadata",s)};a.prototype._gamma=function(s){this.gamma=s};a.prototype._handleClose=function(){if(!this._parser.writable&&!this._packer.readable){this.emit("close")}};a.bitblt=function(s,c,l,d,u,h,b,f){l|=0;d|=0;u|=0;h|=0;b|=0;f|=0;if(l>s.width||d>s.height||l+u>s.width||d+h>s.height){throw new Error("bitblt reading outside image")}if(b>c.width||f>c.height||b+u>c.width||f+h>c.height){throw new Error("bitblt writing outside image")}for(let w=0;w<h;w++){s.data.copy(c.data,(f+w)*c.width+b<<2,(d+w)*s.width+l<<2,(d+w)*s.width+l+u<<2)}};a.prototype.bitblt=function(s,c,l,d,u,h,b){a.bitblt(this,s,c,l,d,u,h,b);return this};a.adjustGamma=function(s){if(s.gamma){for(let c=0;c<s.height;c++){for(let l=0;l<s.width;l++){let d=s.width*c+l<<2;for(let u=0;u<3;u++){let h=s.data[d+u]/255;h=Math.pow(h,1/2.2/s.gamma);s.data[d+u]=Math.round(h*255)}}}s.gamma=0}};a.prototype.adjustGamma=function(){a.adjustGamma(this)}}});import{DEFAULT_BROKER_PORT as Pl}from"@buoy-gg/sync-broker";var ji=Pl;var To=`http://localhost:${ji}`;var Do=15e3;var Li=2500;var Nl=1200;var Io=2e4;var Fl=3e4;var Mo=5e3;var Bl=15*6e4;var qi="Dashboard";var ke="All";import{readFileSync as Ao,writeFileSync as jl,mkdirSync as Ll,renameSync as ql,unlinkSync as Ul}from"fs";import{dirname as zl,join as Co,resolve as Wl}from"path";import{homedir as Hl}from"os";import{createHash as Jl}from"crypto";import{validateLicense as Gl,isAcceptedValidation as Kl,isTransientLicenseError as Vl,resolveTier as Yl,isRecognizedAccount as Xl,isBuoyToken as Zl,verifyFreeToken as Ql,resolveAccountAccess as ed,signCache as Wn,verifyCacheSignature as td,LICENSE_LAUNCH_VALIDATION_TTL as Eo,LICENSE_REVALIDATION_INTERVAL as mt,LICENSE_TRANSIENT_RETRY_DELAYS as Oo,KEYGEN_CONFIG as nd,createNodeSession as id,CLI_AUDIENCE as rd,exchangeServiceKey as od,isServiceKey as ad,normalizeLicenseKey as Ro,checkDevToken as sd,isDevToken as cd}from"@buoy-gg/license/headless";var Po=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","NEXT_PUBLIC_BUOY_KEY","REACT_APP_BUOY_KEY"];function Ui(e,n=process.env){for(const t of Po)if(n[t]!==void 0)return Ro(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let i;try{i=Ao(Co(e,t),"utf8")}catch{continue}const r=new Map;for(const o of i.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const s=a[2].trim();const c=s.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);r.set(a[1],c?c[2]:s.split("#")[0].trim())}for(const o of Po)if(r.has(o))return Ro(r.get(o))||null}return null}var ld=class{constructor(e,n={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.unsubscribeSession=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>this.hasKeyAccess()||this.session?.getStatus().state==="active";this.lastSessionSync=0;this.projectDir=Wl(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=Jl("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??Co(Hl(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:id({audience:rd,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return ed({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted"}start(){if(this.timer)return;if(this.session){this.unsubscribeSession=this.session.subscribe(()=>this.notify());void this.session.load().then(()=>this.notify(),()=>{})}try{const e=JSON.parse(Ao(this.cacheFile,"utf8"));if(e.valid&&td(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;this.unsubscribeSession?.();this.unsubscribeSession=null;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(){if(this.session&&Date.now()-this.lastSessionSync>15e3){this.lastSessionSync=Date.now();void this.session.sync().catch(()=>{})}const e=Ui(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.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<Eo&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+Eo;return}this.busy=true;const n=this.generation;try{if(cd(e)){const s=sd(e);if(!s.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),s.claims.iat*1e3);const l=s.claims.tier==="free"?"free":"pro";this.cache=Wn({licenseKey:e,licenseId:`dev:${s.claims.sid}`,valid:true,tier:l,isPro:l==="pro",expiresAt:new Date(s.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+mt;return}if(ad(e)){const s=await od(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||Ui(this.projectDir)!==e)return;if(!s.ok){if(s.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+mt}else{this.nextCheck=Date.now()+(Oo[this.retry++]??mt)}return}const c=Date.now();const l=s.claims.exp*1e3;this.cache=Wn({licenseKey:e,licenseId:`service:${s.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(l).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,l-60*60*1e3);return}if(Zl(e)){const s=Ql(e);if(!s){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=Wn({licenseKey:e,licenseId:`token:${s.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+mt;return}const t=await Gl(e);if(n!==this.generation||Ui(this.projectDir)!==e)return;const i=t.data?.relationships?.product?.data?.id;const r=t.meta.code==="EXPIRED"&&(Xl(t.data)||this.cache?.licenseKey===e);if(i&&i!==nd.productId||!r&&!Kl(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+mt;return}const o=r?"free":Yl(t.data);const a=Date.now();this.cache=Wn({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:r?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+mt}catch(t){if(n!==this.generation)return;const i=Vl(t)?Oo[this.retry++]:void 0;this.nextCheck=Date.now()+(i??mt)}finally{this.busy=false;this.notify()}}persistCache(){try{Ll(zl(this.cacheFile),{recursive:true,mode:448});jl(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});ql(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{Ul(this.cacheFile)}catch{}}};import dd from"http";import{randomUUID as ud}from"crypto";import{io as hd}from"socket.io-client";import{createBroker as pd,brokerTokenPath as zi,readBrokerToken as fd,writeBrokerToken as md,DASHBOARD_TOKEN_REQUIRED as wd}from"@buoy-gg/sync-broker";import{execFile as gd}from"child_process";import{promisify as bd}from"util";import at from"path";import vd from"os";var No=bd(gd);async function yd(e){let n;try{n=new URL(e)}catch{return null}if(!$d(n.hostname))return null;const t=n.port||(n.protocol==="https:"?"443":"80");try{const{stdout:i}=await No("lsof",["-nP",`-iTCP:${t}`,"-sTCP:LISTEN","-t"],{timeout:3e3});const r=i.split("\n").map(s=>s.trim()).find(Boolean);if(!r)return null;const{stdout:o}=await No("lsof",["-a","-p",r,"-d","cwd","-Fn"],{timeout:3e3});const a=o.split("\n").find(s=>s.startsWith("n"));return a?at.resolve(a.slice(1)):null}catch{return null}}function $d(e,n=vd.networkInterfaces()){const t=e.replace(/^\[|\]$/g,"").toLowerCase();if(t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="10.0.2.2"||t==="10.0.3.2")return true;return Object.values(n).flat().some(i=>i?.address?.toLowerCase()===t)}function Wi(e,n){const t=at.resolve(n);const i=e.map(a=>({deviceId:a.deviceId,score:a.root?kd(at.resolve(a.root),t):-1})).filter(a=>a.score>=0);if(i.length===0)return null;const r=Math.min(...i.map(a=>a.score));const o=i.filter(a=>a.score===r);return o.length===1?o[0].deviceId:null}function kd(e,n){if(e===n)return 0;const t=at.relative(e,n);const i=at.relative(n,e);const r=o=>o&&!o.startsWith("..")&&!at.isAbsolute(o);if(r(t))return t.split(at.sep).length;if(r(i))return i.split(at.sep).length;return-1}function _d(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function Fo(e){return e>=2}function Sd(e){if(_d(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!Fo(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 xd(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var Td=3e4;var Dd=5e3;function Id(e,n,t){if(e!=="highlight-updates"||n!=="waitFor")return 0;const i=Number(t?.timeoutMs);if(!Number.isFinite(i)||i<=0)return 0;return Math.min(i,Td)+Dd}var wt="Not ready:";var Md=class{constructor(e={}){this.socket=null;this.admitted=false;this.createdAt=Date.now();this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.sessionDevice=null;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.crashSeen=new Map;this.rootLookups=new Set;this.devServerRoots=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.transportLost=false;this.deviceConnections=new Map;this.port=e.port??ji;this.url=e.url??(e.port?`http://localhost:${e.port}`:To);this.actionTimeoutMs=e.actionTimeoutMs??Do;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
2
+ `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await Hi(this.url)||await Hi(this.url,8e3);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 ld(()=>this.broker?.refreshAccess());this.hostAccount.start();let t=false;this.broker=pd({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:i=>{if(i.code==="EADDRINUSE")t=true;this.log(`broker error: ${i.code??i.message}`)}});await Ad(this.url).catch(i=>{if(!t)throw i});if(!this.broker.httpServer.listening)t=true;if(t){this.log(`another broker holds :${this.port} \u2014 connecting to it as a dashboard`);await this.broker.close().catch(()=>void 0);this.broker=null;this.hostAccount.stop();this.hostAccount=null;await this.connectSocket();return"connected"}try{md(this.broker.dashboardToken,zi(this.port))}catch(i){this.log(`could not write the broker token file: ${i.message}`)}n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return wl(()=>new Promise((e,n)=>{const t=hd(this.url,{transports:["websocket"],query:{deviceName:qi},auth:s=>{const c=this.broker?.dashboardToken??fd(zi(this.port));s(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let i=false;const r=()=>{if(i)return;i=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(i)return;i=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",s=>{const c=!!s&&typeof s==="object"&&s.protocol===1&&s.ready===true;this.setAdmission(c);if(c){clearTimeout(o);this.rearmWatches();r()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const a=s=>s.message===wd;t.once("connect_error",s=>{if(i)return;i=true;clearTimeout(o);n(new Error(a(s)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${zi(this.port)} is missing or stale. Update Buoy Desktop, or restart it so it writes the file.`:`failed to connect to broker at ${this.url}: ${s.message}`))});t.on("connect_error",s=>{if(!a(s)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",s=>{if(!this.admitted||this.lockedDevices.has(s.persistentDeviceId))return;this.capabilities.set(s.persistentDeviceId,s);this.trackRootLookup(this.learnDevServerOrigin(s.persistentDeviceId));if(s.tools?.some(c=>c.toolId==="console"))this.ensureWatching("console",s.persistentDeviceId)});t.on("all-devices-update",s=>{if(!this.admitted)return;if(!Array.isArray(s))return;let c=false;for(const d of s){if(d.deviceId&&(d.tier==="locked"||d.tier==="checking"))this.lockDevice(String(d.deviceId),d.tier==="checking");else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){c=this.lockedDevices.delete(String(d.deviceId))||c}}this.devices=s;const l=Date.now();for(const d of s)if(d.deviceId)this.lastSeen.set(d.deviceId,l);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(s)});t.on("devtool-sync",s=>{if(!this.admitted||this.lockedDevices.has(s.persistentDeviceId))return;const c=He(s.persistentDeviceId,s.toolId);this.snapshots.set(c,s.payload);this.snapshotAt.set(c,Date.now());if(s.toolId==="console")this.noteCrashReport(s.persistentDeviceId,s.payload);const l=this.snapshotWaiters.get(c);if(l&&l.length){this.snapshotWaiters.set(c,[]);for(const d of l)d(s.payload)}});t.on("devtool-action-result",s=>{if(!this.admitted)return;const c=this.pendingActions.get(s.requestId);if(!c)return;this.pendingActions.delete(s.requestId);clearTimeout(c.timer);c.resolve(s)})}))}isAdmitted(){return this.admitted}requireAdmission(){if(this.transportLost)throw new Error("The broker link was lost. Wait for it to reconnect.");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.transportLost=!e&&n;this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.devServerRoots.clear();this.crashSeen.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(n?"The broker link was lost during the call. Its result is unknown.":"Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const i of t)i(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.devices.some(n=>n.deviceId===e&&n.tier==="checking"))throw new Error(`${wt} The new app link is still checking access.`);if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e,n=false){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.devServerRoots.delete(e);this.originProbed.delete(e);const t=`${e}::`;for(const i of this.snapshots.keys())if(i.startsWith(t)){this.snapshots.delete(i);this.snapshotAt.delete(i)}for(const[i,r]of this.snapshotWaiters)if(i.startsWith(t)){this.snapshotWaiters.delete(i);for(const o of r)o(void 0)}for(const[i,r]of this.pendingActions)if(r.target===e||r.target===ke){clearTimeout(r.timer);this.pendingActions.delete(i);r.reject(new Error(n?"The app link changed during the call. Its result is unknown.":"Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-Bl;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(r=>r.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.devServerRoots.delete(n);this.originProbed.delete(n);const i=`${n}::`;for(const r of[...this.snapshots.keys()]){if(r.startsWith(i)){this.snapshots.delete(r);this.snapshotAt.delete(r)}}for(const r of[...this.watching.keys()]){if(r.startsWith(i))this.watching.delete(r)}}}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!==qi&&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==="checking"?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(i=>i.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}peekProjectDevice(){if(!this.admitted)return null;if(this.sessionDevice)return this.sessionDevice;const e=this.connectedDevices().filter(t=>!this.lockedDevices.has(t.deviceId)&&t.tools.length>0);const n=process.env.BUOY_PROJECT_ROOT||process.cwd();return Wi(e.map(t=>({deviceId:t.deviceId,root:this.devServerRoots.get(t.deviceId)})),n)??null}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=(l,d)=>new Error(`${wt} "${l}" (${d}) is not connected right now. If it is restarting, it usually takes a few seconds; to use another device, pass deviceId (run list_devices to see them).`);if(this.sessionDevice){const l=this.devices.find(d=>d.deviceId===this.sessionDevice);if(l&&l.isConnected!==false)return this.sessionDevice;throw n(this.sessionDevice,"the device this session was using")}const t=this.connectedDevices();const i=t.filter(l=>!this.lockedDevices.has(l.deviceId));const r=i.filter(l=>l.tools.length>0);const o=process.env.BUOY_PROJECT_ROOT||process.cwd();const a=Wi(r.map(l=>({deviceId:l.deviceId,root:this.devServerRoots.get(l.deviceId)})),o);if(a)return this.sessionDevice=a;const s=Wi(this.devices.filter(l=>l.deviceId&&l.isConnected===false&&!this.lockedDevices.has(l.deviceId)).map(l=>({deviceId:l.deviceId,root:this.devServerRoots.get(l.deviceId)})),o);if(s)throw n(s,"this project's app");if(i.length===0){if(t.length>0){throw new Error(`Devices are connected (${t.map(l=>l.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(i.length===1)return this.sessionDevice=i[0].deviceId;if(r.length===1)return this.sessionDevice=r[0].deviceId;if(r.length===0){const l=i.map(d=>d.deviceName).join(", ");throw new Error(`Connected devices (${l}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const c=r.map(l=>`${l.deviceName} (${l.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${c}`)}requireProDevice(e){this.requireAdmission();const n=ft();if(e&&e!==ke)this.sessionDevice=e;const t=a=>{if(!a)return"";return a.tier==="locked"?` The broker has not confirmed account access for "${a.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${a.deviceName}" is on the ${a.tier} tier.`};const i=a=>n.level==="pro"?rn(n,t(a)):`The Buoy MCP needs an app on a Buoy account.${t(a)} Run \`npx buoy login\` in the project (free), restart the dev server with its cache cleared, then run \`list_devices\` to check.`;if(e===ke){const a=this.connectedDevices().filter(c=>c.isPro||c.tier==="free");const s=a.find(c=>!Ln(c,n.level));if(a.length===0)throw new Error(i());if(s)throw new Error(i(s));return ke}const r=this.resolveDevice(e);const o=this.connectedDevices().find(a=>a.deviceId===r);if(!Ln(o,n.level)){const a=this.accessState(r,n.level);if(a==="offline")throw new Error(`${wt} "${r}" is not connected right now. If the app is starting or reloading, it usually takes a few seconds. Run list_devices to check.`);if(a==="pending")throw new Error(`${wt} account access for "${r}" is still being confirmed (the app just connected). Try again in a moment.`);throw new Error(i(o))}return r}checkAppCall(e,n,t){const i=ft();if(!i.tool)return;const r=t===ke?this.connectedDevices().find(o=>o.tier==="free"&&!fe(o)):this.listDevices().find(o=>o.deviceId===t&&!fe(o));if(!r)return;if(i.level==="free"&&ml(e,n))return;throw new Error(rn({...i,level:"pro"},` The app "${r.deviceName}" is on the ${r.tier} tier${i.level==="free"?`, and this part of \`${i.tool}\` reads its ${e} data, which is Pro`:""}.`))}refuseBuoyPanelTap(e,n,t,i){if(e!=="highlight-updates"||n!=="tapElement")return null;const r=ft();if(!r.tool||r.level!=="free")return null;const o=t??{};const a=typeof o.nativeTag==="number"?o.nativeTag:null;const s=typeof o.testID==="string"&&o.testID?o.testID.toLowerCase():null;const c=a!=null||s!=null||!!o.includeBuoy;if(i===ke){if(!c||!this.connectedDevices().some(u=>u.tier==="free"&&!fe(u)))return null;return Promise.reject(new Error(rn({...r,level:"pro"},' On a free app, tap one app at a time (not "All") when tapping by testID or nativeTag.')))}const l=this.listDevices().find(u=>u.deviceId===i);if(!l||fe(l)||!c)return null;const d=u=>new Error(rn({...r,level:"pro"},u));if(o.includeBuoy)return Promise.reject(d(" That reaches Buoy's own panel, not the app."));return(async()=>{let u;try{u=await this.callAction("highlight-updates","describeScreen",{includeBuoy:false,includeHidden:true},i,{timeoutMs:4e3,silent:true})}catch(b){throw new Error(`Could not check that target before tapping (${b instanceof Error?b.message:String(b)}). On a free app, tap by query, or retry once the screen settles.`)}const h=(u?.elements??[]).some(b=>a!=null?b.nativeTag===a:b.testID?.toLowerCase()===s);if(!h&&a==null&&typeof o.query==="string"){const{testID:b,...f}=o;return f}if(!h){throw new Error(`"${a??String(o.testID)}" is not on the app's screen right now. On a free app, a tap by testID or nativeTag must hit an element describe_screen lists, and Buoy's own panels are Pro. Swipe it into view first, or tap by query.`)}})()}fullResultsFor(e){if(e===ke){const n=this.connectedDevices();return n.length>0&&n.every(t=>fe(t))}return fe(this.listDevices().find(n=>n.deviceId===e))}deviceWasAllowed(e){return Ln(this.listDevices().find(n=>n.deviceId===e),ft().level)}accessState(e,n=ft().level){const t=this.devices.find(i=>i.deviceId===e);if(!t||t.isConnected===false)return"offline";if(fe(t)||n==="free"&&t.tier==="free")return"ready";return t.tier===void 0||t.tier==="checking"?"pending":"denied"}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 Fo(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const i=this.healthOf(e);const{timeoutMs:r,usedFullBudget:o}=Sd({consecutiveTimeouts:i.consecutiveTimeouts,lastFullBudgetAt:i.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:Li,recoveryProbeMs:Io,toolId:n,action:t});if(o)i.lastFullBudgetAt=Date.now();return r}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const i=xd({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:Fl});const r=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,i?"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)r.push(`Last captured error: ${o}`);r.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 r.join(" ")}noteCrashReport(e,n){const t=n?.entries;if(!Array.isArray(t)||t.length===0)return;const i=Math.max(...t.map(a=>typeof a?.timestamp==="number"?a.timestamp:-Infinity));const r=this.crashSeen.get(e)??new Set;let o=false;for(const a of t){const s=a?.fatal||/^\[(RENDER ERROR|FATAL)/.test(a?.message??"");if(!s||typeof a.timestamp!=="number")continue;const c=`${a.id??""}|${a.timestamp}|${(a.message??"").slice(0,120)}`;if(r.has(c))continue;r.add(c);if(i-a.timestamp<3e4)o=true}if(r.size>200)for(const a of[...r].slice(0,r.size-200))r.delete(a);this.crashSeen.set(e,r);if(!o)return;if(![...this.pendingActions.values()].some(a=>a.target===e&&a.expire&&!a.cut))return;void this.callAction("app","ping",{},e,{timeoutMs:1500,silent:true}).then(()=>void 0,()=>{const a=Date.now();for(const s of this.pendingActions.values()){if(s.target!==e||!s.expire||s.cut)continue;s.cut=true;if(s.deadline!==void 0&&s.deadline-a<=Li)continue;clearTimeout(s.timer);s.timer=setTimeout(s.expire,Li)}})}lastFatalFor(e){const n=this.snapshots.get(He(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let i=t.length-1;i>=0;i--){const r=t[i];if(r?.fatal||r?.level==="error"){const o=(r.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(He(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){const t=this.devServerOrigins.get(e)!==n;this.devServerOrigins.set(e,n);if(t)this.devServerRoots.delete(e);if(t||!this.devServerRoots.has(e)){const i=yd(n).then(r=>{if(r&&this.devServerOrigins.get(e)===n)this.devServerRoots.set(e,r)});this.trackRootLookup(i)}}}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{}}trackRootLookup(e){const n=e.catch(()=>void 0).finally(()=>this.rootLookups.delete(n));this.rootLookups.add(n)}async awaitBroker(){if(Date.now()-this.createdAt>15e3)return;for(let e=0;!this.admitted&&e<4e3;e+=100)await new Promise(n=>setTimeout(n,100));for(let e=0;this.admitted&&this.devices.length===0&&e<1500;e+=100)await new Promise(n=>setTimeout(n,100))}async settleDeviceChoice(e=1500){await this.awaitBroker();if(this.sessionDevice||this.rootLookups.size===0||this.connectedDevices().length<2)return;await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,e))]);if(this.rootLookups.size)await Promise.race([Promise.all([...this.rootLookups]),new Promise(n=>setTimeout(n,Math.min(800,e)))])}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(He(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=He(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)}}rearmReconnectedDevices(e){for(const n of e){if(!n.deviceId||n.isConnected===false)continue;const t=String(n.deviceId);const i=`${n.id}|${n.connectionNonce??""}`;const r=this.deviceConnections.get(t);this.deviceConnections.set(t,i);if(r===void 0||r===i)continue;for(const{toolId:o,targetDeviceId:a}of this.watching.values()){if(a===t)this.sendWatch(o,a)}}}unwatch(e,n){const t=He(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);this.checkAppCall(e,"@snapshot",n);return this.snapshots.get(He(n,e))}async getSnapshot(e,n,t={}){const i=this.resolveDevice(n);this.checkAppCall(e,"@snapshot",i);const r=He(i,e);this.ensureWatching(e,i);let o=t.timeoutMs??Mo;if(t.timeoutMs===void 0&&this.isUnresponsive(i)){const l=this.healthOf(i);if(Date.now()-l.lastFullBudgetAt>Io){l.lastFullBudgetAt=Date.now()}else{o=Nl}}const a=this.waitForSnapshot(r,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:i});const s=await a;this.requireAdmission();this.requireDeviceAccount(i);if(s!==void 0){if(!t.silent)this.noteOk(i);return{deviceId:i,payload:s}}if(!t.silent)this.noteTimeout(i);const c=this.snapshots.get(r);if(c!==void 0){return{deviceId:i,payload:c,stale:this.staleNote(e,i)}}throw new Error(`Timed out waiting for "${e}" state from ${i} (is the tool active?). ${this.unresponsiveNote(i)}`)}lastSync(e,n){this.checkAppCall(n,"@snapshot",e);const t=this.snapshots.get(He(e,n));return t===void 0?void 0:{payload:t,ageMs:this.snapshotAgeMs(n,e)}}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const i=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${i}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const i=this.snapshotWaiters.get(e)??[];const r=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(s=>s!==o);this.snapshotWaiters.set(e,a);t(void 0)},n);const o=a=>{clearTimeout(r);t(a)};i.push(o);this.snapshotWaiters.set(e,i)})}async callAction(e,n,t,i,r={}){this.requireAdmission();const o=i===ke?ke:this.resolveDevice(i);this.checkAppCall(e,n,o);const a=this.refuseBuoyPanelTap(e,n,t,o);if(a)t=await a??t;if(!this.socket)throw new Error("SyncClient is not connected");const s=ud();const c=r.timeoutMs??Math.max(this.timeoutFor(o,e,n),Id(e,n,t));const l=await new Promise((d,u)=>{const h=Date.now();const b=()=>{this.pendingActions.delete(s);if(o!==ke&&!r.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${n}" got no answer in ${Date.now()-h}ms. `+(o===ke?"No device responded.":this.unresponsiveNote(o))))};const f=setTimeout(b,c);this.pendingActions.set(s,{resolve:d,reject:u,timer:f,target:o,expire:b,deadline:h+c});this.socket.emit("devtool-action",{requestId:s,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==ke&&!r.silent)this.noteOk(o);if(!l.ok){throw new Error(l.error||`Action "${e}.${n}" failed on device`)}return l.result}};var He=(e,n)=>`${e}::${n}`;function Hi(e,n=800){return new Promise(t=>{const i=dd.get(e,r=>{r.resume();t((r.statusCode??0)>0)});i.setTimeout(n,()=>{i.destroy();t(false)});i.on("error",()=>t(false))})}async function Ad(e,n=20){for(let t=0;t<n;t++){if(await Hi(e,300))return;await Cd(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var Cd=e=>new Promise(n=>setTimeout(n,e));import{promises as Bo}from"fs";import Ed from"os";import Od from"path";var Rd=24*60*60*1e3;var Pd=1500;var Nd=`https://registry.npmjs.org/${so}/latest`;var jo=Od.join(Ed.tmpdir(),"buoy-mcp-update-check.json");function Fd(e,n){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const i=t(e);const r=t(n);for(let o=0;o<3;o++){const a=i[o]??0;const s=r[o]??0;if(a>s)return true;if(a<s)return false}return false}async function Bd(){try{const e=await Bo.readFile(jo,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function jd(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),Pd);try{const i=await fetch(Nd,{signal:n.signal});if(!i.ok)return null;const r=await i.json();const o=typeof r.version==="string"?r.version:null;if(o){await Bo.writeFile(jo,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function Ld(e){const n=await Bd();if(n&&e-n.checkedAt<Rd)return n.latest;return jd(e)}async function Lo(e=Date.now()){if(Nn==="0.0.0-dev")return null;try{const n=await Ld(e);if(n&&Fd(n,Nn)){return`A newer buoy-mcp is available (${n}; you're on ${Nn}). Update by re-running: npx -y ${so}@latest init \u2014 then restart your editor.`}}catch{}return null}import{z as te}from"zod";import{execFile as qd}from"child_process";import{promisify as Ud}from"util";import{promises as je}from"fs";import qo from"os";import Ae from"path";import{randomUUID as Uo}from"crypto";var an=Ud(qd);var Hn=(e,n,t)=>Math.min(Math.max(e,n),t);function zo(e){const n=e;if(n?.code==="ENOENT"){return"Xcode command-line tools (xcrun) not found. Install Xcode to capture the iOS Simulator."}const t=String(n?.stderr||n?.message||e);if(/No devices are booted|Unable to find device|current device/i.test(t)){return"No booted iOS Simulator found. Boot the Simulator with your app running, then try again."}return t.trim()||"Failed to capture the iOS Simulator."}function zd(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,i)=>e[i]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}var Wd="@react_buoy_sync_device_id";async function Jn(e){try{return await je.readdir(e)}catch{return[]}}async function Hd(e){const n=[];for(const t of await Jn(e))n.push(...await Wo(Ae.join(e,t)));return n}async function Wo(e){const n=[];const t=Ae.join(e,"Library","WebKit");for(const i of await Jn(t)){const r=Ae.join(t,i,"WebsiteData","Default");for(const o of await Jn(r)){for(const a of await Jn(Ae.join(r,o))){const s=Ae.join(r,o,a,"LocalStorage","localstorage.sqlite3");try{await je.access(s);const{stdout:c}=await an("sqlite3",["-readonly",s,`SELECT hex(value) FROM ItemTable WHERE key='${Wd}' LIMIT 1;`],{timeout:1500});const l=c.trim();if(l)n.push(Buffer.from(l,"hex").toString("utf16le").trim())}catch{}}}}return n}var Ho=new Map;var Jd=3e4;async function Jo(e){if(process.platform!=="darwin")return{ok:false,error:"Simulator lookup needs macOS."};let n=[];try{const{stdout:i}=await an("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const r=JSON.parse(i).devices;n=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch(i){return{ok:false,error:zo(i)}}if(n.length===0)return{ok:false,error:"No iOS Simulator is booted."};const t=e?.split("-").pop();if(t){for(const i of n){if(!i.dataPath)continue;const r=Ae.join(i.dataPath,"Containers","Data","Application");let o=[];try{o=await je.readdir(r)}catch{continue}for(const a of o){try{const s=(await je.readFile(Ae.join(r,a,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(s===t)return{ok:true,udid:i.udid,how:`matched ${e} to "${i.name}"`}}catch{}}}}if(t&&e){const i=n.map(c=>c.udid).sort().join(",");const r=Ho.get(e);const o=r?.booted===i&&(r.udid!==null||Date.now()-r.at<Jd);const a=o&&r?.udid?n.find(c=>c.udid===r.udid):void 0;if(a)return{ok:true,udid:a.udid,how:`matched ${e} to "${a.name}" (WebView storage)`};const s=[];if(!o){const c=await Promise.all(n.map(l=>l.dataPath?Hd(Ae.join(l.dataPath,"Containers","Data","Application")):Promise.resolve([])));c.forEach((l,d)=>{if(l.includes(t))s.push(n[d])});Ho.set(e,{booted:i,udid:s.length===1?s[0].udid:null,at:Date.now()})}if(s.length===1){return{ok:true,udid:s[0].udid,how:`matched ${e} to "${s[0].name}" (WebView storage)`}}if(s.length>1){return{ok:false,error:`${s.length} booted simulators (${s.map(c=>c.name).join(", ")}) hold ${e}. Pass the udid, or shut one down.`}}}if(n.length===1)return{ok:true,udid:n[0].udid,how:`only booted simulator "${n[0].name}"`};return{ok:false,error:`${n.length} iOS Simulators are booted (${n.map(i=>i.name).join(", ")}) and none could be matched to ${e??"this device"}. Shut down the others, or capture from a single booted simulator.`}}async function Gd(e){const n=new Map;if(process.platform!=="darwin"||e.length===0)return n;let t=[];try{const{stdout:r}=await an("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const o=JSON.parse(r).devices;t=Object.entries(o).filter(([a])=>/iOS/.test(a)).flatMap(([,a])=>a.filter(s=>s.state==="Booted"))}catch{return n}const i=new Map;for(const r of t){if(!r.dataPath)continue;const o=Ae.join(r.dataPath,"Containers","Data","Application");let a=[];try{a=await je.readdir(o)}catch{continue}for(const s of a){try{const c=(await je.readFile(Ae.join(o,s,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim();if(c)i.set(c,{name:r.name,udid:r.udid})}catch{}}}for(const r of e){const o=i.get(r.split("-").pop()??"");if(o)n.set(r,o)}return n}async function Go(e="booted"){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const n=Ae.join(qo.tmpdir(),`buoy-mcp-sim-${Uo()}.png`);try{await an("xcrun",["simctl","io",e,"screenshot","--type=png",n],{timeout:15e3})}catch(t){return{ok:false,error:zo(t)}}try{const t=await je.readFile(n);const i=zd(t);if(!i)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:n,width:i.width,height:i.height}}catch(t){return{ok:false,error:`Failed to read screenshot: ${t.message}`}}}async function Kd(e,n,t){const i=Hn(Math.round(t.x),0,Math.max(0,n.width-1));const r=Hn(Math.round(t.y),0,Math.max(0,n.height-1));const o=Hn(Math.round(t.width),1,n.width-i);const a=Hn(Math.round(t.height),1,n.height-r);const s=Ae.join(qo.tmpdir(),`buoy-mcp-crop-${Uo()}.png`);try{await an("sips",["-c",String(a),String(o),"--cropOffset",String(r),String(i),e,"--out",s],{timeout:1e4});const c=await je.readFile(s);return{ok:true,buffer:c,width:o,height:a}}catch(c){const l=c;return{ok:false,error:`Crop failed: ${l?.stderr||l?.message||c}`}}finally{await je.unlink(s).catch(()=>{});await je.unlink(e).catch(()=>{})}}import{execFile as Vd}from"child_process";import{promisify as Yd}from"util";var Ji=Un(zn());import{execFile as Xd}from"child_process";import{promisify as Zd}from"util";import{promises as ie}from"fs";import sn from"os";import Y from"path";import{randomUUID as Qd}from"crypto";var eu=Un(zn());import{execFile as tu}from"child_process";import{promisify as nu}from"util";import{promises as gt}from"fs";import Gi from"os";import Ki from"path";import{randomUUID as Vi}from"crypto";var iu=nu(tu);async function xe(e,n=3e4){const t=Ki.join(Gi.tmpdir(),`buoy-devicectl-${Vi()}.json`);try{await iu("xcrun",["devicectl",...e,"--quiet","--json-output",t],{timeout:n,maxBuffer:16*1024*1024});return JSON.parse(await gt.readFile(t,"utf8"))}catch(i){const r=i;let o=(r.stderr||r.stdout||r.message||"").trim();try{const a=JSON.parse(await gt.readFile(t,"utf8"));o=a.error?.userInfo?.NSLocalizedDescription?.string??o}catch{}if(/locked/i.test(o))throw new Error("The iPhone is locked. Unlock it and try again.");throw new Error(`devicectl ${e.slice(0,2).join(" ")} failed: ${o.split("\n").slice(-3).join(" ").slice(0,300)}`)}finally{await gt.unlink(t).catch(()=>void 0)}}async function ru(){if(process.platform!=="darwin")return[];return ou(await xe(["list","devices"],15e3).catch(()=>null))}function ou(e){const n=e;return(n?.result?.devices??[]).filter(t=>t.hardwareProperties?.reality==="physical"&&t.hardwareProperties?.platform==="iOS"&&t.connectionProperties?.pairingState==="paired"&&t.connectionProperties?.tunnelState!=="unavailable").map(t=>({udid:t.hardwareProperties.udid,name:t.deviceProperties?.name??"iPhone"})).filter(t=>!!t.udid)}async function Ko(e){const n=await xe(["device","info","apps","--device",e]);return(n.result?.apps??[]).filter(t=>t.bundleIdentifier&&!t.bundleIdentifier.startsWith("com.apple.")&&t.builtByDeveloper!==false).map(t=>({bundleId:t.bundleIdentifier,name:t.name??t.bundleIdentifier,path:t.url?.replace(/^file:\/\//,"")}))}async function Vo(e,n,t){const i=await xe(["device","info","files","--device",e,"--domain-type","appDataContainer","--domain-identifier",n],2e4);const r=new Set((i.result?.files??[]).map(a=>a.relativePath??a.name??""));if(!r.has(t))return null;const o=Ki.join(Gi.tmpdir(),`buoy-devfile-${Vi()}`);try{await xe(["device","copy","from","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",o],2e4);return await gt.readFile(o)}finally{await gt.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}async function Yo(e,n){const t=await Vo(e,n,"Documents/react-buoy/@react_buoy_sync_device_id.json").catch(()=>null);const i=t?.toString("utf8").trim().replace(/^"|"$/g,"")??"";return/^[\w-]{4,64}$/.test(i)?i:null}async function Xo(e){const n=Ki.join(Gi.tmpdir(),`buoy-phone-shot-${Vi()}.png`);try{await xe(["device","capture","screenshot","--device",e,"--destination",n],3e4);return await gt.readFile(n)}finally{await gt.unlink(n).catch(()=>void 0)}}var au="The picture is all black. If the app isn't really showing black, the iPhone's own screen is off: it is asleep, or the Mac shows it in iPhone Mirroring (the phone stays locked then). Quit iPhone Mirroring, unlock the phone and keep its screen on, then try again.";function su(e){try{const n=eu.PNG.sync.read(e);const t=Math.max(1,Math.floor(n.width*n.height/4e3));for(let i=0;i<n.width*n.height;i+=t){const r=i*4;if(n.data[r]>12||n.data[r+1]>12||n.data[r+2]>12)return false}return true}catch{return false}}async function Yi(e,n,t={}){await xe(["device","process","launch","--device",e,...t.terminateExisting?["--terminate-existing"]:[],...t.url?["--payload-url",t.url]:[],n])}async function cu(e,n){await xe(["device","process","openURL","--device",e,n])}async function Zo(e,n){const t=await Ko(e);const i=t.find(a=>a.bundleId===n)?.path;const r=await xe(["device","info","processes","--device",e]);const o=(r.result?.runningProcesses??[]).find(a=>i&&decodeURIComponent(a.executable??"").replace(/^file:\/\//,"").startsWith(i.replace(/\/$/,"")));if(!o?.processIdentifier)return false;await xe(["device","process","terminate","--device",e,"--pid",String(o.processIdentifier)]);return true}async function lu(e,n){await xe(["device","uninstall","app","--device",e,n],12e4)}async function du(e,n){await xe(["device","install","app","--device",e,n],3e5)}async function uu(e,n,t,i){await xe(["device","copy","to","--device",e,"--domain-type","appDataContainer","--domain-identifier",n,"--source",t,"--destination",i],3e4)}function hu(e,n,t){return["devicectl","device","capture","screen-record","--device",e,"--destination",n,"--duration",String(t)]}var Qo=new Map;async function Gn(e){const n=Qo.get(e);if(n)return n;const t=await xe(["list","devices"]);const i=t.result?.devices?.find(r=>r.hardwareProperties?.udid===e)?.identifier;if(!i)throw new Error("CoreDevice doesn't list this iPhone right now. Check that it is connected and unlocked.");Qo.set(e,i);return i}import{execFile as pu}from"child_process";import{promisify as fu}from"util";import{promises as Xi}from"fs";import Rt from"path";import{WebSocket as mu}from"ws";import{execFile as wu}from"child_process";import{promisify as gu}from"util";var Zi=/\{\{\s*secret:([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;var Pt=6;var Qi=new Map;var ea=()=>false;function bu(e){ea=e}function vu(e){Zi.lastIndex=0;return Zi.test(e)}function Kn(e,n=process.env){if(!vu(e))return e;if(ea()){throw new Error("A screen recording is running, and a video can't hide a typed secret. Stop the recording first (screen_recording stop), then type it.")}const t=[];const i=[];const r=e.replace(Zi,(o,a)=>{const s=n[`BUOY_SECRET_${a}`]??n[a];if(s===void 0||s===""){t.push(a);return""}if(s.length<Pt){i.push(a);return""}Qi.set(a,s);return s});if(t.length){throw new Error(`No value for ${t.map(o=>`{{secret:${o}}}`).join(", ")}. Set ${t.map(o=>`BUOY_SECRET_${o}`).join(", ")} in the environment the MCP server runs in (for example in your editor's MCP config "env").`)}if(i.length){throw new Error(`${i.map(o=>`{{secret:${o}}}`).join(", ")} is shorter than ${Pt} characters, so Buoy can't hide it reliably in what it sends back. Use a longer value, or type it as plain text if it isn't sensitive.`)}return r}function ta(e=process.env){const n=new Map(Qi);for(const[t,i]of Object.entries(e))if(t.startsWith("BUOY_SECRET_")&&i)n.set(t.slice("BUOY_SECRET_".length),i);return n}var yu=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function Te(e){const n=new Map;for(const[r,o]of ta()){if(o.length<Pt)continue;for(const a of[o,JSON.stringify(o).slice(1,-1),encodeURIComponent(o)])n.set(a,r);for(let a=o.length-1;a>=Pt;a--){n.set(`${o.slice(0,a)}\u2026`,r);n.set(`${o.slice(0,a)}...`,r);if(a>=8)n.set(o.slice(0,a),r)}}if(n.size===0)return e;const t=[...n.keys()].sort((r,o)=>o.length-r.length);const i=new RegExp(t.map(yu).join("|"),"g");return e.replace(i,(r,o,a)=>{if(/^\d+$/.test(r)){const s=a.slice(Math.max(0,o-8),o);const c=a.slice(o+r.length,o+r.length+8);if(/(\d|\d\.)$/.test(s)||/^(\d|\.\d|[eE][+-]?\d)/.test(c))return r;if(/[:[,]\s*$/.test(s)&&/^\s*([,\]}]|$)/.test(c)&&!$u(a,o))return`"{{secret:${n.get(r)}}}"`}return`{{secret:${n.get(r)}}}`})}function $u(e,n){let t=false;for(let i=0;i<n;i++){if(e[i]==="\\")i++;else if(e[i]==='"')t=!t}return t}function er(){return[...Qi.values()]}function ku(){return[...ta().values()].filter(e=>e.length>=Pt)}var na=gu(wu);var tr="Android control needs adb on this computer (Android SDK platform-tools). Install it, or add it to PATH.";function de(e){return`'${e.replace(/'/g,`'\\''`)}'`}function ia(e){if(!/^[A-Za-z][\w]*(\.[A-Za-z_][\w]*)+$/.test(e))throw new Error(`"${e}" is not a valid Android package name.`);return e}function cn(e,n){const t=e?co(e):null;if(t)return{bin:t.adb,args:["-P",String(t.port),"-s",e,...n]};return{bin:"adb",args:e?["-s",e,...n]:n}}async function Le(e,n,t=15e3){try{const i=cn(e,n);const{stdout:r}=await na(i.bin,i.args,{timeout:t,maxBuffer:32*1024*1024});return r}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(tr);const o=n[0]==="shell"?`shell ${(n[1]??"").split(" ")[0]}`:n.slice(0,2).join(" ");const a=n[0]==="shell"?r.stderr||(r.killed?"timed out":"no error text"):r.stderr||r.message||"";const s=Te(a.replace(/'[^']*'/g,"'\u2026'")).trim().slice(0,300);throw new Error(`adb ${o} failed: ${s}`)}}function J(e,n,t=15e3){return Le(e,["shell",n],t)}async function ra(e,n,t=15e3){try{const i=cn(e,n);const{stdout:r}=await na(i.bin,i.args,{timeout:t,encoding:"buffer",maxBuffer:64*1024*1024});return r}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(tr);throw new Error(`adb ${n.slice(0,2).join(" ")} failed: ${r.message.slice(0,300)}`)}}async function oa(){const e=[...nl().keys()];try{const n=_u(await Le(null,["devices"]));return[...n,...e.filter(t=>!n.includes(t))]}catch(n){if(e.length&&n.message===tr)return e;throw n}}function _u(e){return e.split("\n").slice(1).map(n=>n.trim().split(/\s+/)).filter(n=>n[1]==="device").map(n=>n[0])}async function Vn(e){const n=Su(await J(e,"wm size"));const t=xu(await J(e,"dumpsys input").catch(()=>""));return t%2===1?{width:n.height,height:n.width}:n}function Su(e){const n=/Override size:\s*(\d+)x(\d+)/.exec(e);const t=/Physical size:\s*(\d+)x(\d+)/.exec(e);const i=n??t;if(!i)throw new Error(`Could not read the Android screen size (adb said: ${e.trim().slice(0,120)}).`);return{width:Number(i[1]),height:Number(i[2])}}function xu(e){const n=/SurfaceOrientation:\s*(\d)/.exec(e);return n?Number(n[1]):0}async function nr(e,n){const t=await J(e,"dumpsys window windows").catch(()=>"");return Tu(t,n)}function Tu(e,n){const t=e.split(/\n(?=\s*Window #\d+)/);for(const i of t){if(!i.includes(`${n}/`))continue;const r=/mFrame=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(i)??/\bframe=\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(i);if(r){const[o,a,s,c]=r.slice(1).map(Number);if(s>o&&c>a)return{x:o,y:a,width:s-o,height:c-a}}}return null}async function Du(e){return Iu(await J(e,"dumpsys activity activities"))}function Iu(e){const n=/(?:topResumedActivity|mResumedActivity|ResumedActivity)[^\n]*?\s([a-zA-Z][\w.]*)\/[\w.$]+/.exec(e);return n?n[1]:null}async function Mu(e){const n=await J(e,"pm list packages -3");return n.split("\n").map(t=>t.trim().replace(/^package:/,"")).filter(t=>/^[A-Za-z][\w.]+$/.test(t))}async function aa(e,n){ia(n);const t=await J(e,`run-as ${de(n)} cat files/react-buoy/@react_buoy_sync_device_id.json 2>/dev/null`,5e3).catch(()=>"");const i=t.trim().replace(/^"|"$/g,"");if(/^[\w-]{4,64}$/.test(i))return i;const r=await ra(e,["exec-out",`run-as ${de(n)} sh -c 'cat app_webview/Default/"Local Storage"/leveldb/*.log app_webview/Default/"Local Storage"/leveldb/*.ldb 2>/dev/null'`],5e3).catch(()=>null);return r?Au(r):null}function Au(e){const n=e.toString("latin1");const t=/@react_buoy_sync_device_id([\s\S])\x01([\w-]{4,64})/g;let i=null;for(let r=t.exec(n);r;r=t.exec(n)){const o=r[1].charCodeAt(0)-1;if(o>=4&&o<=r[2].length)i=r[2].slice(0,o)}return i}function Cu(e){if(/[^\x20-\x7e]/.test(e))throw new Error("adb can only type plain ASCII text. Use tap_element's text for other characters.");return de(e.replace(/ /g,"%s"))}async function Eu(e,n,t,i){const[r,o]=[Math.round(n),Math.round(t)];if(i)await J(e,`input swipe ${r} ${o} ${r} ${o} ${Math.round(i)}`);else await J(e,`input tap ${r} ${o}`)}async function Ou(e,n,t,i){const r=[...n,...t].map(o=>Math.round(o)).join(" ");await J(e,`input swipe ${r} ${Math.round(i)}`)}async function Ru(e,n){const t=n.replace(/ /g,"\0");for(let i=0;i<t.length;i+=40){const r=t.slice(i,i+40).replace(/\u0000/g," ");await J(e,`input text ${Cu(r)}`)}}async function Pu(e){const n=await ra(e,["exec-out","screencap","-p"]);if(n.length<8||n.readUInt32BE(0)!==2303741511)throw new Error("The Android screenshot was not a PNG.");return n}async function sa(e,n){if(n){await J(e,"settings put global sysui_demo_allowed 1");for(const t of["-e command enter","-e command clock -e hhmm 0941","-e command battery -e level 100 -e plugged false","-e command network -e wifi show -e level 4","-e command notifications -e visible false"]){await J(e,`am broadcast -a com.android.systemui.demo ${t}`).catch(()=>void 0)}}else{await J(e,"am broadcast -a com.android.systemui.demo -e command exit").catch(()=>void 0)}}async function ir(e){const n=await Le(e,["exec-out","uiautomator","dump","/dev/tty"],2e4);return Nu(n)}function Nu(e){const n=[];const t=(i,r)=>{const o=new RegExp(`\\s${r}="([^"]*)"`).exec(i);return o?Fu(o[1]):""};for(const i of e.matchAll(/<node\b[^>]*>/g)){const r=i[0];const o=/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(t(r,"bounds"));if(!o)continue;const[a,s,c,l]=o.slice(1).map(Number);n.push({text:t(r,"text"),desc:t(r,"content-desc"),id:t(r,"resource-id"),cls:t(r,"class"),clickable:t(r,"clickable")==="true",bounds:{x:a,y:s,width:c-a,height:l-s}})}return n}function Fu(e){return e.replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&#(\d+);/g,(n,t)=>String.fromCharCode(Number(t))).replace(/&amp;/g,"&")}function Bu(e,n){const t=n.trim().toLowerCase();const i=c=>{let l=0;for(const d of[c.desc,c.text,c.id.split("/").pop()??""]){const u=d.trim().toLowerCase();if(!u)continue;if(u===t)l=Math.max(l,3);else if(u.startsWith(t))l=Math.max(l,2);else if(u.includes(t))l=Math.max(l,1)}return l};const r=e.filter(c=>c.bounds.width>0&&c.bounds.height>0&&i(c)>0);const o=Math.max(0,...r.map(i));const a=r.filter(c=>i(c)===o);let s=a.filter((c,l)=>a.findIndex(d=>JSON.stringify(d.bounds)===JSON.stringify(c.bounds))===l);if(s.length>1){const c=s.filter(l=>[l.desc,l.text,l.id.split("/").pop()??""].some(d=>d.trim()===n.trim()));if(c.length)s=c}if(s.length>1){const c=s.filter(l=>l.clickable||/EditText|Button/.test(l.cls));if(c.length)s=c}return s}var ju=fu(pu);var ca=/^(@buoy\/session:|buoy-gg-license|buoy-gg-device-fingerprint$|@react_buoy_sync_device_id$|@react_buoy_install)/;async function rr(e){return Xi.readdir(e).catch(()=>[])}async function or(e){const n=[];const t=Rt.join(e,"Library","WebKit");for(const i of await rr(t)){const r=Rt.join(t,i,"WebsiteData","Default");for(const o of await rr(r)){for(const a of await rr(Rt.join(r,o))){const s=Rt.join(r,o,a);const c=Rt.join(s,"LocalStorage","localstorage.sqlite3");if(!await Xi.access(c).then(()=>true,()=>false))continue;const l=await Xi.readFile(Rt.join(s,"origin")).then(d=>d.toString("hex"),()=>"");if(l)n.push({db:c,origin:l})}}}return n}var la=(e,n,t=true)=>ju("sqlite3",[...t?["-readonly"]:[],e,n],{timeout:5e3}).then(i=>i.stdout);async function Lu(e){const n=[];for(const{db:t,origin:i}of await or(e)){const r=await la(t,"SELECT key, hex(value) FROM ItemTable;");for(const o of r.split("\n")){const a=o.lastIndexOf("|");if(a<0)continue;const s=o.slice(0,a);if(ca.test(s))n.push({key:s,value:o.slice(a+1),origin:i})}}return n}async function qu(e,n){if(!n.length)return false;const t=await or(e);const i=new Set(n.map(r=>r.origin??""));for(const r of i){const o=r?t.find(s=>s.origin===r):void 0;if(!o)return false;const a=n.filter(s=>(s.origin??"")===r&&/^[0-9A-F]*$/i.test(s.value)).map(s=>`('${s.key.replace(/'/g,"''")}', X'${s.value}')`).join(",");if(a)await la(o.db,`INSERT OR REPLACE INTO ItemTable (key, value) VALUES ${a};`,false)}return true}async function Uu(e,n=15e3){const t=Date.now()+n;while(Date.now()<t){if((await or(e)).length)return true;await new Promise(i=>setTimeout(i,500))}return false}async function zu(e,n,t){const i=(await J(e,`pidof ${n}`).catch(()=>"")).trim().split(/\s+/)[0];if(!i)throw new Error("the app is not running");const r=(await Le(e,["forward","tcp:0",`localabstract:webview_devtools_remote_${i}`])).trim();try{const o=await(await fetch(`http://127.0.0.1:${r}/json`)).json();const a=o.find(s=>s.type==="page"&&s.webSocketDebuggerUrl);if(!a?.webSocketDebuggerUrl)throw new Error("no debuggable WebView page (needs a debug build)");return await new Promise((s,c)=>{const l=new mu(a.webSocketDebuggerUrl);const d=setTimeout(()=>{l.close();c(new Error("the WebView did not answer"))},5e3);l.on("open",()=>l.send(JSON.stringify({id:1,method:"Runtime.evaluate",params:{expression:t,returnByValue:true}})));l.on("message",u=>{const h=JSON.parse(String(u));if(h.id!==1)return;clearTimeout(d);l.close();if(h.result?.exceptionDetails)c(new Error("the WebView script failed"));else s(h.result?.result?.value)});l.on("error",u=>{clearTimeout(d);c(u)})})}finally{await Le(e,["forward","--remove",`tcp:${r}`]).catch(()=>void 0)}}async function da(e,n,t,i=15e3){const r=Date.now()+i;let o;while(Date.now()<r){try{return await zu(e,n,t)}catch(a){o=a;await new Promise(s=>setTimeout(s,700))}}throw o instanceof Error?o:new Error("the WebView did not open")}async function Wu(e,n){const t=await J(e,`run-as ${n} sh -c 'ls app_webview/Default/"Local Storage"/leveldb 2>/dev/null'`).catch(()=>"");return/CURRENT|\.log/.test(t)}async function Hu(e,n){if(!(await J(e,`pidof ${n}`).catch(()=>"")).trim()){await J(e,`monkey -p ${n} -c android.intent.category.LAUNCHER 1`)}const t=await da(e,n,`(() => { const re = ${ca}; const out = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && re.test(k)) out.push({ key: k, value: localStorage.getItem(k) }); } return out; })()`);return Array.isArray(t)?t.filter(i=>typeof i.key==="string"&&typeof i.value==="string"):[]}async function Ju(e,n,t){await da(e,n,`(() => { for (const r of ${JSON.stringify(t)}) localStorage.setItem(r.key, r.value); setTimeout(() => location.reload(), 50); return true; })()`)}var ua=Zd(Xd);var st=new Map;function Yn(e){return st.get(e)}async function Je(e,n,t=2e4,i){try{const{stdout:r}=await ua(e,n,{timeout:t,maxBuffer:16*1024*1024,env:i?{...process.env,...i}:void 0});return r}catch(r){const o=r;if(o.code==="ENOENT")throw new Error(e==="xcrun"?"This needs macOS with Xcode (xcrun simctl).":`${e} is not installed.`);throw new Error(`${e} ${n.slice(0,2).join(" ")} failed: ${(o.stderr||o.message||"").trim().slice(0,300)}`)}}var we=(e,n)=>Je("xcrun",["simctl",...e],n);async function ar(e,n){const t=await al(e,n).catch(()=>({}));return Je("xcrun",["simctl","launch",e,n],void 0,t)}var ln=e=>e.split("-").pop()??"";async function sr(e,n){if(!n){const t=await cr(e,["rn"]);return t.length?t:cr(e,["webview"])}return cr(e,n)}async function cr(e,n){let t=[];try{const r=JSON.parse(await we(["list","devices","booted","-j"])).devices;t=Object.entries(r).filter(([o])=>/iOS/.test(o)).flatMap(([,o])=>o.filter(a=>a.state==="Booted"))}catch{return[]}const i=await Promise.all(t.map(r=>ha(e,r.udid,r.dataPath,n)));return t.flatMap((r,o)=>i[o]?[{udid:r.udid,name:r.name,found:i[o]}]:[])}function lr(e,n){return new Error(`${n.length} booted simulators hold "${e}" (one is probably a clone): ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}. Pass udid to choose, or shut the extra one down.`)}async function ha(e,n,t,i){const r=ln(e);if(!r)return null;if(!t){try{const c=JSON.parse(await we(["list","devices","-j"])).devices;t=Object.values(c).flat().find(l=>l.udid===n)?.dataPath}catch{return null}}if(!t)return null;const o=Y.join(t,"Containers","Data","Application");let a=[];try{a=await ie.readdir(o)}catch{return null}const s=await Promise.all(a.map(c=>dr(Y.join(o,c),r,i)));for(const[c,l]of a.entries()){const d=Y.join(o,l);if(!s[c])continue;let u=null;try{u=(await Je("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",Y.join(d,".com.apple.mobile_container_manager.metadata.plist")])).trim()||null}catch{}return{container:d,bundleId:u,kind:s[c]}}return null}async function dr(e,n,t=["rn","webview"]){if(t.includes("rn")){try{if((await ie.readFile(Y.join(e,"Documents","react-buoy","@react_buoy_sync_device_id.json"),"utf8")).trim()===n)return"rn"}catch{}}if(!t.includes("webview"))return null;return(await Wo(e).catch(()=>[])).includes(n)?"webview":null}async function Gu(e,n){const t=ln(e);const i=n.serial?[n.serial]:await oa();if(i.length===0)throw new Error("No Android device or emulator is connected to adb.");for(const r of i){const o=await Du(r).catch(()=>null);const a=await Mu(r).catch(()=>[]);const s=[n.bundleId,o,...a].filter((c,l,d)=>!!c&&d.indexOf(c)===l).slice(0,80);for(const c of s){if(await aa(r,c).catch(()=>null)===t)return{platform:"android",serial:r,packageName:c}}}return null}async function Ku(e){const n=ln(e);for(const t of await ru()){for(const i of await Ko(t.udid).catch(()=>[])){if(await Yo(t.udid,i.bundleId)===n)return{platform:"ios-device",udid:t.udid,name:t.name,bundleId:i.bundleId}}}return null}var ur=new Map;function dn(e,n,t={}){if(t.udid||t.serial||t.bundleId)return pa(e,n,t);const i=`${e}|${n??""}`;const r=ur.get(i);if(r)return r;const o=pa(e,n,t).finally(()=>ur.delete(i));ur.set(i,o);return o}async function pa(e,n,t={}){if(e==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');const i=st.get(e);if(i&&!t.udid&&!t.serial&&await ma(e,i)){if(i.platform==="ios"&&!i.picked){const l=await sr(e,i.idKind?[i.idKind]:void 0);if(l.length>1)throw lr(e,l);if(!l.some(d=>d.udid===i.udid)){st.delete(e);throw new Error(`The sim no longer holds "${e}". Try the host scan again.`)}}return i}if(i)st.delete(e);if(n==="android"){const l=await Gu(e,t);if(!l){throw new Error(`Could not prove which Android device and app run "${e}". Buoy checks the app's install id with run-as, which needs a debug build of the app on a device adb can reach${t.serial?` (looked on ${t.serial})`:""}.`)}st.set(e,l);return l}if(n&&n!=="ios")throw new Error(`Device control works on iOS Simulators and Android. This app runs on "${n}".`);if(process.platform!=="darwin")throw new Error("iOS control needs macOS with Xcode.");let r=t.udid;let o=r??"";let a=null;if(!r){const l=await sr(e);a=l.length===1?l[0]:null;if(l.length>1)throw lr(e,l);const d=l.length===1?{ok:true,udid:l[0].udid,how:`matched ${e} to "${l[0].name}"`}:await Jo(e);if(!d.ok||!/^matched /.test(d.how)){const u=await Ku(e).catch(()=>null);if(u){st.set(e,u);return u}throw new Error(`Could not find which simulator or iPhone runs "${e}": no booted simulator holds its install id, and no connected iPhone has it (an iPhone must be unlocked, paired, and run a development build).`)}r=d.udid;o=/"([^"]+)"/.exec(d.how)?.[1]??r}const s=a?.udid===r?await dr(a.found.container,ln(e),[a.found.kind])?a.found:null:await ha(e,r);if(!s)throw new Error(`Simulator ${r} does not hold "${e}".`);const c={platform:"ios",udid:r,name:o,bundleId:t.bundleId??s?.bundleId??null,container:s?.container??null,picked:!!t.udid,idKind:s.kind};st.set(e,c);return c}async function fa(e,n){if(n.platform==="ios"&&!n.picked){const t=await sr(e);if(t.length>1)throw lr(e,t)}if(await ma(e,n))return n;st.delete(e);return dn(e,n.platform==="android"?"android":"ios",n.platform==="ios"&&n.picked?{udid:n.udid}:{})}async function ma(e,n){const t=ln(e);if(n.platform==="android")return await aa(n.serial,n.packageName).catch(()=>null)===t;if(n.platform==="ios-device")return await Yo(n.udid,n.bundleId).catch(()=>null)===t;if(n.container)return!!await dr(n.container,t);const i=await Jo(e);return i.ok&&i.udid===n.udid&&/^matched /.test(i.how)}function hr(e){if(e.platform==="android")return ia(e.packageName);if(e.platform==="ios-device")return e.bundleId;if(!e.bundleId)throw new Error("Could not tell which app this is on the simulator (its container has no bundle metadata).");return e.bundleId}async function un(e){if(e.platform==="android")return Pu(e.serial);if(e.platform==="ios-device")return Xo(e.udid);const n=await Go(e.udid);if(!n.ok||!n.srcPath)throw new Error(n.error??"Screenshot failed.");try{return await ie.readFile(n.srcPath)}finally{await ie.unlink(n.srcPath).catch(()=>{})}}async function Nt(e,n=1280){if(process.platform==="darwin"){const r=Y.join(sn.tmpdir(),`buoy-mcp-img-${Qd()}.png`);const o=r.replace(/\.png$/,".jpg");await ie.writeFile(r,e);try{await Je("sips",["-Z",String(n),"-s","format","jpeg","-s","formatOptions","70",r,"--out",o],15e3);const a=await Je("sips",["-g","pixelWidth","-g","pixelHeight",o]);return{data:(await ie.readFile(o)).toString("base64"),mimeType:"image/jpeg",width:Number(/pixelWidth:\s*(\d+)/.exec(a)?.[1]??0),height:Number(/pixelHeight:\s*(\d+)/.exec(a)?.[1]??0)}}finally{await ie.unlink(r).catch(()=>{});await ie.unlink(o).catch(()=>{})}}const t=Vu(Ji.PNG.sync.read(e),Math.min(n,1024));const i=Ji.PNG.sync.write(t,{colorType:2,deflateLevel:9});return{data:i.toString("base64"),mimeType:"image/png",width:t.width,height:t.height}}function Vu(e,n){const t=Math.min(1,n/Math.max(e.width,e.height));if(t===1)return e;const i=Math.max(1,Math.round(e.width*t));const r=Math.max(1,Math.round(e.height*t));const o=new Ji.PNG({width:i,height:r});const a=e.width/i;const s=e.height/r;for(let c=0;c<r;c++){const l=Math.floor(c*s);const d=Math.max(l+1,Math.floor((c+1)*s));for(let u=0;u<i;u++){const h=Math.floor(u*a);const b=Math.max(h+1,Math.floor((u+1)*a));const f=[0,0,0,0];let w=0;for(let m=l;m<d;m++){for(let p=h;p<b;p++){const y=(m*e.width+p)*4;for(let v=0;v<4;v++)f[v]+=e.data[y+v];w++}}const g=(c*i+u)*4;for(let m=0;m<4;m++)o.data[g+m]=Math.round(f[m]/w)}}return o}var pr=new Set;async function Yu(e,n){let t=false;try{if(e.platform==="ios"){const i=await we(["status_bar",e.udid,"list"]).catch(()=>"");const r=i.split(/^=+$/m).slice(1).join("").trim();if(!r){await we(["status_bar",e.udid,"override","--time","9:41","--batteryState","charged","--batteryLevel","100","--wifiBars","3","--cellularBars","4"]);t=true}}else if(e.platform==="android"){const i=(await J(e.serial,"settings get global sysui_demo_allowed").catch(()=>"")).trim();if(i!=="1"&&!pr.has(e.serial)){pr.add(e.serial);await sa(e.serial,true);t=true}}if(t)await new Promise(i=>setTimeout(i,400))}catch{t=false}try{return{result:await n(),steadied:t}}finally{if(t){if(e.platform==="ios")await we(["status_bar",e.udid,"clear"]).catch(()=>void 0);else if(e.platform==="android"){await sa(e.serial,false);await J(e.serial,"settings put global sysui_demo_allowed 0").catch(()=>void 0);pr.delete(e.serial)}}}}async function fr(e){const n=hr(e);if(e.platform==="ios-device"){const t=await Zo(e.udid,n);return t?`Quit ${n}.`:`${n} was not running.`}if(e.platform==="ios"){await we(["terminate",e.udid,n]).catch(t=>{if(!/found nothing to terminate|not running/i.test(String(t)))throw t})}else{await J(e.serial,`am force-stop ${de(n)}`)}return`Quit ${n}.`}async function Ft(e){const n=hr(e);if(e.platform==="ios")await ar(e.udid,n);else if(e.platform==="ios-device")await Yi(e.udid,n);else await J(e.serial,`monkey -p ${de(n)} -c android.intent.category.LAUNCHER 1`);return`Launched ${n}.`}async function Xu(e){if(e.platform==="ios-device"){await Yi(e.udid,"com.apple.Preferences");return"Sent to the background for real (Settings opened on top)."}if(e.platform==="ios"){await we(["launch",e.udid,"com.apple.Preferences"]);return"Sent to the background for real (Settings opened on top)."}await J(e.serial,"input keyevent 3");return"Sent to the background for real (pressed Home)."}async function Zu(e,n){if(!/^[a-zA-Z][\w+.-]*:/.test(n))throw new Error(`"${n}" is not a link with a scheme (like https://\u2026 or myapp://\u2026).`);if(e.platform==="ios-device"){await cu(e.udid,n);return"Opened through the OS on the iPhone."}if(e.platform==="ios"){await we(["openurl",e.udid,n]);return'Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.'}await J(e.serial,`am start -a android.intent.action.VIEW -d ${de(n)}`);return"Opened through the OS (an Android VIEW intent)."}var Qu=["buoy-license","react-buoy/@react_buoy_sync_device_id.json","react-buoy/@react_buoy_install.json"];async function wa(e){const n=Y.join(e,"Info.plist");const t=a=>Je("plutil",["-extract",a,"raw","-o","-",n]).then(s=>s.trim()).catch(()=>"");const i=await t("CFBundleIdentifier");if(!i)throw new Error(`${e} has no Info.plist with a bundle id. Pass a built .app folder.`);const r=(await Je("plutil",["-extract","CFBundleSupportedPlatforms","json","-o","-",n]).catch(()=>"[]")).toLowerCase();const o=(await t("DTPlatformName")).toLowerCase();return{bundleId:i,simulator:r.includes("iphonesimulator")||o==="iphonesimulator"}}async function eh(e){const n=[process.env.ANDROID_HOME,process.env.ANDROID_SDK_ROOT,Y.join(sn.homedir(),"Library","Android","sdk"),Y.join(sn.homedir(),"Android","Sdk")].filter(t=>!!t);for(const t of n){const i=Y.join(t,"build-tools");const r=(await ie.readdir(i).catch(()=>[])).sort().reverse();for(const o of r){for(const a of["aapt2","aapt"]){const s=Y.join(i,o,a);try{await ie.access(s)}catch{continue}const c=await Je(s,a==="aapt2"?["dump","packagename",e]:["dump","badging",e]).catch(()=>"");const l=a==="aapt2"?c.trim().split("\n")[0]:/package: name='([^']+)'/.exec(c)?.[1]??"";if(l)return l}}}throw new Error("Could not read the APK's package name (needs aapt2 from the Android SDK build-tools). Nothing was changed.")}async function th(e,n,t=true){const i=hr(e);const r=Y.resolve(n);try{await ie.access(r)}catch{throw new Error(`No build at ${r}. Pass appPath: the built .app folder (iOS Simulator) or .apk file (Android). Nothing was changed.`)}if(e.platform==="android")return nh(e,i,r,t);if(e.platform==="ios-device")return ih(e,i,r,t);if(!/\.app\/?$/i.test(r))throw new Error("On the iOS Simulator, appPath must be a built .app folder. Nothing was changed.");const o=await wa(r);if(o.bundleId!==i)throw new Error(`${Y.basename(r)} is ${o.bundleId}, but this app is ${i}. Nothing was changed.`);if(!o.simulator)throw new Error(`${Y.basename(r)} is a device build, not a simulator build. Nothing was changed.`);const a=t?await ie.mkdtemp(Y.join(sn.tmpdir(),"buoy-mcp-keep-")):null;try{let s=false;let c=[];if(a){await ie.chmod(a,448);const d=(await we(["get_app_container",e.udid,i,"data"]).catch(()=>"")).trim();if(d)s=await ga(Y.join(d,"Documents"),a);if(d&&!s){try{c=await Lu(d)}catch(u){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView storage (${u.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}}await we(["terminate",e.udid,i]).catch(()=>void 0);await we(["uninstall",e.udid,i]);try{await we(["install",e.udid,r],18e4)}catch(d){throw new Error(`The app was removed, but installing ${Y.basename(r)} failed: ${d.message}. Install it again by hand.`)}let l="";if(a&&s){try{const d=(await we(["get_app_container",e.udid,i,"data"])).trim();await ga(a,Y.join(d,"Documents"));e.container=d;l=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(d){l=` Buoy's sign-in could not be restored (${d.message}): sign in to Buoy again in the app.`}}else if(c.length){try{const d=(await we(["get_app_container",e.udid,i,"data"])).trim();await ar(e.udid,i);const u=await Uu(d);await we(["terminate",e.udid,i]).catch(()=>void 0);if(!u||!await qu(d,c))throw new Error("the app made no WebView storage");e.container=d;l=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(d){l=` Buoy's sign-in could not be restored (${d.message}): sign in to Buoy again in the app.`}}else if(t){l=" The old install had no Buoy sign-in to keep."}await ar(e.udid,i);return`Reinstalled ${i} from ${r} and launched it. The app's own data is gone, like a new user.${l}`}finally{if(a)await ie.rm(a,{recursive:true,force:true}).catch(()=>void 0)}}async function ga(e,n){let t=false;for(const i of Qu){const r=Y.join(e,i);try{await ie.access(r)}catch{continue}const o=Y.join(n,i);await ie.mkdir(Y.dirname(o),{recursive:true});await ie.cp(r,o,{recursive:true,force:true});t=true}return t}async function nh(e,n,t,i){if(!/\.apk$/i.test(t))throw new Error("On Android, appPath must be an .apk file. Nothing was changed.");const r=await eh(t);if(r!==n)throw new Error(`${Y.basename(t)} is ${r}, but this app is ${n}. Nothing was changed.`);const o=[];if(i){const c=await J(e.serial,`run-as ${de(n)} find files/buoy-license files/react-buoy -maxdepth 1 -type f 2>/dev/null`).catch(()=>"");const l=c.split("\n").map(d=>d.trim()).filter(d=>d.startsWith("files/buoy-license/")||/react-buoy\/@react_buoy_(sync_device_id|install)\.json$/.test(d));for(const d of l){const u=await J(e.serial,`run-as ${de(n)} base64 ${de(d)}`).catch(()=>"");if(u.trim())o.push({rel:d,data:u.replace(/\s+/g,"")})}}let a=[];if(i&&!o.length&&await Wu(e.serial,n)){try{a=await Hu(e.serial,n)}catch(c){throw new Error(`Couldn't read Buoy's sign-in from the app's WebView (${c.message}). Nothing was changed. Pass keepBuoy: false to reinstall anyway.`)}}await Le(e.serial,["install","-r","-d",t],18e4);await J(e.serial,`pm clear ${de(n)}`);let s="";if(a.length){await Ft(e);try{await Ju(e.serial,n,a);s=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){s=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${s}`}if(o.length){try{for(const c of o){const l=Y.posix.dirname(c.rel);await J(e.serial,`echo ${de(c.data)} | run-as ${de(n)} sh -c ${de(`mkdir -p ${de(l)} && base64 -d > ${de(c.rel)}`)}`)}s=" Kept Buoy's sign-in and device id, so the same deviceId still works."}catch(c){s=` Buoy's sign-in could not be restored (${c.message}): sign in to Buoy again in the app.`}}else if(i){s=" No Buoy sign-in could be read to keep (needs a debug build): sign in to Buoy again in the app."}await Ft(e);return`Reinstalled ${n} from ${t} and launched it. The app's data is gone, like a new user.${s}`}async function ih(e,n,t,i){if(!/\.app\/?$/i.test(t))throw new Error("On an iPhone, appPath must be a built .app folder (a device build). Nothing was changed.");const r=await wa(t);if(r.bundleId!==n)throw new Error(`${Y.basename(t)} is ${r.bundleId}, but this app is ${n}. Nothing was changed.`);if(r.simulator)throw new Error(`${Y.basename(t)} is a simulator build, not a device build. Nothing was changed.`);const o=["Documents/buoy-license/buoy-gg-license.json","Documents/react-buoy/@react_buoy_sync_device_id.json","Documents/react-buoy/@react_buoy_install.json"];const a=i?await ie.mkdtemp(Y.join(sn.tmpdir(),"buoy-mcp-keep-")):null;try{const s=[];if(a){await ie.chmod(a,448);for(const l of o){let d;try{d=await Vo(e.udid,n,l)}catch(h){throw new Error(`Could not back up Buoy's ${Y.basename(l)} from the iPhone (${h.message}). Nothing was changed.`)}if(!d)continue;const u=Y.join(a,l.replace(/\//g,"__"));await ie.writeFile(u,d,{mode:384});s.push({rel:l,local:u})}}await Zo(e.udid,n).catch(()=>false);await lu(e.udid,n);try{await du(e.udid,t)}catch(l){throw new Error(`The app was removed, but installing ${Y.basename(t)} failed: ${l.message}. Install it again from Xcode.`)}let c="";if(s.length){try{for(const u of s)await uu(e.udid,n,u.local,u.rel);const l=s.some(u=>u.rel.includes("buoy-license"));const d=s.some(u=>u.rel.includes("sync_device_id"));c=l?` Kept Buoy's sign-in${d?" and device id, so the same deviceId still works":""}.`:` Kept the device id; there was no Buoy sign-in to keep, so sign in to Buoy in the app.`}catch(l){c=` Buoy's sign-in could not be restored (${l.message}): sign in to Buoy again in the app.`}}else if(i)c=" The old install had no Buoy sign-in to keep.";await Yi(e.udid,n);return`Reinstalled ${n} on the iPhone and launched it. The app's own data is gone, like a new user.${c}`}finally{if(a)await ie.rm(a,{recursive:true,force:true}).catch(()=>void 0)}}async function rh(e){const n=JSON.parse(await we(["list","devices","available","-j"])).devices;const t=Object.entries(n).filter(([s])=>/iOS/.test(s)).flatMap(([,s])=>s);const i=e.trim().toLowerCase();const r=t.filter(s=>s.udid.toLowerCase()===i||s.name.toLowerCase()===i);const o=r.length?r:t.filter(s=>s.name.toLowerCase().includes(i));if(o.length===0)throw new Error(`No iOS Simulator matches "${e}". Run \`xcrun simctl list devices available\` to see them.`);if(o.length>1)throw new Error(`${o.length} simulators match "${e}": ${o.map(s=>`${s.name} (${s.udid})`).join(", ")}. Pass the udid.`);const a=o[0];if(a.state!=="Booted"){try{await ua("xcrun",["simctl","boot",a.udid],{timeout:12e4,env:Qc()})}catch(s){const c=s;throw new Error(`xcrun simctl boot failed: ${(c.stderr||c.message||"").trim().slice(0,300)}`)}}await Je("open",["-a","Simulator","--args","-CurrentDeviceUDID",a.udid]).catch(()=>void 0);return a.state==="Booted"?`${a.name} (${a.udid}) was already booted.`:`Booted ${a.name} (${a.udid}).`}var oh=Yd(Vd);async function ge(e,n,t={},i={}){if(n==="All")throw new Error('Device control acts on one app. Pass its deviceId, not "All".');let r;let o=true;try{r=e.requireProDevice(n)}catch(c){const l=c instanceof Error?c.message:String(c);const d=l.startsWith(wt)&&/is not connected right now/.test(l)?/"([^"]+)"/.exec(l)?.[1]:void 0;const u=d?e.deviceWasAllowed(d):false;if(!i.allowOffline||!d||!u)throw c;r=d;o=false}const a=e.listDevices().find(c=>c.deviceId===r)?.platform;let s=await dn(r,a,t);if(i.changesApp&&Yn(r)===s)s=await fa(r,s);return{id:r,host:s,online:o}}function Bt(e){if(e.platform==="ios")return`simulator "${e.name}"`;if(e.platform==="ios-device")return`iPhone "${e.name}"`;return`Android device ${e.serial}`}var mr=new Map;function ba(e){if(!mr.has(e))mr.set(e,oh("which",[e],{timeout:3e3}).then(()=>true,()=>false));return mr.get(e)}async function ah(e,n){let t=n;if(e.platform==="android"){const r=Yn(e.deviceId);const o=r??(e.isConnected?await Promise.race([dn(e.deviceId,"android").catch(()=>void 0),new Promise(s=>setTimeout(()=>s(void 0),3e3))]):void 0);const a=o?.platform==="android"&&!!co(o.serial)||await ba("adb");return{kind:"android",...o&&o.platform==="android"?{id:o.serial,name:o.packageName}:{},can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall","real lifecycle events","native_tap/type/swipe","native_inspect"],...!a?{missing:["adb (Android SDK platform-tools)"]}:!o?{missing:["proof of which phone runs it (needs a debug build reachable by adb)"]}:{}}}if(e.platform!=="ios")return{kind:e.platform??"unknown",can:[],missing:["host control works on iOS and Android apps"]};let i=Yn(e.deviceId);if(!i&&!t&&e.isConnected){i=await Promise.race([dn(e.deviceId,"ios").catch(()=>void 0),new Promise(r=>setTimeout(()=>r(void 0),4e3))])}if(!t&&i?.platform==="ios")t={name:i.name,udid:i.udid};if(i?.platform==="ios-device"){return{kind:"physical iPhone",id:i.udid,name:i.name,can:["screenshot","screen_recording (if this iPhone supports it)","baseline diffs","quit/launch/reinstall","open_url","native_tap/type/swipe/pinch (by query, testID, nativeTag or x/y; not label)","native_inspect (the app's side only)"]}}if(t){const r=await ba("idb");return{kind:"ios simulator",id:t.udid,name:t.name,can:["screenshot","screen_recording","baseline diffs","quit/launch/reinstall/boot","real lifecycle events",...r?["native_tap/type/swipe/pinch","native_inspect"]:[]],...r?{}:{missing:["idb for real touches (brew install facebook/fb/idb-companion && pipx install fb-idb)"]}}}return void 0}function jt(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function Lt(e){return{isError:true,content:[{type:"text",text:e}]}}function va(e,n){e.registerTool("list_devices",{description:"List the apps connected to Buoy: id, name, platform, whether it is connected and has MCP access, the simulator or phone it runs on with the host controls that work there (screenshot, recording, app control, real touches) and anything missing, and the Buoy tools it exposes. Start here. Pass details:true for every tool's action names.",inputSchema:{details:te.boolean().optional().describe("Also list every action of every tool. Default false (tool names only); list_tool_actions gives one tool's actions."),includeDisconnected:te.boolean().optional().describe("Also list apps that were seen before but are not connected now. Default false.")}},async(t={})=>{const i=await Lo();let r=n.listDevices();for(let b=0;r.length===0&&b<10;b++){await new Promise(f=>setTimeout(f,200));r=n.listDevices()}if(r.length===0){return jt({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...i?{updateAvailable:i}:{}})}const o=t.includeDisconnected?0:r.filter(b=>!b.isConnected).length;if(!t.includeDisconnected)r=r.filter(b=>b.isConnected);if(r.length===0){return jt({devices:[],hint:`No app is connected right now${o?` (${o} seen before; pass includeDisconnected:true to list them)`:""}. Open the app with Buoy devtools running, then retry.`,...i?{updateAvailable:i}:{}})}const a=await Gd(r.filter(b=>b.platform==="ios").map(b=>b.deviceId)).catch(()=>new Map);const s=r.filter(b=>b.isConnected);const c=s.some(b=>fe(b));const l=s.some(b=>b.tier!=="locked");const d=s.filter(b=>b.collision);const u=await Promise.all(r.map(b=>b.isConnected?ah(b,a.get(b.deviceId)).catch(()=>void 0):void 0));const h=r.map((b,f)=>{const w=a.get(b.deviceId);const g=u[f];return{deviceId:b.deviceId,deviceName:b.deviceName,platform:b.platform,isConnected:b.isConnected,isPro:b.isPro,tier:b.tier,...!b.isPro&&fe(b)?{weekendPass:true}:{},access:n.accessState(b.deviceId,"free"),...w?{simulator:w}:{},...g?{host:g}:{},...b.collision?{collision:true}:{},tools:t.details?b.tools:b.tools.map(m=>m.toolId)}});return jt({devices:h,...t.details?{}:{note:`Tool names only. Pass details:true, or list_tool_actions({toolId}), for action names.${o?` ${o} apps seen before but not connected now are left out (includeDisconnected:true lists them).`:""}`},...d.length>0?{warning:`${d.map(b=>`"${b.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.`}:{},...c?{}:{hint:l?`The connected app is on the free tier, so only the free MCP tools work: ${fl().join(", ")} (get_network_requests without includeBodies; get_triage without storage values). Every other tool needs Buoy Pro (https://buoy.gg/pricing). To add a license, enter it 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."},...i?{updateAvailable:i}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a tool exposes, and which connected devices offer it. Leave toolId out to list every tool's actions. Use the action names with call_action.",inputSchema:{toolId:te.string().optional().describe('The tool id, e.g. "perf-monitor", "network", "react-query". Leave out for every tool.'),tool:te.string().optional().describe("Same as toolId."),deviceId:te.string().optional().describe("Only list what this device offers.")}},async({toolId:t,tool:i,deviceId:r})=>{const o=t??i;if(!o){const c=n.connectedDevices().filter(d=>!r||d.deviceId===r);if(c.length===0)return Lt(r?`No connected device "${r}". Call list_devices.`:"No devices are connected. Call list_devices.");const l=c.map(d=>[`**${d.deviceName}** (${d.deviceId})`,...d.tools.map(u=>`- ${u.toolId}: ${u.actions.join(", ")}`)].join("\n"));return{content:[{type:"text",text:`${l.join("\n\n")}
3
3
 
4
- Call one with call_action({toolId, action, params}).`}]}}const a=new Set(n.connectedDevices().map(c=>c.deviceId));const s=n.toolActions(o).filter(c=>a.has(c.deviceId)&&(!r||c.deviceId===r));if(s.length===0){const c=[...new Set(n.connectedDevices().flatMap(l=>l.tools.map(d=>d.toolId)))].sort();return Nt(`No connected device exposes tool "${o}". Tool ids: ${c.join(", ")||"(none connected)"}. MCP tool names like "${o}" are not tool ids.`)}return Pt({toolId:o,offerings:s})});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:te.string().optional().describe("The tool id to read state from."),tool:te.string().optional().describe("Same as toolId."),deviceId:te.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out."),timeoutMs:te.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,tool:i,deviceId:r,timeoutMs:o})=>{try{const a=t??i;if(!a)return Nt("get_snapshot needs toolId (see list_tool_actions).");const s=n.requireProDevice(r);const{payload:c,stale:l}=await n.getSnapshot(a,s,{timeoutMs:o});return Pt({toolId:a,deviceId:s,...l?{stale:l}:{},snapshot:c})}catch(a){return Nt(ua(a))}});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:te.string().optional().describe('The tool id, e.g. "perf-monitor".'),tool:te.string().optional().describe("Same as toolId."),action:te.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:te.union([te.record(te.unknown()),te.string()]).optional().describe("JSON params object for the action (shape depends on the action)."),args:te.union([te.record(te.unknown()),te.string()]).optional().describe("Same as params."),deviceId:te.string().optional().describe('The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out. Or "All" to broadcast.'),timeoutMs:te.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,tool:i,action:r,params:o,args:a,deviceId:s,timeoutMs:c})=>{try{const l=t??i;if(!l)return Nt("call_action needs toolId (see list_tool_actions).");const d=Wu(o??a);const u=n.requireProDevice(s);const h=await n.callAction(l,r,d,u,{timeoutMs:c});return Pt({toolId:l,action:r,ok:true,result:h??null})}catch(l){return Nt(ua(l))}})}function Wu(e){if(e===void 0||typeof e==="object")return e;const n=e.trim();if(!n)return void 0;let t;try{t=JSON.parse(n)}catch{throw new Error('params is a string but not JSON. Pass an object, e.g. {"user": {...}}.')}if(!t||typeof t!=="object"||Array.isArray(t))throw new Error("params must be a JSON object.");return t}function ua(e){return e instanceof Error?e.message:String(e)}var Ft=(e,n,t)=>Math.max(n,Math.min(t,e));var cr="__batch_warmup__";var Hu={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var Bt={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function jt(e,n,t){return e??n??t}function qn(e,n={}){const t=[];const i=(e.targetRoute||"").trim();if(!i)throw new Error("targetRoute is required (the screen to benchmark).");let r=(e.bounceRoute||"").trim();if(!r)r=i==="/"?"/_buoy_bounce":"/";if(r===i){throw new Error(`bounceRoute ("${r}") 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((u,h)=>{const b=(u.name||"").trim()||`case ${h+1}`;return{id:`case-${h}`,name:b,params:u.params??{},route:u.route}});const a=Ft(Math.round(jt(e.runsPerCase,n.runsPerCase,Bt.runsPerCase)),1,10);const s=Ft(Math.round(jt(e.coolDownMs,n.coolDownMs,Bt.coolDownMs)),0,3e4);const c=Ft(Math.round(jt(e.discardWarmupRuns,n.discardWarmupRuns,Bt.discardWarmupRuns)),0,a-1);const l=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&l===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 d={targetRoute:i,bounceRoute:r,perCaseDurationMs:Ft(Math.round(jt(e.perCaseDurationMs,n.perCaseDurationMs,Bt.perCaseDurationMs)),1e3,6e4),settleMs:Ft(Math.round(jt(e.settleMs,n.settleMs,Bt.settleMs)),0,1e4),navTimeoutMs:Ft(Math.round(jt(e.navTimeoutMs,n.navTimeoutMs,Bt.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:l,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:d,warnings:t}}function ha(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const i=lr(e);const r=i/6e4;const o=r>=1?`~${r.toFixed(1)} min`:`~${Math.round(i/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 s=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${n(s)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function lr(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let i=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)i+=e.cases.length*3e3;return i}function Ju(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 Gu(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.jsFps!=null&&t&&e.jsFps<t*.95){n.push("Low main-thread frame rate")}else if(e.longFrames!=null&&e.frames>0&&e.longFrames/e.frames>=.02){n.push("Frequent long frames")}const i=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(i>=100)n.push(`Freeze of ${Math.round(i)} ms`);if(e.longTaskPct!=null&&e.longTaskPct>=20)n.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50){n.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`)}if(e.styleLayoutMs!=null&&e.styleLayoutMs>=100){n.push(`Style and layout in slow frames (${Math.round(e.styleLayoutMs)} ms)`)}if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){n.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}if(e.layoutShift!=null&&e.layoutShift>=.1)n.push(`Layout shifts (${e.layoutShift})`);if(e.domGrowth!=null&&e.domGrowth>0){n.push(`DOM grew every run (+${e.domGrowth} nodes): check for a leak`)}return n}function Ku(e){const n=e.filter(i=>!i.batchFailureReason&&!i.discardedWarmup).sort((i,r)=>(i.runIndex??0)-(r.runIndex??0)).map(i=>i.webRun?.domNodesStart).filter(i=>typeof i==="number");if(n.length<3)return null;for(let i=1;i<n.length;i++)if(n[i]<=n[i-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function Vu(e,n){const t=e.find(r=>r.isBaseline);if(t)return t;const i=n?.[0]?.name;if(i){const r=e.filter(o=>o.caseName===i);if(r.length===1)return r[0]}return e[0]}function Un(e,n,t,i=[]){const r=e.filter(m=>m.batchId===n);const o=new Map(i.map(m=>[m.index,m.reason]));const a=Array.from(new Set(r.map(m=>m.batchIndex??0))).sort((m,p)=>m-p);const s=m=>{const p=r.filter($=>($.batchIndex??0)===m);if(p.length===0)return void 0;const y=p.filter($=>!$.batchFailureReason&&!$.discardedWarmup);const v=y.filter($=>$.webRun?.fps!=null);if(v.length>0&&v.length===y.length){const $=[...v].sort((T,k)=>T.webRun.fps-k.webRun.fps);return $[Math.floor(($.length-1)/2)]}if(y.length>0){return y.find($=>$.isMedianRun===true)??y.find($=>$.runIndex===0||$.runIndex===void 0)??y[0]}return p.find($=>$.batchFailureReason)??p[0]};const c=m=>typeof m==="number"?m:null;const l=a.map(m=>{const p=s(m);const y=r.filter(P=>(P.batchIndex??0)===m);const v=y.length;const $=y.filter(P=>!P.batchFailureReason&&!P.discardedWarmup);const T=P=>P.webRun?P.webRun.fps??P.jsFpsAvg:P.uiFpsAvg;const k=$.map(T).filter(P=>typeof P==="number");const D=p?.name??t?.[m]?.name??`case ${m+1}`;const E=r.some(P=>(P.batchIndex??0)===m&&P.isBaseline===true);const I=o.get(m)??p?.batchFailureReason;const R=y.every(P=>P.batchFailureReason||P.discardedWarmup);const _=Boolean(I)||!p||R;const S={batchIndex:m,caseName:D,params:p?.params??{},runs:v,failed:_,...p?.route?{route:p.route}:{},...E?{isBaseline:true}:{},failureReason:_?I??(R&&p?"only warmup runs were saved":"no run recorded"):void 0,uiFps:c(p?.uiFpsAvg),jsFps:c(p?.jsFpsAvg),cpu:c(p?.cpuAvg),memMb:c(p?.memMaxMb),jsJank:c(p?.jsJankFrames),uiJank:c(p?.uiJankFrames),deviceMaxRefreshRate:c(p?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:$.length,fpsMin:k.length?Ee(Math.min(...k),1):null,fpsMax:k.length?Ee(Math.max(...k),1):null,...p?.thermalState?{thermalState:p.thermalState}:{},renderCommits:c(p?.renderCommits),renderWasted:p?.renderWasted,topRenderers:p?.topRenderers??[]};const A=p?.webRun;if(!A)return{...S,flags:_?[]:Ju(S)};const O=[...A.longTasksSupported?[]:["long tasks"],...A.heapSupported?[]:["JS heap"],...A.loaf===null?["long animation frames"]:[]];const F={...S,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:A.heapSupported?S.memMb:null,deviceMaxRefreshRate:Math.max(S.deviceMaxRefreshRate??0,A.refreshHz??0)||null,longTaskPct:A.longTaskPct,frameGapP95Ms:A.frameGapP95Ms,frameGapMaxMs:A.frameGapMaxMs,edgeSilenceMs:A.edgeSilenceMs??null,jsFps:A.fps??S.jsFps,longFrames:A.longFrames,...O.length?{missingMetrics:O}:{},loafBlockingMs:A.loaf?.blockingMs??null,styleLayoutMs:A.loaf?.styleLayoutMs??null,forcedLayoutMs:A.loaf?.forcedLayoutMs??null,topScripts:A.loaf?.topScripts??[],worstInteractionMs:A.interactions?.worstMs??null,layoutShift:A.layoutShift??null,domNodes:A.domNodesEnd??null,domGrowth:Ku(y),pageLoad:A.pageLoad??null,refreshCalibrated:A.refreshCalibrated??null};return{...F,flags:_?[]:Gu({...F,frames:A.frames})}}).filter(m=>m.caseName!==cr).map((m,p)=>({...m,batchIndex:p}));const d=Vu(l,t);if(d){d.isBaseline=true;for(const m of l){if(m===d)continue;if(m.uiFps!=null&&d.uiFps!=null)m.deltaUiFps=Ee(m.uiFps-d.uiFps,1);if(m.jsFps!=null&&d.jsFps!=null)m.deltaJsFps=Ee(m.jsFps-d.jsFps,1)}}const u=l.filter(m=>!m.failed);const h=(m,p)=>{let y;let v=NaN;for(const $ of u){const T=m($);if(T==null)continue;if(y===void 0||p(T,v)){y=$;v=T}}return y?.caseName};const b=m=>m.platform==="web"?m.longFrames??null:m.jsJank==null&&m.uiJank==null?null:(m.jsJank??0)+(m.uiJank??0);const f={uiFps:h(m=>m.uiFps,(m,p)=>m>p),jsFps:h(m=>m.jsFps,(m,p)=>m>p),lowestCpu:h(m=>m.cpu,(m,p)=>m<p),lowestMem:h(m=>m.memMb,(m,p)=>m<p),fewestJank:h(b,(m,p)=>m<p),fewestLongTasks:h(m=>m.longTaskPct??null,(m,p)=>m<p)};const w=u.filter(m=>m.flags.length>0).map(m=>({caseName:m.caseName,flags:m.flags}));const g=u.filter(m=>m.flags.length===0).sort((m,p)=>(p.uiFps??p.jsFps??0)-(m.uiFps??m.jsFps??0)||(m.cpu??m.longTaskPct??Infinity)-(p.cpu??p.longTaskPct??Infinity)||(m.memMb??Infinity)-(p.memMb??Infinity)).map(m=>m.caseName);return{batchId:n,baselineName:d?.caseName??null,deviceMaxRefreshRate:u.find(m=>m.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:l,leaders:f,flagged:w,balanced:g}}function Yu(e){const n=e.filter(a=>!a.failed);if(n.length===0)return[];const t=n.map(a=>a.pageLoad?.navigationId);if(t.some(a=>a==null)||new Set(t).size!==t.length)return[];if(n.some(a=>(a.pageLoad?.pageAgeMs??Infinity)>3e4))return[];const i=a=>(a??"").replace(/\/+$/,"")||"/";const r=a=>{const s=a.pageLoad;if(!s?.loadPath||i(s.loadPath)!==i(a.route))return false;const c=new URLSearchParams(s.loadSearch??"");const l=[...c.keys()];if(new Set(l).size!==l.length)return false;const d=Object.entries(a.params);return l.length===d.length&&d.every(([u,h])=>c.get(u)===h)};if(n.some(a=>!r(a)))return[];const o=a=>a==null?"\u2014":String(Math.round(a));return[`**Page load** (each case started on a fresh page load):`,"",`| Case | TTFB ms | FCP ms | LCP ms | DOM ready ms | Script KB | Total KB |`,`|------|--------:|-------:|-------:|-------------:|----------:|---------:|`,...n.map(a=>{const s=a.pageLoad;return`| ${a.caseName} | ${o(s.ttfbMs)} | ${o(s.fcpMs)} | ${o(s.lcpMs)} | ${o(s.domContentLoadedMs)} | ${o(s.scriptKb)} | ${o(s.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function pa(e){const n=o=>o.platform==="web"?o.jsFps:o.uiFps;const t=e.filter(o=>!o.failed&&n(o)!=null&&o.fpsMin!=null&&o.fpsMax!=null).sort((o,a)=>(n(a)??0)-(n(o)??0));if(t.length<2)return null;const[i,r]=t;if((i.validRuns??0)<2||(r.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${i.caseName} and ${r.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((r.fpsMax??0)>=(i.fpsMin??0)||(n(i)??0)-(n(r)??0)<=2){return`**Too close to call:** ${i.caseName} (${i.fpsMin}\u2013${i.fpsMax} FPS) and ${r.caseName} (${r.fpsMin}\u2013${r.fpsMax} FPS) overlap or sit within 2 FPS. No clear winner between them; more runs may separate them.`}return null}function an(e){const n=(h,b=1)=>h==null?"\u2014":h.toFixed(b);const t=h=>h==null?"":h>0?` (+${h.toFixed(1)})`:` (${h.toFixed(1)})`;const i=[];i.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(h=>h.platform==="web")){Zu(e,i);return i.join("\n")}i.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.`);i.push("");const r=e.rows.some(h=>h.thermalState);const o=r?" Thermal |":"";const a=r?"-------|":"";i.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);i.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const h of e.rows){const b=r?` ${Hu[h.thermalState??"unknown"]??"\u2014"} |`:"";if(h.failed){i.push(`| ${h.batchIndex} | ${h.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${b} \u274C FAILED (${h.failureReason}) |`);continue}const f=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";i.push(`| ${h.batchIndex} | ${h.caseName} | ${n(h.uiFps)}${t(h.deltaUiFps)} | ${n(h.jsFps)}${t(h.deltaJsFps)} | ${n(h.cpu)} | ${n(h.memMb)} | ${n(h.jsJank,0)} | ${n(h.uiJank,0)} |${b} ${f} |`)}const s=pa(e.rows);if(s){i.push("");i.push(s)}const c=e.rows.filter(h=>h.thermalState==="serious"||h.thermalState==="critical");if(c.length){i.push("");i.push(`**\u{1F525} Thermally throttled: ${c.map(h=>h.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.`)}i.push("");const l=e.leaders;const d=[l.uiFps&&`UI FPS \u2192 **${l.uiFps}**`,l.jsFps&&`JS FPS \u2192 **${l.jsFps}**`,l.lowestCpu&&`lowest CPU \u2192 **${l.lowestCpu}**`,l.lowestMem&&`lowest mem \u2192 **${l.lowestMem}**`,l.fewestJank&&`fewest jank \u2192 **${l.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)i.push(`**Per-metric best:** ${d}`);i.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){i.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(h=>`${h.caseName} (${h.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(h=>!h.failed&&(h.renderCommits!=null||h.topRenderers.length>0)).map(h=>{const b=h.topRenderers.map(g=>`${g.name} \xD7${g.renders} (${g.totalMs}ms${g.cause?`, ${g.cause}`:""})`).join(", ");const f=h.renderCommits!=null?`${h.renderCommits} commits`:"";const w=h.renderWasted!=null?`${h.renderWasted} wasted`:"";return`- **${h.caseName}** \u2014 ${[f,w,b].filter(Boolean).join(": ")}`});if(u.length){i.push("");i.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.");i.push(...u)}i.push("");i.push(`_Deltas vs baseline (\`${e.baselineName??"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 i.join("\n")}function dr(e,n){const t=(i,r)=>({a:{avg:Ee(i.avg,1),p95:Ee(i.p95,1)},b:{avg:Ee(r.avg,1),p95:Ee(r.p95,1)},deltaAvg:Ee(r.avg-i.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},...fa(e.renders,n.renders)}}function Xu(e,n){const t=(a,s,c)=>({a:a??null,b:s??null,delta:a!=null&&s!=null?Ee(s-a,1):null,better:c});const i=a=>a.webRun.heapSupported?Ee(a.stats.memoryUsage.max,1):null;const r=e.webRun;const o=n.webRun;return{a:e.name,b:n.name,platform:"web",note:"Browser runs: FPS is the main thread's whole-run requestAnimationFrame rate; long tasks % is not CPU. null = this browser couldn't measure it.",fps:t(r.fps??e.stats.jsFps.avg,o.fps??n.stats.jsFps.avg,"higher"),frameGapP95Ms:t(r.frameGapP95Ms,o.frameGapP95Ms,"lower"),frameGapMaxMs:t(r.frameGapMaxMs,o.frameGapMaxMs,"lower"),longFrames:t(r.longFrames,o.longFrames,"lower"),longTaskPct:t(r.longTaskPct,o.longTaskPct,"lower"),slowFrameBlockingMs:t(r.loaf?.blockingMs,o.loaf?.blockingMs,"lower"),styleLayoutPaintMs:t(r.loaf?.styleLayoutMs,o.loaf?.styleLayoutMs,"lower"),forcedLayoutMs:t(r.loaf?.forcedLayoutMs,o.loaf?.forcedLayoutMs,"lower"),worstInteractionMs:t(r.interactions?.worstMs,o.interactions?.worstMs,"lower"),layoutShift:t(r.layoutShift,o.layoutShift,"lower"),domNodes:t(r.domNodesEnd,o.domNodesEnd,"lower"),jsHeapMb:t(i(e),i(n),"lower"),topScripts:{a:r.loaf?.topScripts??[],b:o.loaf?.topScripts??[]},...fa(e.renders,n.renders)}}function fa(e,n){if(!e&&!n)return null;const t=i=>i??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 Ee(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function Zu(e,n){const t=(d,u=1)=>d==null?"\u2014":d.toFixed(u);const i=d=>d==null?"":d>0?` (+${d.toFixed(1)})`:` (${d.toFixed(1)})`;n.push(`**How to read this.** This is a browser run. FPS is the main thread's frame rate (requestAnimationFrame); it can't see frames dropped later by the GPU or compositor. Long tasks % is time the main thread was blocked by tasks over 50 ms, not CPU use. For Chrome's own dropped-frame count, use run_web_benchmark.`);n.push("");n.push(`| # | Case | FPS | p95 frame gap ms | Worst gap ms | Long frames | Long tasks % | JS heap MB | Flags |`);n.push(`|---|------|----:|-----------------:|-------------:|------------:|-------------:|--------:|-------|`);for(const d of e.rows){const u=d.isBaseline?`${d.caseName} (baseline)`:d.caseName;if(d.failed){n.push(`| ${d.batchIndex} | ${u} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${d.failureReason}) |`);continue}const h=d.flags.length?`\u26A0\uFE0F ${d.flags.join("; ")}`:"no flags";n.push(`| ${d.batchIndex} | ${u} | ${t(d.jsFps)}${i(d.deltaJsFps)} | ${t(d.frameGapP95Ms)} | ${t(d.frameGapMaxMs)} | ${t(d.longFrames,0)} | ${t(d.longTaskPct)} | ${t(d.memMb)} | ${h} |`)}const r=e.rows.filter(d=>!d.failed).map(d=>{const u=[d.loafBlockingMs!=null&&d.loafBlockingMs>0&&`slow frames blocked input ${d.loafBlockingMs} ms`,d.styleLayoutMs!=null&&d.styleLayoutMs>0&&`style + layout ${d.styleLayoutMs} ms`,d.forcedLayoutMs!=null&&d.forcedLayoutMs>0&&`forced layout ${d.forcedLayoutMs} ms`,d.worstInteractionMs!=null&&`slowest input ${d.worstInteractionMs} ms`,d.layoutShift!=null&&d.layoutShift>0&&`layout shift ${d.layoutShift}`,d.domNodes!=null&&`${d.domNodes} DOM nodes${d.domGrowth?` (+${d.domGrowth} over the runs)`:""}`,d.topScripts&&d.topScripts.length>0&&`top scripts: ${d.topScripts.slice(0,3).map(h=>`${h.name} ${h.ms} ms`).join(", ")}`].filter(Boolean);return u.length?`- **${d.caseName}**: ${u.join(" \xB7 ")}`:null}).filter(Boolean);if(r.length){n.push("");n.push(`**Why frames were slow** (Long Animation Frames, Event Timing, DOM size; slow frames that ran past a run's end are left out):`);n.push(...r)}const o=Yu(e.rows);if(o.length){n.push("");n.push(...o)}const a=pa(e.rows);if(a){n.push("");n.push(a)}const s=Array.from(new Set(e.rows.flatMap(d=>d.missingMetrics??[])));if(s.length){n.push("");n.push(`**Not measured in this browser:** ${s.join(", ")}. These show as \u2014 and can't win or lose.`)}if(e.rows.some(d=>d.refreshCalibrated===false)){n.push("");n.push(`**Refresh rate not checked on an idle screen:** the batch couldn't reach its bounce screen first, so the screen rate (and frame budgets) may read low.`)}if(e.deviceMaxRefreshRate){n.push("");n.push(`Screen rate: ${e.deviceMaxRefreshRate} Hz. A frame gap over ${(1500/e.deviceMaxRefreshRate).toFixed(1)} ms missed at least one frame.`)}n.push("");const c=e.leaders;const l=[c.jsFps&&`FPS \u2192 **${c.jsFps}**`,c.fewestJank&&`fewest long frames \u2192 **${c.fewestJank}**`,c.fewestLongTasks&&`fewest long tasks \u2192 **${c.fewestLongTasks}**`,c.lowestMem&&`lowest heap \u2192 **${c.lowestMem}**`].filter(Boolean).join(" \xB7 ");if(l)n.push(`**Per-metric leaders:** ${l}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(d=>`${d.caseName} (${d.flags.join("; ")})`).join(" \xB7 ")}`)}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.`);n.push("");n.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Confirm finalists on a real phone browser: desktop numbers don't predict mobile GPU, memory or heat._`)}import{z as G}from"zod";import{z as Qu}from"zod";var M=e=>({content:[{type:"text",text:e}]});var x=e=>({isError:true,content:[{type:"text",text:e}]});var j=e=>e instanceof Error?e.message:String(e);var L=Qu.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.");var He=()=>process.env.BUOY_LOAD_CORE_TOOLS==="0"?void 0:{"anthropic/alwaysLoad":true};function eh(e,n){return typeof e==="number"&&e>0?e:n}function at(e){return e?`${e}
4
+ Call one with call_action({toolId, action, params}).`}]}}const a=new Set(n.connectedDevices().map(c=>c.deviceId));const s=n.toolActions(o).filter(c=>a.has(c.deviceId)&&(!r||c.deviceId===r));if(s.length===0){const c=[...new Set(n.connectedDevices().flatMap(l=>l.tools.map(d=>d.toolId)))].sort();return Lt(`No connected device exposes tool "${o}". Tool ids: ${c.join(", ")||"(none connected)"}. MCP tool names like "${o}" are not tool ids.`)}return jt({toolId:o,offerings:s})});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:te.string().optional().describe("The tool id to read state from."),tool:te.string().optional().describe("Same as toolId."),deviceId:te.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out."),timeoutMs:te.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,tool:i,deviceId:r,timeoutMs:o})=>{try{const a=t??i;if(!a)return Lt("get_snapshot needs toolId (see list_tool_actions).");const s=n.requireProDevice(r);const{payload:c,stale:l}=await n.getSnapshot(a,s,{timeoutMs:o});return jt({toolId:a,deviceId:s,...l?{stale:l}:{},snapshot:c})}catch(a){return Lt(ya(a))}});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:te.string().optional().describe('The tool id, e.g. "perf-monitor".'),tool:te.string().optional().describe("Same as toolId."),action:te.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:te.union([te.record(te.unknown()),te.string()]).optional().describe("JSON params object for the action (shape depends on the action)."),args:te.union([te.record(te.unknown()),te.string()]).optional().describe("Same as params."),deviceId:te.string().optional().describe('The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out. Or "All" to broadcast.'),timeoutMs:te.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,tool:i,action:r,params:o,args:a,deviceId:s,timeoutMs:c})=>{try{const l=t??i;if(!l)return Lt("call_action needs toolId (see list_tool_actions).");const d=sh(o??a);const u=n.requireProDevice(s);const h=await n.callAction(l,r,d,u,{timeoutMs:c});return jt({toolId:l,action:r,ok:true,result:h??null})}catch(l){return Lt(ya(l))}})}function sh(e){if(e===void 0||typeof e==="object")return e;const n=e.trim();if(!n)return void 0;let t;try{t=JSON.parse(n)}catch{throw new Error('params is a string but not JSON. Pass an object, e.g. {"user": {...}}.')}if(!t||typeof t!=="object"||Array.isArray(t))throw new Error("params must be a JSON object.");return t}function ya(e){return e instanceof Error?e.message:String(e)}var qt=(e,n,t)=>Math.max(n,Math.min(t,e));var wr="__batch_warmup__";var ch={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var Ut={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function zt(e,n,t){return e??n??t}function Xn(e,n={}){const t=[];const i=(e.targetRoute||"").trim();if(!i)throw new Error("targetRoute is required (the screen to benchmark).");let r=(e.bounceRoute||"").trim();if(!r)r=i==="/"?"/_buoy_bounce":"/";if(r===i){throw new Error(`bounceRoute ("${r}") 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((u,h)=>{const b=(u.name||"").trim()||`case ${h+1}`;return{id:`case-${h}`,name:b,params:u.params??{},route:u.route}});const a=qt(Math.round(zt(e.runsPerCase,n.runsPerCase,Ut.runsPerCase)),1,10);const s=qt(Math.round(zt(e.coolDownMs,n.coolDownMs,Ut.coolDownMs)),0,3e4);const c=qt(Math.round(zt(e.discardWarmupRuns,n.discardWarmupRuns,Ut.discardWarmupRuns)),0,a-1);const l=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&l===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 d={targetRoute:i,bounceRoute:r,perCaseDurationMs:qt(Math.round(zt(e.perCaseDurationMs,n.perCaseDurationMs,Ut.perCaseDurationMs)),1e3,6e4),settleMs:qt(Math.round(zt(e.settleMs,n.settleMs,Ut.settleMs)),0,1e4),navTimeoutMs:qt(Math.round(zt(e.navTimeoutMs,n.navTimeoutMs,Ut.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:l,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:d,warnings:t}}function $a(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const i=gr(e);const r=i/6e4;const o=r>=1?`~${r.toFixed(1)} min`:`~${Math.round(i/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 s=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${n(s)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function gr(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let i=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)i+=e.cases.length*3e3;return i}function lh(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 dh(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.jsFps!=null&&t&&e.jsFps<t*.95){n.push("Low main-thread frame rate")}else if(e.longFrames!=null&&e.frames>0&&e.longFrames/e.frames>=.02){n.push("Frequent long frames")}const i=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(i>=100)n.push(`Freeze of ${Math.round(i)} ms`);if(e.longTaskPct!=null&&e.longTaskPct>=20)n.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50){n.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`)}if(e.styleLayoutMs!=null&&e.styleLayoutMs>=100){n.push(`Style and layout in slow frames (${Math.round(e.styleLayoutMs)} ms)`)}if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){n.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}if(e.layoutShift!=null&&e.layoutShift>=.1)n.push(`Layout shifts (${e.layoutShift})`);if(e.domGrowth!=null&&e.domGrowth>0){n.push(`DOM grew every run (+${e.domGrowth} nodes): check for a leak`)}return n}function uh(e){const n=e.filter(i=>!i.batchFailureReason&&!i.discardedWarmup).sort((i,r)=>(i.runIndex??0)-(r.runIndex??0)).map(i=>i.webRun?.domNodesStart).filter(i=>typeof i==="number");if(n.length<3)return null;for(let i=1;i<n.length;i++)if(n[i]<=n[i-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function hh(e,n){const t=e.find(r=>r.isBaseline);if(t)return t;const i=n?.[0]?.name;if(i){const r=e.filter(o=>o.caseName===i);if(r.length===1)return r[0]}return e[0]}function Zn(e,n,t,i=[]){const r=e.filter(m=>m.batchId===n);const o=new Map(i.map(m=>[m.index,m.reason]));const a=Array.from(new Set(r.map(m=>m.batchIndex??0))).sort((m,p)=>m-p);const s=m=>{const p=r.filter($=>($.batchIndex??0)===m);if(p.length===0)return void 0;const y=p.filter($=>!$.batchFailureReason&&!$.discardedWarmup);const v=y.filter($=>$.webRun?.fps!=null);if(v.length>0&&v.length===y.length){const $=[...v].sort((T,k)=>T.webRun.fps-k.webRun.fps);return $[Math.floor(($.length-1)/2)]}if(y.length>0){return y.find($=>$.isMedianRun===true)??y.find($=>$.runIndex===0||$.runIndex===void 0)??y[0]}return p.find($=>$.batchFailureReason)??p[0]};const c=m=>typeof m==="number"?m:null;const l=a.map(m=>{const p=s(m);const y=r.filter(P=>(P.batchIndex??0)===m);const v=y.length;const $=y.filter(P=>!P.batchFailureReason&&!P.discardedWarmup);const T=P=>P.webRun?P.webRun.fps??P.jsFpsAvg:P.uiFpsAvg;const k=$.map(T).filter(P=>typeof P==="number");const D=p?.name??t?.[m]?.name??`case ${m+1}`;const C=r.some(P=>(P.batchIndex??0)===m&&P.isBaseline===true);const I=o.get(m)??p?.batchFailureReason;const R=y.every(P=>P.batchFailureReason||P.discardedWarmup);const _=Boolean(I)||!p||R;const S={batchIndex:m,caseName:D,params:p?.params??{},runs:v,failed:_,...p?.route?{route:p.route}:{},...C?{isBaseline:true}:{},failureReason:_?I??(R&&p?"only warmup runs were saved":"no run recorded"):void 0,uiFps:c(p?.uiFpsAvg),jsFps:c(p?.jsFpsAvg),cpu:c(p?.cpuAvg),memMb:c(p?.memMaxMb),jsJank:c(p?.jsJankFrames),uiJank:c(p?.uiJankFrames),deviceMaxRefreshRate:c(p?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:$.length,fpsMin:k.length?Ee(Math.min(...k),1):null,fpsMax:k.length?Ee(Math.max(...k),1):null,...p?.thermalState?{thermalState:p.thermalState}:{},renderCommits:c(p?.renderCommits),renderWasted:p?.renderWasted,topRenderers:p?.topRenderers??[]};const A=p?.webRun;if(!A)return{...S,flags:_?[]:lh(S)};const O=[...A.longTasksSupported?[]:["long tasks"],...A.heapSupported?[]:["JS heap"],...A.loaf===null?["long animation frames"]:[]];const F={...S,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:A.heapSupported?S.memMb:null,deviceMaxRefreshRate:Math.max(S.deviceMaxRefreshRate??0,A.refreshHz??0)||null,longTaskPct:A.longTaskPct,frameGapP95Ms:A.frameGapP95Ms,frameGapMaxMs:A.frameGapMaxMs,edgeSilenceMs:A.edgeSilenceMs??null,jsFps:A.fps??S.jsFps,longFrames:A.longFrames,...O.length?{missingMetrics:O}:{},loafBlockingMs:A.loaf?.blockingMs??null,styleLayoutMs:A.loaf?.styleLayoutMs??null,forcedLayoutMs:A.loaf?.forcedLayoutMs??null,topScripts:A.loaf?.topScripts??[],worstInteractionMs:A.interactions?.worstMs??null,layoutShift:A.layoutShift??null,domNodes:A.domNodesEnd??null,domGrowth:uh(y),pageLoad:A.pageLoad??null,refreshCalibrated:A.refreshCalibrated??null};return{...F,flags:_?[]:dh({...F,frames:A.frames})}}).filter(m=>m.caseName!==wr).map((m,p)=>({...m,batchIndex:p}));const d=hh(l,t);if(d){d.isBaseline=true;for(const m of l){if(m===d)continue;if(m.uiFps!=null&&d.uiFps!=null)m.deltaUiFps=Ee(m.uiFps-d.uiFps,1);if(m.jsFps!=null&&d.jsFps!=null)m.deltaJsFps=Ee(m.jsFps-d.jsFps,1)}}const u=l.filter(m=>!m.failed);const h=(m,p)=>{let y;let v=NaN;for(const $ of u){const T=m($);if(T==null)continue;if(y===void 0||p(T,v)){y=$;v=T}}return y?.caseName};const b=m=>m.platform==="web"?m.longFrames??null:m.jsJank==null&&m.uiJank==null?null:(m.jsJank??0)+(m.uiJank??0);const f={uiFps:h(m=>m.uiFps,(m,p)=>m>p),jsFps:h(m=>m.jsFps,(m,p)=>m>p),lowestCpu:h(m=>m.cpu,(m,p)=>m<p),lowestMem:h(m=>m.memMb,(m,p)=>m<p),fewestJank:h(b,(m,p)=>m<p),fewestLongTasks:h(m=>m.longTaskPct??null,(m,p)=>m<p)};const w=u.filter(m=>m.flags.length>0).map(m=>({caseName:m.caseName,flags:m.flags}));const g=u.filter(m=>m.flags.length===0).sort((m,p)=>(p.uiFps??p.jsFps??0)-(m.uiFps??m.jsFps??0)||(m.cpu??m.longTaskPct??Infinity)-(p.cpu??p.longTaskPct??Infinity)||(m.memMb??Infinity)-(p.memMb??Infinity)).map(m=>m.caseName);return{batchId:n,baselineName:d?.caseName??null,deviceMaxRefreshRate:u.find(m=>m.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:l,leaders:f,flagged:w,balanced:g}}function ph(e){const n=e.filter(a=>!a.failed);if(n.length===0)return[];const t=n.map(a=>a.pageLoad?.navigationId);if(t.some(a=>a==null)||new Set(t).size!==t.length)return[];if(n.some(a=>(a.pageLoad?.pageAgeMs??Infinity)>3e4))return[];const i=a=>(a??"").replace(/\/+$/,"")||"/";const r=a=>{const s=a.pageLoad;if(!s?.loadPath||i(s.loadPath)!==i(a.route))return false;const c=new URLSearchParams(s.loadSearch??"");const l=[...c.keys()];if(new Set(l).size!==l.length)return false;const d=Object.entries(a.params);return l.length===d.length&&d.every(([u,h])=>c.get(u)===h)};if(n.some(a=>!r(a)))return[];const o=a=>a==null?"\u2014":String(Math.round(a));return[`**Page load** (each case started on a fresh page load):`,"",`| Case | TTFB ms | FCP ms | LCP ms | DOM ready ms | Script KB | Total KB |`,`|------|--------:|-------:|-------:|-------------:|----------:|---------:|`,...n.map(a=>{const s=a.pageLoad;return`| ${a.caseName} | ${o(s.ttfbMs)} | ${o(s.fcpMs)} | ${o(s.lcpMs)} | ${o(s.domContentLoadedMs)} | ${o(s.scriptKb)} | ${o(s.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function ka(e){const n=o=>o.platform==="web"?o.jsFps:o.uiFps;const t=e.filter(o=>!o.failed&&n(o)!=null&&o.fpsMin!=null&&o.fpsMax!=null).sort((o,a)=>(n(a)??0)-(n(o)??0));if(t.length<2)return null;const[i,r]=t;if((i.validRuns??0)<2||(r.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${i.caseName} and ${r.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((r.fpsMax??0)>=(i.fpsMin??0)||(n(i)??0)-(n(r)??0)<=2){return`**Too close to call:** ${i.caseName} (${i.fpsMin}\u2013${i.fpsMax} FPS) and ${r.caseName} (${r.fpsMin}\u2013${r.fpsMax} FPS) overlap or sit within 2 FPS. No clear winner between them; more runs may separate them.`}return null}function hn(e){const n=(h,b=1)=>h==null?"\u2014":h.toFixed(b);const t=h=>h==null?"":h>0?` (+${h.toFixed(1)})`:` (${h.toFixed(1)})`;const i=[];i.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(h=>h.platform==="web")){mh(e,i);return i.join("\n")}i.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.`);i.push("");const r=e.rows.some(h=>h.thermalState);const o=r?" Thermal |":"";const a=r?"-------|":"";i.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);i.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const h of e.rows){const b=r?` ${ch[h.thermalState??"unknown"]??"\u2014"} |`:"";if(h.failed){i.push(`| ${h.batchIndex} | ${h.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${b} \u274C FAILED (${h.failureReason}) |`);continue}const f=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"no flags";i.push(`| ${h.batchIndex} | ${h.caseName} | ${n(h.uiFps)}${t(h.deltaUiFps)} | ${n(h.jsFps)}${t(h.deltaJsFps)} | ${n(h.cpu)} | ${n(h.memMb)} | ${n(h.jsJank,0)} | ${n(h.uiJank,0)} |${b} ${f} |`)}const s=ka(e.rows);if(s){i.push("");i.push(s)}const c=e.rows.filter(h=>h.thermalState==="serious"||h.thermalState==="critical");if(c.length){i.push("");i.push(`**\u{1F525} Thermally throttled: ${c.map(h=>h.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.`)}i.push("");const l=e.leaders;const d=[l.uiFps&&`UI FPS \u2192 **${l.uiFps}**`,l.jsFps&&`JS FPS \u2192 **${l.jsFps}**`,l.lowestCpu&&`lowest CPU \u2192 **${l.lowestCpu}**`,l.lowestMem&&`lowest mem \u2192 **${l.lowestMem}**`,l.fewestJank&&`fewest jank \u2192 **${l.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)i.push(`**Per-metric best:** ${d}`);i.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){i.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(h=>`${h.caseName} (${h.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(h=>!h.failed&&(h.renderCommits!=null||h.topRenderers.length>0)).map(h=>{const b=h.topRenderers.map(g=>`${g.name} \xD7${g.renders} (${g.totalMs}ms${g.cause?`, ${g.cause}`:""})`).join(", ");const f=h.renderCommits!=null?`${h.renderCommits} commits`:"";const w=h.renderWasted!=null?`${h.renderWasted} wasted`:"";return`- **${h.caseName}** \u2014 ${[f,w,b].filter(Boolean).join(": ")}`});if(u.length){i.push("");i.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.");i.push(...u)}i.push("");i.push(`_Deltas vs baseline (\`${e.baselineName??"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 i.join("\n")}function br(e,n){const t=(i,r)=>({a:{avg:Ee(i.avg,1),p95:Ee(i.p95,1)},b:{avg:Ee(r.avg,1),p95:Ee(r.p95,1)},deltaAvg:Ee(r.avg-i.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},..._a(e.renders,n.renders)}}function fh(e,n){const t=(a,s,c)=>({a:a??null,b:s??null,delta:a!=null&&s!=null?Ee(s-a,1):null,better:c});const i=a=>a.webRun.heapSupported?Ee(a.stats.memoryUsage.max,1):null;const r=e.webRun;const o=n.webRun;return{a:e.name,b:n.name,platform:"web",note:"Browser runs: FPS is the main thread's whole-run requestAnimationFrame rate; long tasks % is not CPU. null = this browser couldn't measure it.",fps:t(r.fps??e.stats.jsFps.avg,o.fps??n.stats.jsFps.avg,"higher"),frameGapP95Ms:t(r.frameGapP95Ms,o.frameGapP95Ms,"lower"),frameGapMaxMs:t(r.frameGapMaxMs,o.frameGapMaxMs,"lower"),longFrames:t(r.longFrames,o.longFrames,"lower"),longTaskPct:t(r.longTaskPct,o.longTaskPct,"lower"),slowFrameBlockingMs:t(r.loaf?.blockingMs,o.loaf?.blockingMs,"lower"),styleLayoutPaintMs:t(r.loaf?.styleLayoutMs,o.loaf?.styleLayoutMs,"lower"),forcedLayoutMs:t(r.loaf?.forcedLayoutMs,o.loaf?.forcedLayoutMs,"lower"),worstInteractionMs:t(r.interactions?.worstMs,o.interactions?.worstMs,"lower"),layoutShift:t(r.layoutShift,o.layoutShift,"lower"),domNodes:t(r.domNodesEnd,o.domNodesEnd,"lower"),jsHeapMb:t(i(e),i(n),"lower"),topScripts:{a:r.loaf?.topScripts??[],b:o.loaf?.topScripts??[]},..._a(e.renders,n.renders)}}function _a(e,n){if(!e&&!n)return null;const t=i=>i??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 Ee(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function mh(e,n){const t=(d,u=1)=>d==null?"\u2014":d.toFixed(u);const i=d=>d==null?"":d>0?` (+${d.toFixed(1)})`:` (${d.toFixed(1)})`;n.push(`**How to read this.** This is a browser run. FPS is the main thread's frame rate (requestAnimationFrame); it can't see frames dropped later by the GPU or compositor. Long tasks % is time the main thread was blocked by tasks over 50 ms, not CPU use. For Chrome's own dropped-frame count, use run_web_benchmark.`);n.push("");n.push(`| # | Case | FPS | p95 frame gap ms | Worst gap ms | Long frames | Long tasks % | JS heap MB | Flags |`);n.push(`|---|------|----:|-----------------:|-------------:|------------:|-------------:|--------:|-------|`);for(const d of e.rows){const u=d.isBaseline?`${d.caseName} (baseline)`:d.caseName;if(d.failed){n.push(`| ${d.batchIndex} | ${u} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${d.failureReason}) |`);continue}const h=d.flags.length?`\u26A0\uFE0F ${d.flags.join("; ")}`:"no flags";n.push(`| ${d.batchIndex} | ${u} | ${t(d.jsFps)}${i(d.deltaJsFps)} | ${t(d.frameGapP95Ms)} | ${t(d.frameGapMaxMs)} | ${t(d.longFrames,0)} | ${t(d.longTaskPct)} | ${t(d.memMb)} | ${h} |`)}const r=e.rows.filter(d=>!d.failed).map(d=>{const u=[d.loafBlockingMs!=null&&d.loafBlockingMs>0&&`slow frames blocked input ${d.loafBlockingMs} ms`,d.styleLayoutMs!=null&&d.styleLayoutMs>0&&`style + layout ${d.styleLayoutMs} ms`,d.forcedLayoutMs!=null&&d.forcedLayoutMs>0&&`forced layout ${d.forcedLayoutMs} ms`,d.worstInteractionMs!=null&&`slowest input ${d.worstInteractionMs} ms`,d.layoutShift!=null&&d.layoutShift>0&&`layout shift ${d.layoutShift}`,d.domNodes!=null&&`${d.domNodes} DOM nodes${d.domGrowth?` (+${d.domGrowth} over the runs)`:""}`,d.topScripts&&d.topScripts.length>0&&`top scripts: ${d.topScripts.slice(0,3).map(h=>`${h.name} ${h.ms} ms`).join(", ")}`].filter(Boolean);return u.length?`- **${d.caseName}**: ${u.join(" \xB7 ")}`:null}).filter(Boolean);if(r.length){n.push("");n.push(`**Why frames were slow** (Long Animation Frames, Event Timing, DOM size; slow frames that ran past a run's end are left out):`);n.push(...r)}const o=ph(e.rows);if(o.length){n.push("");n.push(...o)}const a=ka(e.rows);if(a){n.push("");n.push(a)}const s=Array.from(new Set(e.rows.flatMap(d=>d.missingMetrics??[])));if(s.length){n.push("");n.push(`**Not measured in this browser:** ${s.join(", ")}. These show as \u2014 and can't win or lose.`)}if(e.rows.some(d=>d.refreshCalibrated===false)){n.push("");n.push(`**Refresh rate not checked on an idle screen:** the batch couldn't reach its bounce screen first, so the screen rate (and frame budgets) may read low.`)}if(e.deviceMaxRefreshRate){n.push("");n.push(`Screen rate: ${e.deviceMaxRefreshRate} Hz. A frame gap over ${(1500/e.deviceMaxRefreshRate).toFixed(1)} ms missed at least one frame.`)}n.push("");const c=e.leaders;const l=[c.jsFps&&`FPS \u2192 **${c.jsFps}**`,c.fewestJank&&`fewest long frames \u2192 **${c.fewestJank}**`,c.fewestLongTasks&&`fewest long tasks \u2192 **${c.fewestLongTasks}**`,c.lowestMem&&`lowest heap \u2192 **${c.lowestMem}**`].filter(Boolean).join(" \xB7 ");if(l)n.push(`**Per-metric leaders:** ${l}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(d=>`${d.caseName} (${d.flags.join("; ")})`).join(" \xB7 ")}`)}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.`);n.push("");n.push(`_Deltas vs baseline (\`${e.baselineName??"case #0"}\`). Confirm finalists on a real phone browser: desktop numbers don't predict mobile GPU, memory or heat._`)}import{z as G}from"zod";import{z as wh}from"zod";var M=e=>({content:[{type:"text",text:e}]});var x=e=>({isError:true,content:[{type:"text",text:e}]});var j=e=>e instanceof Error?e.message:String(e);var L=wh.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.");var Ge=()=>process.env.BUOY_LOAD_CORE_TOOLS==="0"?void 0:{"anthropic/alwaysLoad":true};function gh(e,n){return typeof e==="number"&&e>0?e:n}function ct(e){return e?`${e}
5
5
 
6
- `:""}function ma(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 zn(e,n){return async t=>{try{const i=e.requireProDevice(t.deviceId);return await n(t,i)}catch(i){return x(j(i))}}}function th(e,n,t){const i=n instanceof Error?n.message:String(n);if(!/is not connected right now/.test(i))return void 0;const r=/"([^"]+)"/.exec(i)?.[1];if(!r||!e.deviceWasAllowed(r))return void 0;const o=e.lastSync(r,t);if(!o)return void 0;const a=o.ageMs==null?"earlier":`${Math.round(o.ageMs/1e3)}s ago`;return{deviceId:r,payload:o.payload,stale:`\u26A0\uFE0F "${r}" is not connected, so this is the last data it sent (${a}). If it dropped right after an action, the app likely hit a fatal error (a red error screen): look for it below, then reload_app to recover.`}}var oe="perf-monitor";var sn=1500;var cn=e=>new Promise(n=>setTimeout(n,e));var wa=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var nh=G.object({name:G.string().describe("Display name for this variant (becomes the saved run's name)."),params:G.record(G.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:G.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function ur(e,n){try{const t=await e.callAction(oe,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:i,settleMs:r,navTimeoutMs:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,shuffleCases:l,reloadBetweenCases:d,captureRenders:u}=t;return{perCaseDurationMs:i,settleMs:r,navTimeoutMs:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,shuffleCases:l,reloadBetweenCases:d,captureRenders:u}}catch{return{}}}async function Je(e,n){const{payload:t}=await e.getSnapshot(oe,n,{timeoutMs:4e3});return t}function hr(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function ga(e,n){return(e??[]).filter(t=>t.batchId===n).length}function ih(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 mt(e,n){const t=new Map;for(const i of e??[]){if(i.batchId===n&&i.batchFailureReason&&!t.has(i.batchIndex??0)){t.set(i.batchIndex??0,i.batchFailureReason)}}return[...t.entries()].map(([i,r])=>({index:i,reason:r}))}function rh(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}var oh=1e4;function ah(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function ba(e,n,t,i=oh){const r=Date.now();while(Date.now()-r<i){const o=await Je(e,n).catch(()=>null);if(o&&!ah(o.live?.automation?.phase))return true;await cn(sn)}t(`automation still busy after ${Math.round(i/1e3)}s`);return false}async function sh(e,n,t){const{preBatchIds:i,expectedRuns:r,maxWaitMs:o,log:a}=t;const s=t.reloadsBetweenCases?10:3;const c=Date.now();let l="";let d=false;let u=0;while(Date.now()-c<o){let f;try{f=await Je(e,n)}catch{await cn(sn);continue}const w=f.index??[];const g=f.live?.automation?.phase??"idle";const m=f.live?.automationCompleted;if(m?.phase==="cancelled"&&!i.has(m.batchId)){return{batchId:m.batchId,failures:[],cancelled:true}}if(m?.phase==="done"&&!i.has(m.batchId)){return{batchId:m.batchId,failures:m.failures??mt(w,m.batchId)}}if(g!=="idle")d=true;if(g!==l){l=g;a(`automation phase: ${g}`)}const p=[...hr(w)].find(y=>!i.has(y));if(p){const y=ga(w,p);if(r>0&&y>=r){await ba(e,n,a);a(`batch ${p} complete (${y}/${r} runs saved)`);return{batchId:p,failures:mt(w,p)}}const v=(w??[]).filter(D=>D.batchId===p&&D.name!==cr).length;const $=f.live?.pendingBatchId===p;const T=Boolean(f.live&&"pendingBatchId"in f.live);const k=!t.reloadsBetweenCases||T;if(g==="idle"&&d&&v>0&&!$&&k){if(++u>=s){a(`batch ${p} settled (${y} runs, automation idle)`);return{batchId:p,failures:mt(w,p)}}}else{u=0}}await cn(sn)}const h=await Je(e,n).catch(()=>null);const b=h?[...hr(h.index)].find(f=>!i.has(f)):void 0;if(b&&h)return{batchId:b,failures:mt(h.index,b),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${l||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}async function ch(e,n,t,i){const r=n.filter(w=>w.batchId===t&&!w.discardedWarmup&&w.name!==cr&&w.durationMs);if(!r.length)return null;const o=r.map(w=>({id:w.id,from:w.createdAt-w.durationMs,to:w.createdAt}));const a=await Lc(e,o);if(!a)return null;if(!a.checked)return{line:`**Fair check:** not done. ${a.why}`,detail:a};const s=new Map(r.map(w=>[w.id,w]));const c=w=>i.find(g=>g.id===w?.caseId)?.name??w?.name??"?";const l=a.runs.filter(w=>!w.fair).map(w=>{const g=s.get(w.id);return{case:c(g),run:(g?.runIndex??0)+1,median:g?.isMedianRun===true,reason:w.reason}});const d=[...new Set(l.map(w=>w.case))];const u=new Map;for(const w of l){const g=w.reason??"No reason given.";u.set(g,[...u.get(g)??[],`${w.case} run ${w.run}`])}const h=[...u].map(([w,g])=>`${w} (${g.join(", ")})`).join(" ");const b=l.length>0&&l.every(w=>/perf-slot/.test(w.reason??""));const f=!l.length?`**Fair check:** all ${a.runs.length} runs were fair. The Mac stayed calm.`:`**Fair check:** ${l.length} of ${a.runs.length} runs were not fair. ${h} Don't rank on these. `+(b?`Take perf-slot with sim_farm_lock, hold it for the whole batch, then run the batch again.`:`Run these cases again with the baseline once the Mac is calm: ${d.join(", ")}.`);return{line:f,detail:{fair:a.fair,unfair:a.unfair,rules:a.rules,unfairRuns:l,redoCases:d,runs:a.runs}}}function va(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:G.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:G.array(nh).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:G.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:G.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:G.number().optional().describe("How long to record each run. Default 5000."),settleMs:G.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:G.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:G.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:G.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:G.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:G.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:G.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:G.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out."),maxWaitMs:G.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const i=r=>process.stderr.write(`[buoy-mcp] ${r}
7
- `);try{const r=n.requireProDevice(t.deviceId);const o=await ur(n,r);const{config:a,warnings:s}=qn(t,o);const c=lr(a);const l=t.maxWaitMs??c*3+6e4;const d=rh(a);if(!await ba(n,r,i)){return x("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(oe,"acknowledgeAutomation",{},r).catch(()=>{});await n.callAction(oe,"setEnabled",{enabled:true},r).catch(()=>{});await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const u=await Je(n,r);const h=hr(u.index);i(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${r}`);await n.callAction(oe,"startAutomation",{config:a},r);const b=await sh(n,r,{preBatchIds:h,expectedRuns:d,maxWaitMs:l,log:i,reloadsBetweenCases:a.reloadBetweenCases===true});if(b.cancelled){await n.callAction(oe,"acknowledgeAutomation",{},r).catch(()=>{});return x(`Batch ${b.batchId} was cancelled before completing.`)}await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const f=await Je(n,r);const w=Un(f.index??[],b.batchId,a.cases,b.failures);const{ranking:g,locked:m}=n.fullResultsFor(r)?{ranking:w,locked:En()}:Ai(w);await n.callAction(oe,"acknowledgeAutomation",{},r).catch(()=>{});await n.callAction(oe,"setEnabled",{enabled:false},r).catch(()=>{});if(g.rows.length===0){const $=n.connectedDevices().some(D=>D.deviceId===r);const T=f.live?.pendingBatchId===b.batchId;const k=!$?`The device disconnected during the batch (a browser tab that closes, reloads or sleeps drops off). Reconnect it and run again.`:T?`The batch reloaded the app between cases and never resumed. Check that <AutomationResumer/> is mounted (the web host mounts it), or pass reloadBetweenCases: false.`:b.timedOut?`No runs were saved before the wait cap (${Math.round(l/1e3)}s). On web, a hidden or background tab pauses the run: keep the tab in front. Check get_automation_status for the device's phase.`:`The batch finished without saving any runs. Check get_automation_status and the device console.`;return x(`Benchmark batch ${b.batchId||"(no id)"} saved no runs. ${k}`)}const p=await ch(r,f.index??[],b.batchId,a.cases);const y=p?.detail&&"unfairRuns"in p.detail&&p.detail.unfairRuns.length?`${p.line}
6
+ `:""}function Sa(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 Qn(e,n){return async t=>{try{const i=e.requireProDevice(t.deviceId);return await n(t,i)}catch(i){return x(j(i))}}}function bh(e,n,t){const i=n instanceof Error?n.message:String(n);if(!/is not connected right now/.test(i))return void 0;const r=/"([^"]+)"/.exec(i)?.[1];if(!r||!e.deviceWasAllowed(r))return void 0;const o=e.lastSync(r,t);if(!o)return void 0;const a=o.ageMs==null?"earlier":`${Math.round(o.ageMs/1e3)}s ago`;return{deviceId:r,payload:o.payload,stale:`\u26A0\uFE0F "${r}" is not connected, so this is the last data it sent (${a}). If it dropped right after an action, the app likely hit a fatal error (a red error screen): look for it below, then reload_app to recover.`}}var oe="perf-monitor";var pn=1500;var fn=e=>new Promise(n=>setTimeout(n,e));var xa=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var vh=G.object({name:G.string().describe("Display name for this variant (becomes the saved run's name)."),params:G.record(G.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:G.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function vr(e,n){try{const t=await e.callAction(oe,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:i,settleMs:r,navTimeoutMs:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,shuffleCases:l,reloadBetweenCases:d,captureRenders:u}=t;return{perCaseDurationMs:i,settleMs:r,navTimeoutMs:o,runsPerCase:a,coolDownMs:s,discardWarmupRuns:c,shuffleCases:l,reloadBetweenCases:d,captureRenders:u}}catch{return{}}}async function Ke(e,n){const{payload:t}=await e.getSnapshot(oe,n,{timeoutMs:4e3});return t}function yr(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function Ta(e,n){return(e??[]).filter(t=>t.batchId===n).length}function yh(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 bt(e,n){const t=new Map;for(const i of e??[]){if(i.batchId===n&&i.batchFailureReason&&!t.has(i.batchIndex??0)){t.set(i.batchIndex??0,i.batchFailureReason)}}return[...t.entries()].map(([i,r])=>({index:i,reason:r}))}function $h(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}var kh=1e4;function _h(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function Da(e,n,t,i=kh){const r=Date.now();while(Date.now()-r<i){const o=await Ke(e,n).catch(()=>null);if(o&&!_h(o.live?.automation?.phase))return true;await fn(pn)}t(`automation still busy after ${Math.round(i/1e3)}s`);return false}async function Sh(e,n,t){const{preBatchIds:i,expectedRuns:r,maxWaitMs:o,log:a}=t;const s=t.reloadsBetweenCases?10:3;const c=Date.now();let l="";let d=false;let u=0;while(Date.now()-c<o){let f;try{f=await Ke(e,n)}catch{await fn(pn);continue}const w=f.index??[];const g=f.live?.automation?.phase??"idle";const m=f.live?.automationCompleted;if(m?.phase==="cancelled"&&!i.has(m.batchId)){return{batchId:m.batchId,failures:[],cancelled:true}}if(m?.phase==="done"&&!i.has(m.batchId)){return{batchId:m.batchId,failures:m.failures??bt(w,m.batchId)}}if(g!=="idle")d=true;if(g!==l){l=g;a(`automation phase: ${g}`)}const p=[...yr(w)].find(y=>!i.has(y));if(p){const y=Ta(w,p);if(r>0&&y>=r){await Da(e,n,a);a(`batch ${p} complete (${y}/${r} runs saved)`);return{batchId:p,failures:bt(w,p)}}const v=(w??[]).filter(D=>D.batchId===p&&D.name!==wr).length;const $=f.live?.pendingBatchId===p;const T=Boolean(f.live&&"pendingBatchId"in f.live);const k=!t.reloadsBetweenCases||T;if(g==="idle"&&d&&v>0&&!$&&k){if(++u>=s){a(`batch ${p} settled (${y} runs, automation idle)`);return{batchId:p,failures:bt(w,p)}}}else{u=0}}await fn(pn)}const h=await Ke(e,n).catch(()=>null);const b=h?[...yr(h.index)].find(f=>!i.has(f)):void 0;if(b&&h)return{batchId:b,failures:bt(h.index,b),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${l||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}async function xh(e,n,t,i){const r=n.filter(w=>w.batchId===t&&!w.discardedWarmup&&w.name!==wr&&w.durationMs);if(!r.length)return null;const o=r.map(w=>({id:w.id,from:w.createdAt-w.durationMs,to:w.createdAt}));const a=await Xc(e,o);if(!a)return null;if(!a.checked)return{line:`**Fair check:** not done. ${a.why}`,detail:a};const s=new Map(r.map(w=>[w.id,w]));const c=w=>i.find(g=>g.id===w?.caseId)?.name??w?.name??"?";const l=a.runs.filter(w=>!w.fair).map(w=>{const g=s.get(w.id);return{case:c(g),run:(g?.runIndex??0)+1,median:g?.isMedianRun===true,reason:w.reason}});const d=[...new Set(l.map(w=>w.case))];const u=new Map;for(const w of l){const g=w.reason??"No reason given.";u.set(g,[...u.get(g)??[],`${w.case} run ${w.run}`])}const h=[...u].map(([w,g])=>`${w} (${g.join(", ")})`).join(" ");const b=l.length>0&&l.every(w=>/perf-slot/.test(w.reason??""));const f=!l.length?`**Fair check:** all ${a.runs.length} runs were fair. The Mac stayed calm.`:`**Fair check:** ${l.length} of ${a.runs.length} runs were not fair. ${h} Don't rank on these. `+(b?`Take perf-slot with sim_farm_lock, hold it for the whole batch, then run the batch again.`:`Run these cases again with the baseline once the Mac is calm: ${d.join(", ")}.`);return{line:f,detail:{fair:a.fair,unfair:a.unfair,rules:a.rules,unfairRuns:l,redoCases:d,runs:a.runs}}}function Ia(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:G.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:G.array(vh).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:G.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:G.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:G.number().optional().describe("How long to record each run. Default 5000."),settleMs:G.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:G.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:G.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:G.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:G.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:G.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:G.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:G.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out."),maxWaitMs:G.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const i=r=>process.stderr.write(`[buoy-mcp] ${r}
7
+ `);try{const r=n.requireProDevice(t.deviceId);const o=await vr(n,r);const{config:a,warnings:s}=Xn(t,o);const c=gr(a);const l=t.maxWaitMs??c*3+6e4;const d=$h(a);if(!await Da(n,r,i)){return x("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(oe,"acknowledgeAutomation",{},r).catch(()=>{});await n.callAction(oe,"setEnabled",{enabled:true},r).catch(()=>{});await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const u=await Ke(n,r);const h=yr(u.index);i(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${r}`);await n.callAction(oe,"startAutomation",{config:a},r);const b=await Sh(n,r,{preBatchIds:h,expectedRuns:d,maxWaitMs:l,log:i,reloadsBetweenCases:a.reloadBetweenCases===true});if(b.cancelled){await n.callAction(oe,"acknowledgeAutomation",{},r).catch(()=>{});return x(`Batch ${b.batchId} was cancelled before completing.`)}await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const f=await Ke(n,r);const w=Zn(f.index??[],b.batchId,a.cases,b.failures);const{ranking:g,locked:m}=n.fullResultsFor(r)?{ranking:w,locked:qn()}:Bi(w);await n.callAction(oe,"acknowledgeAutomation",{},r).catch(()=>{});await n.callAction(oe,"setEnabled",{enabled:false},r).catch(()=>{});if(g.rows.length===0){const $=n.connectedDevices().some(D=>D.deviceId===r);const T=f.live?.pendingBatchId===b.batchId;const k=!$?`The device disconnected during the batch (a browser tab that closes, reloads or sleeps drops off). Reconnect it and run again.`:T?`The batch reloaded the app between cases and never resumed. Check that <AutomationResumer/> is mounted (the web host mounts it), or pass reloadBetweenCases: false.`:b.timedOut?`No runs were saved before the wait cap (${Math.round(l/1e3)}s). On web, a hidden or background tab pauses the run: keep the tab in front. Check get_automation_status for the device's phase.`:`The batch finished without saving any runs. Check get_automation_status and the device console.`;return x(`Benchmark batch ${b.batchId||"(no id)"} saved no runs. ${k}`)}const p=await xh(r,f.index??[],b.batchId,a.cases);const y=p?.detail&&"unfairRuns"in p.detail&&p.detail.unfairRuns.length?`${p.line}
8
8
 
9
- ${an(g)}`:an(g)+(p?`
9
+ ${hn(g)}`:hn(g)+(p?`
10
10
 
11
- ${p.line}`:"");const v={batchId:b.batchId,leaders:g.leaders,balanced:g.balanced,flagged:g.flagged,rows:g.rows,failures:b.failures,warnings:s,estimateMs:c,...p?{fairness:p.detail}:{},...b.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return M(It(`${y}
11
+ ${p.line}`:"");const v={batchId:b.batchId,leaders:g.leaders,balanced:g.balanced,flagged:g.flagged,rows:g.rows,failures:b.failures,warnings:s,estimateMs:c,...p?{fairness:p.detail}:{},...b.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return M(Et(`${y}
12
12
 
13
13
  \`\`\`json
14
14
  ${JSON.stringify(v,null,2)}
15
- \`\`\``,m))}catch(r){return x(j(r))}});e.registerTool("get_automation_status",{description:"Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",inputSchema:{deviceId:G.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async({deviceId:t})=>{try{const i=n.requireProDevice(t);const r=await Je(n,i);return wa({deviceId:i,automation:r.live?.automation??{phase:"idle"},automationCompleted:r.live?.automationCompleted??null})}catch(i){return x(j(i))}});e.registerTool("wait_for_automation",{description:"Block until the in-flight perf-monitor batch finishes, then return its ranked comparison. Use this if a batch was started another way (e.g. on the device).",inputSchema:{deviceId:L,maxWaitMs:G.number().optional().describe("Max time to wait. Default 600000 (10 min).")}},async({deviceId:t,maxWaitMs:i})=>{const r=a=>process.stderr.write(`[buoy-mcp] ${a}
16
- `);const o=i??6e5;try{const a=n.requireProDevice(t);const s=Date.now();let c=false;let l=0;let d=-1;let u=null;while(Date.now()-s<o){let g;try{g=await Je(n,a)}catch{await cn(sn);continue}const m=g.live?.automationCompleted;if(m?.phase==="cancelled")return x(`Batch ${m.batchId} was cancelled.`);if(m?.phase==="done"){u={batchId:m.batchId,failures:m.failures??mt(g.index,m.batchId)};break}const p=g.live?.automation?.phase??"idle";if(p!=="idle")c=true;const y=ih(g.index);if(p==="idle"&&y){const v=ga(g.index,y);if(v===d){if(c&&++l>=3){u={batchId:y,failures:mt(g.index,y)};break}if(!c&&Date.now()-s>4e3){u={batchId:y,failures:mt(g.index,y)};break}}else{l=0;d=v}}else{l=0}await cn(sn)}if(!u)return x("No automation batch completed within the wait window.");await n.callAction(oe,"refreshIndex",{},a).catch(()=>{});const h=await Je(n,a);const b=Un(h.index??[],u.batchId,void 0,u.failures);if(b.rows.length===0){await n.callAction(oe,"acknowledgeAutomation",{},a).catch(()=>{});return x(`Batch ${u.batchId} saved no measured runs (only a warmup, or nothing). Check get_automation_status and run it again.`)}await n.callAction(oe,"acknowledgeAutomation",{},a).catch(()=>{});const{ranking:f,locked:w}=n.fullResultsFor(a)?{ranking:b,locked:En()}:Ai(b);return M(It(`${an(f)}
15
+ \`\`\``,m))}catch(r){return x(j(r))}});e.registerTool("get_automation_status",{description:"Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",inputSchema:{deviceId:G.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async({deviceId:t})=>{try{const i=n.requireProDevice(t);const r=await Ke(n,i);return xa({deviceId:i,automation:r.live?.automation??{phase:"idle"},automationCompleted:r.live?.automationCompleted??null})}catch(i){return x(j(i))}});e.registerTool("wait_for_automation",{description:"Block until the in-flight perf-monitor batch finishes, then return its ranked comparison. Use this if a batch was started another way (e.g. on the device).",inputSchema:{deviceId:L,maxWaitMs:G.number().optional().describe("Max time to wait. Default 600000 (10 min).")}},async({deviceId:t,maxWaitMs:i})=>{const r=a=>process.stderr.write(`[buoy-mcp] ${a}
16
+ `);const o=i??6e5;try{const a=n.requireProDevice(t);const s=Date.now();let c=false;let l=0;let d=-1;let u=null;while(Date.now()-s<o){let g;try{g=await Ke(n,a)}catch{await fn(pn);continue}const m=g.live?.automationCompleted;if(m?.phase==="cancelled")return x(`Batch ${m.batchId} was cancelled.`);if(m?.phase==="done"){u={batchId:m.batchId,failures:m.failures??bt(g.index,m.batchId)};break}const p=g.live?.automation?.phase??"idle";if(p!=="idle")c=true;const y=yh(g.index);if(p==="idle"&&y){const v=Ta(g.index,y);if(v===d){if(c&&++l>=3){u={batchId:y,failures:bt(g.index,y)};break}if(!c&&Date.now()-s>4e3){u={batchId:y,failures:bt(g.index,y)};break}}else{l=0;d=v}}else{l=0}await fn(pn)}if(!u)return x("No automation batch completed within the wait window.");await n.callAction(oe,"refreshIndex",{},a).catch(()=>{});const h=await Ke(n,a);const b=Zn(h.index??[],u.batchId,void 0,u.failures);if(b.rows.length===0){await n.callAction(oe,"acknowledgeAutomation",{},a).catch(()=>{});return x(`Batch ${u.batchId} saved no measured runs (only a warmup, or nothing). Check get_automation_status and run it again.`)}await n.callAction(oe,"acknowledgeAutomation",{},a).catch(()=>{});const{ranking:f,locked:w}=n.fullResultsFor(a)?{ranking:b,locked:qn()}:Bi(b);return M(Et(`${hn(f)}
17
17
 
18
18
  \`\`\`json
19
19
  ${JSON.stringify(f,null,2)}
20
- \`\`\``,w))}catch(a){return x(j(a))}});e.registerTool("get_batch_report",{description:"Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",inputSchema:{batchId:G.string().describe("The batch id to report on."),deviceId:L}},async({batchId:t,deviceId:i})=>{try{const r=n.requireProDevice(i);await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const o=await Je(n,r);const a=(o.index??[]).filter(d=>d.batchId===t);if(a.length===0)return x(`No saved runs found for batch "${t}".`);const s=Un(o.index??[],t);if(s.rows.length===0)return x(`Batch "${t}" has no measured runs, only a warmup.`);const{ranking:c,locked:l}=n.fullResultsFor(r)?{ranking:s,locked:En()}:Ai(s);return M(It(`${an(c)}
20
+ \`\`\``,w))}catch(a){return x(j(a))}});e.registerTool("get_batch_report",{description:"Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",inputSchema:{batchId:G.string().describe("The batch id to report on."),deviceId:L}},async({batchId:t,deviceId:i})=>{try{const r=n.requireProDevice(i);await n.callAction(oe,"refreshIndex",{},r).catch(()=>{});const o=await Ke(n,r);const a=(o.index??[]).filter(d=>d.batchId===t);if(a.length===0)return x(`No saved runs found for batch "${t}".`);const s=Zn(o.index??[],t);if(s.rows.length===0)return x(`Batch "${t}" has no measured runs, only a warmup.`);const{ranking:c,locked:l}=n.fullResultsFor(r)?{ranking:s,locked:qn()}:Bi(s);return M(Et(`${hn(c)}
21
21
 
22
22
  \`\`\`json
23
23
  ${JSON.stringify(c,null,2)}
24
- \`\`\``,l))}catch(r){return x(j(r))}});e.registerTool("compare_reports",{description:"Compare two saved benchmark runs head-to-head. Native runs: avg + p95 for FPS/CPU/memory, jank. Browser runs: FPS, frame gaps, long tasks, slow-frame scripts, input, DOM and heap (no CPU). Both: render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use comparable workloads and inspect available metrics together. A wasted-render delta alone does not prove a fix.",inputSchema:{reportIdA:G.string().describe("First (baseline) report id."),reportIdB:G.string().describe("Second report id to compare against the baseline."),deviceId:L}},async({reportIdA:t,reportIdB:i,deviceId:r})=>{try{const o=n.requireProDevice(r);const[a,s]=await Promise.all([n.callAction(oe,"loadReport",{id:t},o),n.callAction(oe,"loadReport",{id:i},o)]);if(!a)return x(`Report "${t}" not found.`);if(!s)return x(`Report "${i}" not found.`);const c=n.fullResultsFor(o);const l=h=>{if(c)return wa(h);const{comparison:b,locked:f}=el(h);return M(It(JSON.stringify(b,null,2),f))};const d=a.metadata.webRun;const u=s.metadata.webRun;if(d&&u){return l(Xu({name:a.metadata.name,stats:a.stats,webRun:d,renders:a.renders},{name:s.metadata.name,stats:s.stats,webRun:u,renders:s.renders}))}if(d||u){return l({...dr({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:s.metadata.name,stats:s.stats,renders:s.renders}),note:"One report has browser details and one doesn't (an older save, or a different platform). Only the stats both share are compared. On web, UI FPS and JS FPS are the same main-thread number and CPU is long-task time, not CPU. Re-run the older case to compare frame gaps, long tasks and input."})}return l(dr({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:s.metadata.name,stats:s.stats,renders:s.renders}))}catch(o){return x(j(o))}});e.registerTool("get_benchmark_settings",{description:"Read the device's saved benchmark settings (per-page duration, runs, cooldown, reload, shuffle) in plain English with a time estimate. Call this before run_benchmark_batch so you can tell the user what the run will do \u2014 run_benchmark_batch inherits these unless you override them.",inputSchema:{pages:G.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),deviceId:L}},async({pages:t,deviceId:i})=>{try{const r=n.requireProDevice(i);const o=await ur(n,r);const a=Math.max(1,Math.round(t??1));const{config:s}=qn({targetRoute:"/_preview",cases:Array.from({length:a},(l,d)=>({name:`page ${d+1}`}))},o);const c=Object.keys(o).length>0?"this device's saved settings":"default profile (none saved)";return M(`**Benchmark settings (${c}):** ${ha(s)}
24
+ \`\`\``,l))}catch(r){return x(j(r))}});e.registerTool("compare_reports",{description:"Compare two saved benchmark runs head-to-head. Native runs: avg + p95 for FPS/CPU/memory, jank. Browser runs: FPS, frame gaps, long tasks, slow-frame scripts, input, DOM and heap (no CPU). Both: render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use comparable workloads and inspect available metrics together. A wasted-render delta alone does not prove a fix.",inputSchema:{reportIdA:G.string().describe("First (baseline) report id."),reportIdB:G.string().describe("Second report id to compare against the baseline."),deviceId:L}},async({reportIdA:t,reportIdB:i,deviceId:r})=>{try{const o=n.requireProDevice(r);const[a,s]=await Promise.all([n.callAction(oe,"loadReport",{id:t},o),n.callAction(oe,"loadReport",{id:i},o)]);if(!a)return x(`Report "${t}" not found.`);if(!s)return x(`Report "${i}" not found.`);const c=n.fullResultsFor(o);const l=h=>{if(c)return xa(h);const{comparison:b,locked:f}=gl(h);return M(Et(JSON.stringify(b,null,2),f))};const d=a.metadata.webRun;const u=s.metadata.webRun;if(d&&u){return l(fh({name:a.metadata.name,stats:a.stats,webRun:d,renders:a.renders},{name:s.metadata.name,stats:s.stats,webRun:u,renders:s.renders}))}if(d||u){return l({...br({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:s.metadata.name,stats:s.stats,renders:s.renders}),note:"One report has browser details and one doesn't (an older save, or a different platform). Only the stats both share are compared. On web, UI FPS and JS FPS are the same main-thread number and CPU is long-task time, not CPU. Re-run the older case to compare frame gaps, long tasks and input."})}return l(br({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:s.metadata.name,stats:s.stats,renders:s.renders}))}catch(o){return x(j(o))}});e.registerTool("get_benchmark_settings",{description:"Read the device's saved benchmark settings (per-page duration, runs, cooldown, reload, shuffle) in plain English with a time estimate. Call this before run_benchmark_batch so you can tell the user what the run will do \u2014 run_benchmark_batch inherits these unless you override them.",inputSchema:{pages:G.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),deviceId:L}},async({pages:t,deviceId:i})=>{try{const r=n.requireProDevice(i);const o=await vr(n,r);const a=Math.max(1,Math.round(t??1));const{config:s}=Xn({targetRoute:"/_preview",cases:Array.from({length:a},(l,d)=>({name:`page ${d+1}`}))},o);const c=Object.keys(o).length>0?"this device's saved settings":"default profile (none saved)";return M(`**Benchmark settings (${c}):** ${$a(s)}
25
25
 
26
26
  \`\`\`json
27
27
  ${JSON.stringify(o,null,2)}
28
- \`\`\``)}catch(r){return x(j(r))}});e.registerTool("set_benchmark_settings",{description:`PERSIST benchmark settings on the device so they stick across runs. Only use when the user explicitly wants to save a change ("always", "save this"). For a one-off, pass overrides to run_benchmark_batch instead \u2014 don't change their saved profile.`,inputSchema:{perCaseDurationMs:G.number().optional().describe("Recording length per page (ms)."),settleMs:G.number().optional().describe("Idle after navigation before recording (ms)."),coolDownMs:G.number().optional().describe("Idle between runs/cases (ms)."),navTimeoutMs:G.number().optional().describe("Max wait for navigation per run (ms)."),runsPerCase:G.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),discardWarmupRuns:G.number().optional().describe("Discard first N runs of each page before the median."),shuffleCases:G.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),reloadBetweenCases:G.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),captureRenders:G.boolean().optional().describe("Capture per-component render counts/durations during each run."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(oe,"getAutomationConfig",{},i)??{};const o=["perCaseDurationMs","settleMs","coolDownMs","navTimeoutMs","runsPerCase","discardWarmupRuns","shuffleCases","reloadBetweenCases","captureRenders"];const a={};for(const u of o){const h=t[u];if(h!==void 0)a[u]=h}if(Object.keys(a).length===0){return x("No settings provided to save. Pass at least one field (e.g. perCaseDurationMs).")}const s={...r,...a};const c=await n.callAction(oe,"setAutomationConfig",{config:s},i);const l=await ur(n,i);const{config:d}=qn({targetRoute:"/_preview",cases:[{name:"page 1"}]},l);return M(`Saved. **New benchmark settings:** ${ha(d)}
28
+ \`\`\``)}catch(r){return x(j(r))}});e.registerTool("set_benchmark_settings",{description:`PERSIST benchmark settings on the device so they stick across runs. Only use when the user explicitly wants to save a change ("always", "save this"). For a one-off, pass overrides to run_benchmark_batch instead \u2014 don't change their saved profile.`,inputSchema:{perCaseDurationMs:G.number().optional().describe("Recording length per page (ms)."),settleMs:G.number().optional().describe("Idle after navigation before recording (ms)."),coolDownMs:G.number().optional().describe("Idle between runs/cases (ms)."),navTimeoutMs:G.number().optional().describe("Max wait for navigation per run (ms)."),runsPerCase:G.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),discardWarmupRuns:G.number().optional().describe("Discard first N runs of each page before the median."),shuffleCases:G.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),reloadBetweenCases:G.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),captureRenders:G.boolean().optional().describe("Capture per-component render counts/durations during each run."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(oe,"getAutomationConfig",{},i)??{};const o=["perCaseDurationMs","settleMs","coolDownMs","navTimeoutMs","runsPerCase","discardWarmupRuns","shuffleCases","reloadBetweenCases","captureRenders"];const a={};for(const u of o){const h=t[u];if(h!==void 0)a[u]=h}if(Object.keys(a).length===0){return x("No settings provided to save. Pass at least one field (e.g. perCaseDurationMs).")}const s={...r,...a};const c=await n.callAction(oe,"setAutomationConfig",{config:s},i);const l=await vr(n,i);const{config:d}=Xn({targetRoute:"/_preview",cases:[{name:"page 1"}]},l);return M(`Saved. **New benchmark settings:** ${$a(d)}
29
29
 
30
30
  \`\`\`json
31
31
  ${JSON.stringify(c??l,null,2)}
32
- \`\`\``)}catch(i){return x(j(i))}})}import{z as wt}from"zod";var lh="events";var dh={storage:["storage-async","storage-mmkv"],"react-query":["react-query","react-query-query","react-query-mutation"]};function uh(e){if(!e||e.length===0)return void 0;return[...new Set(e.flatMap(n=>dh[n]??[n]))]}function ya(e,n){e.registerTool("get_events",{description:"Read retained events from connected Buoy sources. Filter with sources, such as ['network'], ['redux'], ['react-query'], ['storage'] or ['route']. Returns compact summaries by default; use includeData:true for available payload details. Coverage depends on integrations and retention. An empty timeline does not prove an event never happened. Use get_network_requests for request IDs and get_snapshot for available current tool state.",inputSchema:{limit:wt.number().optional().describe("Most-recent N events to include. Default 25."),sources:wt.array(wt.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai", "capacitor" (Capacitor plugin calls). Omit for all.'),status:wt.enum(["all","errors","success","pending"]).optional().describe("Filter by status. Default all."),includeData:wt.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),format:wt.enum(["markdown","json","text","mermaid"]).optional().describe("Output format. Default markdown."),preset:wt.enum(["llm","bugReport","json","errors","minimal","mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r={};const o=uh(t.sources);if(o)r.filterSources=o;if(t.status)r.filterMode=t.status;if(t.includeData!==void 0)r.includeEventData=t.includeData;if(t.format)r.format=t.format==="text"?"plaintext":t.format;const a=await n.callAction(lh,"exportEvents",{preset:t.preset,settings:r,limit:t.limit??25},i);if(!a||typeof a.output!=="string"){return x("No events result from the device. The events tool may be inactive, or the app needs a newer Buoy (get_events requires the exportEvents action).")}const s=a.includedData?"":" \u2014 compact view; pass includeData:true, a specific source, or a bigger limit for full payloads";const c=`
32
+ \`\`\``)}catch(i){return x(j(i))}})}import{z as vt}from"zod";var Th="events";var Dh={storage:["storage-async","storage-mmkv"],"react-query":["react-query","react-query-query","react-query-mutation"]};function Ih(e){if(!e||e.length===0)return void 0;return[...new Set(e.flatMap(n=>Dh[n]??[n]))]}function Ma(e,n){e.registerTool("get_events",{description:"Read retained events from connected Buoy sources. Filter with sources, such as ['network'], ['redux'], ['react-query'], ['storage'] or ['route']. Returns compact summaries by default; use includeData:true for available payload details. Coverage depends on integrations and retention. An empty timeline does not prove an event never happened. Use get_network_requests for request IDs and get_snapshot for available current tool state.",inputSchema:{limit:vt.number().optional().describe("Most-recent N events to include. Default 25."),sources:vt.array(vt.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai", "capacitor" (Capacitor plugin calls). Omit for all.'),status:vt.enum(["all","errors","success","pending"]).optional().describe("Filter by status. Default all."),includeData:vt.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),format:vt.enum(["markdown","json","text","mermaid"]).optional().describe("Output format. Default markdown."),preset:vt.enum(["llm","bugReport","json","errors","minimal","mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r={};const o=Ih(t.sources);if(o)r.filterSources=o;if(t.status)r.filterMode=t.status;if(t.includeData!==void 0)r.includeEventData=t.includeData;if(t.format)r.format=t.format==="text"?"plaintext":t.format;const a=await n.callAction(Th,"exportEvents",{preset:t.preset,settings:r,limit:t.limit??25},i);if(!a||typeof a.output!=="string"){return x("No events result from the device. The events tool may be inactive, or the app needs a newer Buoy (get_events requires the exportEvents action).")}const s=a.includedData?"":" \u2014 compact view; pass includeData:true, a specific source, or a bigger limit for full payloads";const c=`
33
33
 
34
- _Showing ${a.returned} of ${a.totalAvailable} captured event(s)${s}._`;return M((a.output||"(no events captured)")+c)}catch(i){return x(j(i))}})}import{z as Ge}from"zod";var hh="query";var ph="react-query";async function Wn(e,n,t,i){try{return await e.callAction(hh,n,t,i)}catch(r){const o=j(r);if(!/unknown tool/i.test(o))throw r;return await e.callAction(ph,n,t,i)}}var fh={fresh:"\u{1F7E2}",stale:"\u{1F7E1}",fetching:"\u{1F504}",error:"\u{1F534}",inactive:"\u26AA\uFE0F",paused:"\u23F8\uFE0F",disabled:"\u{1F6AB}"};function mh(e){if(!e)return"\u2014";const n=Math.round((Date.now()-e)/1e3);if(n<60)return`${n}s ago`;if(n<3600)return`${Math.round(n/60)}m ago`;if(n<86400)return`${Math.round(n/3600)}h ago`;return`${Math.round(n/86400)}d ago`}function $a(e,n){e.registerTool("get_react_query",{description:"Read queries reported by the connected React Query integration, including available status, observer counts, timestamps and errors. includeData:true requests cached payloads; staleOnly:true filters stale queries. Use each returned hash with react_query_action. History depends on retained events.",inputSchema:{includeData:Ge.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),staleOnly:Ge.boolean().optional().describe("Only stale queries. Default false."),limit:Ge.number().optional().describe("Most-recently-updated N queries. Default 25."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await Wn(n,"listQueries",{includeData:t.includeData??false,staleOnly:t.staleOnly??false,limit:t.limit??25},i);if(!r||!Array.isArray(r.queries)){return x("No react-query result from the device. The react-query tool may be inactive, or the app needs a newer Buoy (get_react_query requires the listQueries action).")}if(r.queries.length===0){return M(`React Query cache is empty (0 of ${r.total} queries${t.staleOnly?" match staleOnly":""}).`)}const o=r.queries.map((l,d)=>{const u=fh[l.status]??"\u2022";const h=`${d+1}. ${u} ${l.status} \xB7 \`${l.queryHash}\` \xB7 ${l.observers} obs \xB7 ${mh(l.updatedAt)}`;const b=l.error?` \u2014 \u26A0\uFE0F ${l.error}`:"";const f=r.includedData?`
34
+ _Showing ${a.returned} of ${a.totalAvailable} captured event(s)${s}._`;return M((a.output||"(no events captured)")+c)}catch(i){return x(j(i))}})}import{z as Ve}from"zod";var Mh="query";var Ah="react-query";async function ei(e,n,t,i){try{return await e.callAction(Mh,n,t,i)}catch(r){const o=j(r);if(!/unknown tool/i.test(o))throw r;return await e.callAction(Ah,n,t,i)}}var Ch={fresh:"\u{1F7E2}",stale:"\u{1F7E1}",fetching:"\u{1F504}",error:"\u{1F534}",inactive:"\u26AA\uFE0F",paused:"\u23F8\uFE0F",disabled:"\u{1F6AB}"};function Eh(e){if(!e)return"\u2014";const n=Math.round((Date.now()-e)/1e3);if(n<60)return`${n}s ago`;if(n<3600)return`${Math.round(n/60)}m ago`;if(n<86400)return`${Math.round(n/3600)}h ago`;return`${Math.round(n/86400)}d ago`}function Aa(e,n){e.registerTool("get_react_query",{description:"Read queries reported by the connected React Query integration, including available status, observer counts, timestamps and errors. includeData:true requests cached payloads; staleOnly:true filters stale queries. Use each returned hash with react_query_action. History depends on retained events.",inputSchema:{includeData:Ve.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),staleOnly:Ve.boolean().optional().describe("Only stale queries. Default false."),limit:Ve.number().optional().describe("Most-recently-updated N queries. Default 25."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await ei(n,"listQueries",{includeData:t.includeData??false,staleOnly:t.staleOnly??false,limit:t.limit??25},i);if(!r||!Array.isArray(r.queries)){return x("No react-query result from the device. The react-query tool may be inactive, or the app needs a newer Buoy (get_react_query requires the listQueries action).")}if(r.queries.length===0){return M(`React Query cache is empty (0 of ${r.total} queries${t.staleOnly?" match staleOnly":""}).`)}const o=r.queries.map((l,d)=>{const u=Ch[l.status]??"\u2022";const h=`${d+1}. ${u} ${l.status} \xB7 \`${l.queryHash}\` \xB7 ${l.observers} obs \xB7 ${Eh(l.updatedAt)}`;const b=l.error?` \u2014 \u26A0\uFE0F ${l.error}`:"";const f=r.includedData?`
35
35
  data: ${JSON.stringify(l.data)}`:"";return h+b+f});const a=r.queries.filter(l=>l.isStale).length;const s=`**React Query cache \u2014 ${r.total} queries (showing ${r.returned}, ${a} stale):**`;const c=r.includedData?"":"\n\n_Compact view. Pass includeData:true for cached payloads, staleOnly:true to narrow. Act on one with react_query_action using the `hash` in backticks._";return M(`${s}
36
36
 
37
- ${o.join("\n")}${c}`)}catch(i){return x(j(i))}});e.registerTool("react_query_action",{description:"Mutate the app's React Query cache: invalidate / refetch / reset / remove a single query (by queryHash, from get_react_query), or clear the entire query cache. Use this to fix stale data, force a refetch, or reset state during debugging.",inputSchema:{action:Ge.enum(["invalidate","refetch","reset","remove","clear","list"]).describe("invalidate/refetch/reset/remove one query (by queryHash or queryKey); clear empties the whole cache; list shows the queries."),queryHash:Ge.string().optional().describe("The target query's hash (from get_react_query, shown in backticks)."),queryKey:Ge.union([Ge.array(Ge.unknown()),Ge.string()]).optional().describe('Or the query key, e.g. ["offers"]. Acts on every query whose key starts with it.'),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);if(t.action==="clear"){await Wn(n,"clearQueryCache",{},i);return M("Requested clearing of the connected React Query cache. Read it again to confirm.")}const r=t.action==="list"||!t.queryHash&&t.queryKey!==void 0;const o=r?await Wn(n,"listQueries",{includeData:false,staleOnly:false,limit:5e3},i):null;if(t.action==="list"){const c=o?.queries??[];return M(c.length?`**Queries (${c.length}):**
38
- ${c.map(l=>`- \`${l.queryHash}\` \xB7 ${l.status}`).join("\n")}`:"React Query cache is empty.")}let a=t.queryHash?[t.queryHash]:[];if(!a.length&&t.queryKey!==void 0){a=wh(t.queryKey,o?.queries??[]);if(!a.length){const c=o&&o.total>o.returned?` (searched ${o.returned} of ${o.total} queries)`:"";return x(`No query has a key starting with ${JSON.stringify(t.queryKey)}${c}. Call get_react_query to see the keys.`)}}if(!a.length){return x(`'${t.action}' needs a queryHash or queryKey. Call get_react_query first and use a row's hash (in backticks).`)}for(const c of a)await Wn(n,t.action,{queryHash:c},i);const s=o&&o.total>o.returned?` Note: only ${o.returned} of ${o.total} queries were searched, so some matches may have been missed.`:"";return M(`Requested ${t.action} on ${a.map(c=>`\`${c}\``).join(", ")}.${s}`)}catch(i){return x(j(i))}})}function wh(e,n){let t;if(Array.isArray(e))t=e;else{try{const i=JSON.parse(e);t=Array.isArray(i)?i:[i]}catch{t=[e]}}return n.filter(i=>{let r=i.queryKey;if(!Array.isArray(r)){try{r=JSON.parse(i.queryHash)}catch{return false}}return Array.isArray(r)&&t.length<=r.length&&t.every((o,a)=>ka(o,r[a]))}).map(i=>i.queryHash)}function ka(e,n){if(e&&n&&typeof e==="object"&&typeof n==="object"&&!Array.isArray(e)&&!Array.isArray(n)){return Object.entries(e).every(([t,i])=>ka(i,n[t]))}return JSON.stringify(e)===JSON.stringify(n)}import{z as Ke}from"zod";var ln="zustand";function _a(e,n){e.registerTool("get_zustand_state",{description:'Read registered Zustand stores, available state keys, change counts and persistence metadata. includeValues:true requests values. Pass store (and optionally path, like "member.crowns") to read just one store or one value. Use get_events with sources:["zustand"] for retained changes.',inputSchema:{store:Ke.string().optional().describe("Read only this store (name from the list), with its values."),path:Ke.string().optional().describe('With store: read one value by dot path, e.g. "member.crowns" or "lines.0.id".'),includeValues:Ke.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),limit:Ke.number().optional().describe("First N stores. Default all."),deviceId:L}},zn(n,async(t,i)=>{if(t.store){const s=await n.callAction(ln,"getStoreState",{storeName:t.store},i);if(!s?.found)return x(`No Zustand store named "${t.store}". Call get_zustand_state without store to list them.`);if(!t.path)return M(`**${t.store}**
39
- ${JSON.stringify(s.currentState)}`);let c=s.currentState;for(const l of t.path.split(".").filter(Boolean)){if(c==null||typeof c!=="object"||!(l in c)){return x(`"${t.path}" is not in store "${t.store}" (stopped at "${l}"). Read the store without path to see its shape.`)}c=c[l]}return M(`**${t.store}** \xB7 ${t.path} = ${JSON.stringify(c)}`)}const r=await n.callAction(ln,"listStores",{includeValues:t.includeValues??false,limit:t.limit},i);if(!r||!Array.isArray(r.stores)){return x(ma({toolId:ln,mcpTool:"get_zustand_state",action:"listStores"}))}if(r.stores.length===0){return M("No Zustand stores are registered (call watchStores to register them).")}const o=r.stores.map((s,c)=>{const l=s.isPersisted?` \xB7 persisted${s.persistName?` (${s.persistName})`:""}`:"";const d=r.includedValues?`
37
+ ${o.join("\n")}${c}`)}catch(i){return x(j(i))}});e.registerTool("react_query_action",{description:"Mutate the app's React Query cache: invalidate / refetch / reset / remove a single query (by queryHash, from get_react_query), or clear the entire query cache. Use this to fix stale data, force a refetch, or reset state during debugging.",inputSchema:{action:Ve.enum(["invalidate","refetch","reset","remove","clear","list"]).describe("invalidate/refetch/reset/remove one query (by queryHash or queryKey); clear empties the whole cache; list shows the queries."),queryHash:Ve.string().optional().describe("The target query's hash (from get_react_query, shown in backticks)."),queryKey:Ve.union([Ve.array(Ve.unknown()),Ve.string()]).optional().describe('Or the query key, e.g. ["offers"]. Acts on every query whose key starts with it.'),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);if(t.action==="clear"){await ei(n,"clearQueryCache",{},i);return M("Requested clearing of the connected React Query cache. Read it again to confirm.")}const r=t.action==="list"||!t.queryHash&&t.queryKey!==void 0;const o=r?await ei(n,"listQueries",{includeData:false,staleOnly:false,limit:5e3},i):null;if(t.action==="list"){const c=o?.queries??[];return M(c.length?`**Queries (${c.length}):**
38
+ ${c.map(l=>`- \`${l.queryHash}\` \xB7 ${l.status}`).join("\n")}`:"React Query cache is empty.")}let a=t.queryHash?[t.queryHash]:[];if(!a.length&&t.queryKey!==void 0){a=Oh(t.queryKey,o?.queries??[]);if(!a.length){const c=o&&o.total>o.returned?` (searched ${o.returned} of ${o.total} queries)`:"";return x(`No query has a key starting with ${JSON.stringify(t.queryKey)}${c}. Call get_react_query to see the keys.`)}}if(!a.length){return x(`'${t.action}' needs a queryHash or queryKey. Call get_react_query first and use a row's hash (in backticks).`)}for(const c of a)await ei(n,t.action,{queryHash:c},i);const s=o&&o.total>o.returned?` Note: only ${o.returned} of ${o.total} queries were searched, so some matches may have been missed.`:"";return M(`Requested ${t.action} on ${a.map(c=>`\`${c}\``).join(", ")}.${s}`)}catch(i){return x(j(i))}})}function Oh(e,n){let t;if(Array.isArray(e))t=e;else{try{const i=JSON.parse(e);t=Array.isArray(i)?i:[i]}catch{t=[e]}}return n.filter(i=>{let r=i.queryKey;if(!Array.isArray(r)){try{r=JSON.parse(i.queryHash)}catch{return false}}return Array.isArray(r)&&t.length<=r.length&&t.every((o,a)=>Ca(o,r[a]))}).map(i=>i.queryHash)}function Ca(e,n){if(e&&n&&typeof e==="object"&&typeof n==="object"&&!Array.isArray(e)&&!Array.isArray(n)){return Object.entries(e).every(([t,i])=>Ca(i,n[t]))}return JSON.stringify(e)===JSON.stringify(n)}import{z as Ye}from"zod";var mn="zustand";function Ea(e,n){e.registerTool("get_zustand_state",{description:'Read registered Zustand stores, available state keys, change counts and persistence metadata. includeValues:true requests values. Pass store (and optionally path, like "member.crowns") to read just one store or one value. Use get_events with sources:["zustand"] for retained changes.',inputSchema:{store:Ye.string().optional().describe("Read only this store (name from the list), with its values."),path:Ye.string().optional().describe('With store: read one value by dot path, e.g. "member.crowns" or "lines.0.id".'),includeValues:Ye.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),limit:Ye.number().optional().describe("First N stores. Default all."),deviceId:L}},Qn(n,async(t,i)=>{if(t.store){const s=await n.callAction(mn,"getStoreState",{storeName:t.store},i);if(!s?.found)return x(`No Zustand store named "${t.store}". Call get_zustand_state without store to list them.`);if(!t.path)return M(`**${t.store}**
39
+ ${JSON.stringify(s.currentState)}`);let c=s.currentState;for(const l of t.path.split(".").filter(Boolean)){if(c==null||typeof c!=="object"||!(l in c)){return x(`"${t.path}" is not in store "${t.store}" (stopped at "${l}"). Read the store without path to see its shape.`)}c=c[l]}return M(`**${t.store}** \xB7 ${t.path} = ${JSON.stringify(c)}`)}const r=await n.callAction(mn,"listStores",{includeValues:t.includeValues??false,limit:t.limit},i);if(!r||!Array.isArray(r.stores)){return x(Sa({toolId:mn,mcpTool:"get_zustand_state",action:"listStores"}))}if(r.stores.length===0){return M("No Zustand stores are registered (call watchStores to register them).")}const o=r.stores.map((s,c)=>{const l=s.isPersisted?` \xB7 persisted${s.persistName?` (${s.persistName})`:""}`:"";const d=r.includedValues?`
40
40
  ${JSON.stringify(s.currentState)}`:` \xB7 keys: ${(s.keys??[]).join(", ")||"\u2014"}`;return`${c+1}. **${s.name}** \xB7 ${s.changes} changes${l}${d}`});const a=r.includedValues?"":"\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";return M(`**Zustand stores \u2014 ${r.total} (showing ${r.returned}):**
41
41
 
42
- ${o.join("\n")}${a}`)}));e.registerTool("zustand_action",{description:"Change a Zustand store by name (from get_zustand_state). Merges by default: send only the fields to change, including nested ones ({member:{crowns:5}} keeps member's other fields), and the store's action functions stay. Pass replace:true only to reset the whole store; that drops the store's functions and can break the app.",inputSchema:{store:Ke.string().optional().describe("Store name to target (from get_zustand_state). Leave out to list the stores."),state:Ke.record(Ke.string(),Ke.unknown()).optional().describe("The fields to change."),replace:Ke.boolean().optional().describe("Default false (merge). true replaces the whole state and deletes the store's action functions."),deviceId:L}},zn(n,async(t,i)=>{if(!t.store||!t.state){const a=await n.callAction(ln,"listStores",{includeValues:false},i);const s=a?.stores??[];const c=s.map(d=>`- **${d.name}** \xB7 keys: ${(d.keys??[]).join(", ")||"\u2014"}`).join("\n")||"(no stores registered)";const l=!t.store?"a store":"state (the fields to change)";return M(`Nothing changed: zustand_action needs ${l}. Stores:
42
+ ${o.join("\n")}${a}`)}));e.registerTool("zustand_action",{description:"Change a Zustand store by name (from get_zustand_state). Merges by default: send only the fields to change, including nested ones ({member:{crowns:5}} keeps member's other fields), and the store's action functions stay. Pass replace:true only to reset the whole store; that drops the store's functions and can break the app.",inputSchema:{store:Ye.string().optional().describe("Store name to target (from get_zustand_state). Leave out to list the stores."),state:Ye.record(Ye.string(),Ye.unknown()).optional().describe("The fields to change."),replace:Ye.boolean().optional().describe("Default false (merge). true replaces the whole state and deletes the store's action functions."),deviceId:L}},Qn(n,async(t,i)=>{if(!t.store||!t.state){const a=await n.callAction(mn,"listStores",{includeValues:false},i);const s=a?.stores??[];const c=s.map(d=>`- **${d.name}** \xB7 keys: ${(d.keys??[]).join(", ")||"\u2014"}`).join("\n")||"(no stores registered)";const l=!t.store?"a store":"state (the fields to change)";return M(`Nothing changed: zustand_action needs ${l}. Stores:
43
43
  ${c}
44
44
 
45
- Example: zustand_action({store: "${s[0]?.name??"cart"}", state: {...}})`)}const r=await n.callAction(ln,"setState",{storeName:t.store,state:t.state,replace:t.replace??false},i);if(r&&r.ok===false)return x(`The app refused the change to "${t.store}": ${(r.error??"no reason given").replace(/\.$/,"")}.`);const o=[...r?.keyed??[],...r?.unchecked??[]];return M(`Updated Zustand store "${t.store}" (${t.replace===true?"replaced the whole state":"merged"}).${o.length?` Note: ${o.join(" ")}`:""}`)}))}import{z as Hn}from"zod";var pr="jotai";function Sa(e,n){e.registerTool("get_jotai_state",{description:'Read watched Jotai atoms and their available labels, change counts and writable status. includeValues:true requests values. Use get_events with sources:["jotai"] for retained changes.',inputSchema:{includeValues:Hn.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),limit:Hn.number().optional().describe("First N atoms. Default all."),deviceId:L}},zn(n,async(t,i)=>{const r=await n.callAction(pr,"listAtoms",{includeValues:t.includeValues??false,limit:t.limit},i);if(!r||!Array.isArray(r.atoms)){return x(ma({toolId:pr,mcpTool:"get_jotai_state",action:"listAtoms"}))}if(r.atoms.length===0){return M("No Jotai atoms are being watched (call watchAtoms to register them).")}const o=r.atoms.map((s,c)=>{const l=s.writable?"writable":"read-only";const d=r.includedValues?`
45
+ Example: zustand_action({store: "${s[0]?.name??"cart"}", state: {...}})`)}const r=await n.callAction(mn,"setState",{storeName:t.store,state:t.state,replace:t.replace??false},i);if(r&&r.ok===false)return x(`The app refused the change to "${t.store}": ${(r.error??"no reason given").replace(/\.$/,"")}.`);const o=[...r?.keyed??[],...r?.unchecked??[]];return M(`Updated Zustand store "${t.store}" (${t.replace===true?"replaced the whole state":"merged"}).${o.length?` Note: ${o.join(" ")}`:""}`)}))}import{z as ti}from"zod";var $r="jotai";function Oa(e,n){e.registerTool("get_jotai_state",{description:'Read watched Jotai atoms and their available labels, change counts and writable status. includeValues:true requests values. Use get_events with sources:["jotai"] for retained changes.',inputSchema:{includeValues:ti.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),limit:ti.number().optional().describe("First N atoms. Default all."),deviceId:L}},Qn(n,async(t,i)=>{const r=await n.callAction($r,"listAtoms",{includeValues:t.includeValues??false,limit:t.limit},i);if(!r||!Array.isArray(r.atoms)){return x(Sa({toolId:$r,mcpTool:"get_jotai_state",action:"listAtoms"}))}if(r.atoms.length===0){return M("No Jotai atoms are being watched (call watchAtoms to register them).")}const o=r.atoms.map((s,c)=>{const l=s.writable?"writable":"read-only";const d=r.includedValues?`
46
46
  ${JSON.stringify(s.currentValue)}`:"";return`${c+1}. **${s.label}** \xB7 ${s.changes} changes \xB7 ${l}${d}`});const a=r.includedValues?"":"\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";return M(`**Jotai atoms \u2014 ${r.total} (showing ${r.returned}):**
47
47
 
48
- ${o.join("\n")}${a}`)}));e.registerTool("jotai_action",{description:"Set the value of a writable Jotai atom (by label, from get_jotai_state). Read-only/derived atoms cannot be set (you'll get a clear error). Use for tweaking state during debugging.",inputSchema:{atom:Hn.string().describe("Atom label to set (from get_jotai_state; must be writable)."),value:Hn.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),deviceId:L}},zn(n,async(t,i)=>{await n.callAction(pr,"setAtom",{label:t.atom,value:t.value},i);return M(`Requested update of Jotai atom "${t.atom}" to ${JSON.stringify(t.value)}.`)}))}import{z as fr}from"zod";var xa="redux";function Ta(e){switch(e){case"no-react-redux":return"react-redux is not installed in the app \u2014 the Redux tool requires it (npm install react-redux).";case"no-provider":return"No Redux Provider above <FloatingDevTools />. Move <FloatingDevTools /> inside your <Provider store={store}> and retry.";case"not-instrumented":default:return"Redux store not instrumented yet. On current Buoy versions the store binds automatically at app startup \u2014 this usually means the app is running an older @buoy-gg/core (upgrade it), or the Redux tool hasn't been opened once (older versions only). Apps can also bind explicitly with registerReduxStore(store) from @buoy-gg/redux."}}var gh="\n\n_Note: capture is top-level-only on this device (thunk-internal / RTK Query actions aren't recorded). Put `import '@buoy-gg/redux';` first in the app entry, or add `buoyReduxMiddleware`, for full capture._";function Da(e,n){e.registerTool("get_redux_state",{description:'Read available state from the connected Redux integration. Lists top-level slices by default; includeValues:true requests state values. Use get_events with sources:["redux"] for retained action history.',inputSchema:{includeValues:fr.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(xa,"getState",{includeValues:t.includeValues??false},i);if(!r){return x("No redux result from the device. The redux tool may be inactive, or the app needs a newer Buoy (get_redux_state requires the getState action).")}if(!r.available)return x(Ta(r.reason));const o=r.slices.length?r.slices.join(", "):"(none)";const a=r.capture==="top-level-only"?gh:"";if(t.includeValues){return M(`**Redux store \u2014 slices: ${o}**
48
+ ${o.join("\n")}${a}`)}));e.registerTool("jotai_action",{description:"Set the value of a writable Jotai atom (by label, from get_jotai_state). Read-only/derived atoms cannot be set (you'll get a clear error). Use for tweaking state during debugging.",inputSchema:{atom:ti.string().describe("Atom label to set (from get_jotai_state; must be writable)."),value:ti.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),deviceId:L}},Qn(n,async(t,i)=>{await n.callAction($r,"setAtom",{label:t.atom,value:t.value},i);return M(`Requested update of Jotai atom "${t.atom}" to ${JSON.stringify(t.value)}.`)}))}import{z as kr}from"zod";var Ra="redux";function Pa(e){switch(e){case"no-react-redux":return"react-redux is not installed in the app \u2014 the Redux tool requires it (npm install react-redux).";case"no-provider":return"No Redux Provider above <FloatingDevTools />. Move <FloatingDevTools /> inside your <Provider store={store}> and retry.";case"not-instrumented":default:return"Redux store not instrumented yet. On current Buoy versions the store binds automatically at app startup \u2014 this usually means the app is running an older @buoy-gg/core (upgrade it), or the Redux tool hasn't been opened once (older versions only). Apps can also bind explicitly with registerReduxStore(store) from @buoy-gg/redux."}}var Rh="\n\n_Note: capture is top-level-only on this device (thunk-internal / RTK Query actions aren't recorded). Put `import '@buoy-gg/redux';` first in the app entry, or add `buoyReduxMiddleware`, for full capture._";function Na(e,n){e.registerTool("get_redux_state",{description:'Read available state from the connected Redux integration. Lists top-level slices by default; includeValues:true requests state values. Use get_events with sources:["redux"] for retained action history.',inputSchema:{includeValues:kr.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Ra,"getState",{includeValues:t.includeValues??false},i);if(!r){return x("No redux result from the device. The redux tool may be inactive, or the app needs a newer Buoy (get_redux_state requires the getState action).")}if(!r.available)return x(Pa(r.reason));const o=r.slices.length?r.slices.join(", "):"(none)";const a=r.capture==="top-level-only"?Rh:"";if(t.includeValues){return M(`**Redux store \u2014 slices: ${o}**
49
49
 
50
50
  \`\`\`json
51
51
  ${JSON.stringify(r.state,null,2)}
52
52
  \`\`\`${a}`)}return M(`**Redux store \u2014 slices: ${o}**
53
53
 
54
- _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${a}`)}catch(i){return x(j(i))}});e.registerTool("redux_dispatch",{description:"Dispatch a Redux action to the app's store (e.g. to trigger a state change while debugging). Provide the action type and optional payload \u2014 they're sent as { type, payload }.",inputSchema:{type:fr.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),payload:fr.unknown().optional().describe("Optional action payload (any JSON)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r={type:t.type};if(t.payload!==void 0)r.payload=t.payload;const o=await n.callAction(xa,"dispatch",{action:r},i);if(!o||!o.dispatched)return x(Ta(o?.reason));return M(`Dispatched \`${t.type}\`${t.payload!==void 0?` with payload ${JSON.stringify(t.payload)}`:""}.`)}catch(i){return x(j(i))}})}import{z as Lt}from"zod";var st="storage";function mr(e,n=200){if(e===null||e===void 0)return"(null)";const t=typeof e==="string"?e:JSON.stringify(e);return t.length>n?`${t.slice(0,n)}\u2026(+${t.length-n} chars)`:t}function Ia(e,n){e.registerTool("get_storage",{description:"Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Data wrong or missing after an app restart \u2192 read the persisted values here FIRST \u2014 the bug is usually a stale, missing, or malformed stored value, and reading it beats hunting for it in code. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out.",inputSchema:{pattern:Lt.string().optional().describe("Only keys containing this substring (case-insensitive)."),includeValues:Lt.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),limit:Lt.number().optional().describe("Max keys per backend section. Default 100."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=t.pattern?.toLowerCase();const o=typeof t.limit==="number"&&t.limit>0?t.limit:100;const a=f=>!r||f.toLowerCase().includes(r);const s=[];const c=await n.callAction(st,"async.getAllKeys",{},i);if(!Array.isArray(c)){return x("No storage result from the device. The storage tool may be inactive.")}const l=c.filter(a).slice(0,o);if(l.length>0){let f;if(t.includeValues){const w=await n.callAction(st,"async.multiGet",{keys:l},i);const g=new Map((w??[]).map(([m,p])=>[m,p]));f=l.map(m=>`- \`${m}\` = ${mr(g.get(m)??null)}`)}else{f=l.map(w=>`- \`${w}\``)}s.push(`**AsyncStorage (${l.length}/${c.length} keys):**
55
- ${f.join("\n")}`)}const d=await n.callAction(st,"mmkv.snapshot",{},i).catch(()=>null);for(const f of d??[]){const w=f.entries.filter(p=>a(p.key)).slice(0,o);if(w.length===0)continue;const g=w.map(p=>t.includeValues?`- \`${p.key}\` (${p.valueType}) = ${mr(p.value)}`:`- \`${p.key}\``);const m=[f.encrypted&&"encrypted",f.readOnly&&"read-only"].filter(Boolean).join(", ");s.push(`**MMKV \`${f.id}\`${m?` (${m})`:""} (${w.length}/${f.entries.length} keys):**
56
- ${g.join("\n")}`)}const u=await n.callAction(st,"secure.keys",{},i).catch(()=>null);const h=(u??[]).filter(f=>a(f.key)).slice(0,o);if(h.length>0){const f=await Promise.all(h.map(async w=>{if(w.requireAuthentication)return`- \`${w.key}\` \u{1F512} (biometric-protected \u2014 not read)`;if(!t.includeValues)return`- \`${w.key}\``;const g=await n.callAction(st,"secure.get",{key:w.key},i).catch(()=>null);return`- \`${w.key}\` = ${mr(g)}`}));s.push(`**SecureStore (${h.length} registered keys):**
57
- ${f.join("\n")}`)}if(s.length===0){return M(`No storage keys${r?` matching "${t.pattern}"`:""} in any backend.`)}const b=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return M(`${s.join("\n\n")}${b}`)}catch(i){return x(j(i))}});e.registerTool("storage_action",{description:"Modify the app's AsyncStorage: set a key, remove a key, or clear supported app AsyncStorage keys (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",inputSchema:{action:Lt.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:Lt.string().optional().describe("The key (required for set and remove)."),value:Lt.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(st,"clearAppStorage",{},i);return M("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return x(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(st,"async.removeItem",{key:t.key},i);return M(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return x("'set' needs a value (a string).")}await n.callAction(st,"async.setItem",{key:t.key,value:t.value},i);return M(`Requested update of storage key "${t.key}".`)}catch(i){return x(j(i))}})}import{spawn as bh}from"child_process";var gt=new Map;var Jn=new Map;function wr(){for(const e of gt.values())e.child.kill("SIGKILL")}function Ma(e,n,t,i=""){const r=n;const o=(Jn.get(r)??Promise.resolve()).catch(()=>void 0).then(()=>vh(r,e,t,i));Jn.set(r,o);void o.finally(()=>{if(Jn.get(r)===o)Jn.delete(r)}).catch(()=>void 0);return o}async function vh(e,n,t,i){let r=gt.get(e);if(r&&r.bin!==n)r.stop(new Error("The touch helper build changed."));if(r?.stopped){await r.exited;r=void 0}if(!r){const a=bh(n,["--stream"],{stdio:["pipe","pipe","pipe"]});let s;const c=new Promise(u=>{s=u});r={bin:n,child:a,buffer:"",stopped:false,exited:c,stop:()=>{}};if(gt.size===0)process.once("exit",wr);gt.set(e,r);const l=r;const d=u=>{if(l.stopped)return;l.stopped=true;if(l.idle)clearTimeout(l.idle);const h=l.pending;l.pending=void 0;if(h)clearTimeout(h.timer);a.ref();a.kill("SIGKILL");void c.then(()=>{if(gt.get(e)===l)gt.delete(e);if(gt.size===0)process.removeListener("exit",wr);h?.reject(u)})};l.stop=d;a.on("error",u=>{s();d(u)});a.on("exit",()=>{s();d(new Error("The touch helper closed. Try the next touch."))});a.stdin.on("error",u=>d(u));a.stdout.on("data",u=>{l.buffer+=u;let h;while((h=l.buffer.indexOf("\n"))>=0){const b=l.buffer.slice(0,h);l.buffer=l.buffer.slice(h+1);const f=l.pending;if(!f)continue;try{const w=JSON.parse(b);if(!w||typeof w.ok!=="boolean")throw new Error("Bad reply.");if(w.ok===true){clearTimeout(f.timer);l.pending=void 0;f.resolve()}else d(new Error(w.error??"The touch failed."))}catch{d(new Error("The touch helper sent a bad reply."))}}});a.stderr.resume();a.unref();for(const u of[a.stdin,a.stdout,a.stderr])u.unref?.()}const o=r;if(o.idle)clearTimeout(o.idle);return new Promise((a,s)=>{const c=setTimeout(()=>{o.stop(new Error("The touch helper timed out."))},45e3);o.pending={resolve:a,reject:s,timer:c};o.child.stdin.write(JSON.stringify({args:t,input:i})+"\n")}).finally(()=>{if(o.stopped)return;o.idle=setTimeout(()=>{o.child.stdin.end();o.stop(new Error("The touch helper went idle."))},1e4);o.idle.unref()})}import{z as ue}from"zod";function yh(e,n,t){if(t!=null){const o=e.find(a=>a.nativeTag===t);if(o)return o}const i=(n??"").trim().toLowerCase();if(!i)return void 0;let r;for(const o of e){const a=[o.testID,o.label,o.text,o.name].filter(Boolean).map(l=>l.toLowerCase());let s=0;for(const l of a){if(l===i)s=Math.max(s,100);else if(l.startsWith(i))s=Math.max(s,50);else if(l.includes(i))s=Math.max(s,25)}const c=o.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)s+=20;else if(c)s-=40;if(s<=0)continue;if(!r||s>r.score)r={el:o,score:s}}return r?.el}function $h(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var bt=Cn(On());function Aa(e,n,t,i,r,o={}){const{threshold:a=.1,alpha:s=.1,aaColor:c=[255,255,0],diffColor:l=[255,0,0],checkerboard:d=true,includeAA:u,diffColorAlt:h,diffMask:b}=o;if(!gr(e)||!gr(n)||t&&!gr(t))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(e.length!==n.length||t&&t.length!==e.length)throw new Error(`Image sizes do not match. Image 1 size: ${e.length}, image 2 size: ${n.length}`);if(e.length!==i*r*4)throw new Error(`Image data size does not match width/height. Expecting ${i*r*4}. Got ${e.length}`);const f=i*r;const w=new Uint32Array(e.buffer,e.byteOffset,f);const g=new Uint32Array(n.buffer,n.byteOffset,f);let m=true;for(let S=0;S<f;S++){if(w[S]!==g[S]){m=false;break}}if(m){if(t&&!b){for(let S=0,A=0;S<f;S++,A+=4)Ca(e,A,s,t)}return 0}const p=35215*a*a;const[y,v,$]=c;const[T,k,D]=l;const[E,I,R]=h||l;let _=0;for(let S=0,A=0;S<f;S++,A+=4){const O=w[S]===g[S]?0:kh(e,n,A,A,d);if(Math.abs(O)>p){const F=S%i;const P=S/i|0;const K=!u&&(Ea(e,F,P,i,r,w,g,d)||Ea(n,F,P,i,r,g,w,d));if(K){if(t&&!b)Kn(t,A,y,v,$)}else{if(t){if(O<0){Kn(t,A,E,I,R)}else{Kn(t,A,T,k,D)}}_++}}else if(t&&!b){Ca(e,A,s,t)}}return _}function gr(e){return ArrayBuffer.isView(e)&&e.BYTES_PER_ELEMENT===1}function Ea(e,n,t,i,r,o,a,s){const c=Math.max(n-1,0);const l=Math.max(t-1,0);const d=Math.min(n+1,i-1);const u=Math.min(t+1,r-1);const h=(t*i+n)*4;const b=e[h];const f=e[h+1];const w=e[h+2];const g=e[h+3];let m=n===c||n===d||t===l||t===u?1:0;let p=0;let y=0;let v=0;let $=0;let T=0;let k=0;for(let D=c;D<=d;D++){for(let E=l;E<=u;E++){if(D===n&&E===t)continue;const I=_h(e,h,(E*i+D)*4,b,f,w,g,s);if(I===0){m++;if(m>2)return false}else if(I<p){p=I;v=D;$=E}else if(I>y){y=I;T=D;k=E}}}if(p===0||y===0)return false;return Gn(o,v,$,i,r)&&Gn(a,v,$,i,r)||Gn(o,T,k,i,r)&&Gn(a,T,k,i,r)}function Gn(e,n,t,i,r){const o=Math.max(n-1,0);const a=Math.max(t-1,0);const s=Math.min(n+1,i-1);const c=Math.min(t+1,r-1);const l=e[t*i+n];let d=n===o||n===s||t===a||t===c?1:0;for(let u=o;u<=s;u++){for(let h=a;h<=c;h++){if(u===n&&h===t)continue;d+=+(l===e[h*i+u]);if(d>2)return true}}return false}function kh(e,n,t,i,r){const o=e[t];const a=e[t+1];const s=e[t+2];const c=e[t+3];const l=n[i];const d=n[i+1];const u=n[i+2];const h=n[i+3];let b=o-l;let f=a-d;let w=s-u;const g=c-h;if(c<255||h<255){let $=255,T=255,k=255;if(r){$=48+159*(t%2);T=48+159*((t/1.618033988749895|0)%2);k=48+159*((t/2.618033988749895|0)%2)}b=(o*c-l*h-$*g)/255;f=(a*c-d*h-T*g)/255;w=(s*c-u*h-k*g)/255}const m=b*.29889531+f*.58662247+w*.11448223;const p=b*.59597799-f*.2741761-w*.32180189;const y=b*.21147017-f*.52261711+w*.31114694;const v=.5053*m*m+.299*p*p+.1957*y*y;return m>0?-v:v}function _h(e,n,t,i,r,o,a,s){const c=e[t];const l=e[t+1];const d=e[t+2];const u=e[t+3];let h=i-c;let b=r-l;let f=o-d;const w=a-u;if(!h&&!b&&!f&&!w)return 0;if(a<255||u<255){let g=255,m=255,p=255;if(s){g=48+159*(n%2);m=48+159*((n/1.618033988749895|0)%2);p=48+159*((n/2.618033988749895|0)%2)}h=(i*a-c*u-g*w)/255;b=(r*a-l*u-m*w)/255;f=(o*a-d*u-p*w)/255}return h*.29889531+b*.58662247+f*.11448223}function Kn(e,n,t,i,r){e[n]=t;e[n+1]=i;e[n+2]=r;e[n+3]=255}function Ca(e,n,t,i){const r=255+(e[n]*.29889531+e[n+1]*.58662247+e[n+2]*.11448223-255)*t*e[n+3]/255;Kn(i,n,r,r,r)}function dn(e){return bt.PNG.sync.read(e)}function br(e,n,t={}){const i=dn(e);const r=dn(n);if(i.width!==r.width||i.height!==r.height){throw new Error(`The screenshots are different sizes (${i.width}\xD7${i.height} vs ${r.width}\xD7${r.height}). Compare on the same device and orientation, or save a new baseline.`)}const{width:o,height:a}=i;const s=Math.round(a*(t.ignoreTop??0));for(const b of[i,r])b.data.fill(0,0,s*o*4);for(const b of t.masks??[]){const f=Math.max(0,Math.floor(b.x*o));const w=Math.min(o,Math.ceil((b.x+b.width)*o));const g=Math.max(0,Math.floor(b.y*a));const m=Math.min(a,Math.ceil((b.y+b.height)*a));for(const p of[i,r])for(let y=g;y<m;y++)p.data.fill(0,(y*o+f)*4,(y*o+w)*4)}const c=t.threshold??.1;const l=new bt.PNG({width:o,height:a});const d=Aa(i.data,r.data,l.data,o,a,{threshold:c,includeAA:false,diffMask:true});const u=new bt.PNG({width:o,height:a});Aa(i.data,r.data,u.data,o,a,{threshold:c,includeAA:false,alpha:.25,diffColor:[230,30,40]});const h=o*(a-s);return{width:o,height:a,changedPixels:d,percent:h?d/h*100:0,boxes:Sh(l.data,o,a),diffPng:bt.PNG.sync.write(u)}}function Sh(e,n,t){const i=Math.max(8,Math.round(n/48));const r=Math.ceil(n/i);const o=Math.ceil(t/i);const a=new Uint32Array(r*o);for(let d=0;d<t;d++){for(let u=0;u<n;u++){if(e[(d*n+u)*4+3]>0)a[Math.floor(d/i)*r+Math.floor(u/i)]++}}const s=1;const c=new Uint8Array(r*o);const l=[];for(let d=0;d<a.length;d++){if(c[d]||a[d]<s)continue;const u={x0:Infinity,y0:Infinity,x1:-1,y1:-1,pixels:0};const h=[d];c[d]=1;while(h.length){const b=h.pop();const f=b%r;const w=Math.floor(b/r);u.x0=Math.min(u.x0,f);u.y0=Math.min(u.y0,w);u.x1=Math.max(u.x1,f);u.y1=Math.max(u.y1,w);u.pixels+=a[b];for(let g=-2;g<=2;g++){for(let m=-2;m<=2;m++){const p=f+m;const y=w+g;if(p<0||y<0||p>=r||y>=o)continue;const v=y*r+p;if(!c[v]&&a[v]>=s){c[v]=1;h.push(v)}}}}l.push(u)}return l.sort((d,u)=>u.pixels-d.pixels).slice(0,8).map(d=>({x:d.x0*i/n,y:d.y0*i/t,width:Math.min(n,(d.x1+1)*i-d.x0*i)/n,height:Math.min(t,(d.y1+1)*i-d.y0*i)/t,pixels:d.pixels}))}function xh(e,n,t,i=0){const r=dn(e);const o=dn(n);if(r.width!==o.width||r.height!==o.height)return null;const{width:a,height:s}=r;const c=Math.max(0,Math.floor(t.x*a));const l=Math.min(a,Math.ceil((t.x+t.width)*a));const d=Math.max(Math.round(s*i),Math.floor(t.y*s));const u=Math.min(s,Math.ceil((t.y+t.height)*s));const h=new Map;const b=new Map;for(let p=d;p<u;p++){for(let y=c;y<l;y++){const v=(p*a+y)*4;const $=r.data[v]<<16|r.data[v+1]<<8|r.data[v+2];const T=o.data[v]<<16|o.data[v+1]<<8|o.data[v+2];if($===T)continue;h.set($,(h.get($)??0)+1);b.set(T,(b.get(T)??0)+1)}}const f=p=>[...p].sort((y,v)=>v[1]-y[1])[0]?.[0];const w=f(h);const g=f(b);if(w===void 0||g===void 0)return null;const m=p=>`#${p.toString(16).padStart(6,"0").toUpperCase()}`;return{before:m(w),after:m(g)}}function Oa(e){const n=e.x+e.width/2;const t=e.y+e.height/2;const i=t<.33?"top":t<.66?"middle":"bottom";const r=n<.33?" left":n<.66?"":" right";return`${i}${r}`}function Ra(e,n){const t=dn(e);const i=Math.max(0,Math.floor(n.x*t.width));const r=Math.max(0,Math.floor(n.y*t.height));const o=Math.max(1,Math.min(t.width-i,Math.round(n.width*t.width)));const a=Math.max(1,Math.min(t.height-r,Math.round(n.height*t.height)));const s=new bt.PNG({width:o,height:a});bt.PNG.bitblt(t,s,i,r,o,a,0,0);return bt.PNG.sync.write(s)}import Pa from"os";import{constants as Na,promises as Ve}from"fs";import un from"path";var Fa=Cn(On());var qt="highlight-updates";var Th=12;var Dh=6;var Ye=e=>({isError:true,content:[{type:"text",text:e}]});var vr=e=>e instanceof Error?e.message:String(e);function Ih(e,n){switch(e.reason){case"no-renders":return"No components matched via highlight tracking or the live fiber walk. Pass a testID / name from describe_screen, then retry.";case"no-match":return n?`Nothing matched "${n}".`:"No query or nativeTag given \u2014 pass one, or a testID/name to search for.";case"no-measurement":return"Found the component but couldn't measure it \u2014 it may have just unmounted. Re-navigate to it and retry.";default:return e.reason?`Locate failed: ${e.reason}`:"Couldn't locate that component."}}function Ba(e){if(!e||e.length===0)return"";const n=e.map(t=>{const i=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${i?` (${i})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
54
+ _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${a}`)}catch(i){return x(j(i))}});e.registerTool("redux_dispatch",{description:"Dispatch a Redux action to the app's store (e.g. to trigger a state change while debugging). Provide the action type and optional payload \u2014 they're sent as { type, payload }.",inputSchema:{type:kr.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),payload:kr.unknown().optional().describe("Optional action payload (any JSON)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r={type:t.type};if(t.payload!==void 0)r.payload=t.payload;const o=await n.callAction(Ra,"dispatch",{action:r},i);if(!o||!o.dispatched)return x(Pa(o?.reason));return M(`Dispatched \`${t.type}\`${t.payload!==void 0?` with payload ${JSON.stringify(t.payload)}`:""}.`)}catch(i){return x(j(i))}})}import{z as Wt}from"zod";var lt="storage";function _r(e,n=200){if(e===null||e===void 0)return"(null)";const t=typeof e==="string"?e:JSON.stringify(e);return t.length>n?`${t.slice(0,n)}\u2026(+${t.length-n} chars)`:t}function Fa(e,n){e.registerTool("get_storage",{description:"Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Data wrong or missing after an app restart \u2192 read the persisted values here FIRST \u2014 the bug is usually a stale, missing, or malformed stored value, and reading it beats hunting for it in code. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out.",inputSchema:{pattern:Wt.string().optional().describe("Only keys containing this substring (case-insensitive)."),includeValues:Wt.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),limit:Wt.number().optional().describe("Max keys per backend section. Default 100."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=t.pattern?.toLowerCase();const o=typeof t.limit==="number"&&t.limit>0?t.limit:100;const a=f=>!r||f.toLowerCase().includes(r);const s=[];const c=await n.callAction(lt,"async.getAllKeys",{},i);if(!Array.isArray(c)){return x("No storage result from the device. The storage tool may be inactive.")}const l=c.filter(a).slice(0,o);if(l.length>0){let f;if(t.includeValues){const w=await n.callAction(lt,"async.multiGet",{keys:l},i);const g=new Map((w??[]).map(([m,p])=>[m,p]));f=l.map(m=>`- \`${m}\` = ${_r(g.get(m)??null)}`)}else{f=l.map(w=>`- \`${w}\``)}s.push(`**AsyncStorage (${l.length}/${c.length} keys):**
55
+ ${f.join("\n")}`)}const d=await n.callAction(lt,"mmkv.snapshot",{},i).catch(()=>null);for(const f of d??[]){const w=f.entries.filter(p=>a(p.key)).slice(0,o);if(w.length===0)continue;const g=w.map(p=>t.includeValues?`- \`${p.key}\` (${p.valueType}) = ${_r(p.value)}`:`- \`${p.key}\``);const m=[f.encrypted&&"encrypted",f.readOnly&&"read-only"].filter(Boolean).join(", ");s.push(`**MMKV \`${f.id}\`${m?` (${m})`:""} (${w.length}/${f.entries.length} keys):**
56
+ ${g.join("\n")}`)}const u=await n.callAction(lt,"secure.keys",{},i).catch(()=>null);const h=(u??[]).filter(f=>a(f.key)).slice(0,o);if(h.length>0){const f=await Promise.all(h.map(async w=>{if(w.requireAuthentication)return`- \`${w.key}\` \u{1F512} (biometric-protected \u2014 not read)`;if(!t.includeValues)return`- \`${w.key}\``;const g=await n.callAction(lt,"secure.get",{key:w.key},i).catch(()=>null);return`- \`${w.key}\` = ${_r(g)}`}));s.push(`**SecureStore (${h.length} registered keys):**
57
+ ${f.join("\n")}`)}if(s.length===0){return M(`No storage keys${r?` matching "${t.pattern}"`:""} in any backend.`)}const b=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return M(`${s.join("\n\n")}${b}`)}catch(i){return x(j(i))}});e.registerTool("storage_action",{description:"Modify the app's AsyncStorage: set a key, remove a key, or clear supported app AsyncStorage keys (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",inputSchema:{action:Wt.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:Wt.string().optional().describe("The key (required for set and remove)."),value:Wt.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(lt,"clearAppStorage",{},i);return M("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return x(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(lt,"async.removeItem",{key:t.key},i);return M(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return x("'set' needs a value (a string).")}await n.callAction(lt,"async.setItem",{key:t.key,value:t.value},i);return M(`Requested update of storage key "${t.key}".`)}catch(i){return x(j(i))}})}import{spawn as Ph}from"child_process";var yt=new Map;var ni=new Map;function Sr(){for(const e of yt.values())e.child.kill("SIGKILL")}function Ba(e,n,t,i=""){const r=n;const o=(ni.get(r)??Promise.resolve()).catch(()=>void 0).then(()=>Nh(r,e,t,i));ni.set(r,o);void o.finally(()=>{if(ni.get(r)===o)ni.delete(r)}).catch(()=>void 0);return o}async function Nh(e,n,t,i){let r=yt.get(e);if(r&&r.bin!==n)r.stop(new Error("The touch helper build changed."));if(r?.stopped){await r.exited;r=void 0}if(!r){const a=Ph(n,["--stream"],{stdio:["pipe","pipe","pipe"]});let s;const c=new Promise(u=>{s=u});r={bin:n,child:a,buffer:"",stopped:false,exited:c,stop:()=>{}};if(yt.size===0)process.once("exit",Sr);yt.set(e,r);const l=r;const d=u=>{if(l.stopped)return;l.stopped=true;if(l.idle)clearTimeout(l.idle);const h=l.pending;l.pending=void 0;if(h)clearTimeout(h.timer);a.ref();a.kill("SIGKILL");void c.then(()=>{if(yt.get(e)===l)yt.delete(e);if(yt.size===0)process.removeListener("exit",Sr);h?.reject(u)})};l.stop=d;a.on("error",u=>{s();d(u)});a.on("exit",()=>{s();d(new Error("The touch helper closed. Try the next touch."))});a.stdin.on("error",u=>d(u));a.stdout.on("data",u=>{l.buffer+=u;let h;while((h=l.buffer.indexOf("\n"))>=0){const b=l.buffer.slice(0,h);l.buffer=l.buffer.slice(h+1);const f=l.pending;if(!f)continue;try{const w=JSON.parse(b);if(!w||typeof w.ok!=="boolean")throw new Error("Bad reply.");if(w.ok===true){clearTimeout(f.timer);l.pending=void 0;f.resolve()}else d(new Error(w.error??"The touch failed."))}catch{d(new Error("The touch helper sent a bad reply."))}}});a.stderr.resume();a.unref();for(const u of[a.stdin,a.stdout,a.stderr])u.unref?.()}const o=r;if(o.idle)clearTimeout(o.idle);return new Promise((a,s)=>{const c=setTimeout(()=>{o.stop(new Error("The touch helper timed out."))},45e3);o.pending={resolve:a,reject:s,timer:c};o.child.stdin.write(JSON.stringify({args:t,input:i})+"\n")}).finally(()=>{if(o.stopped)return;o.idle=setTimeout(()=>{o.child.stdin.end();o.stop(new Error("The touch helper went idle."))},1e4);o.idle.unref()})}import{z as ue}from"zod";function Fh(e,n,t){if(t!=null){const o=e.find(a=>a.nativeTag===t);if(o)return o}const i=(n??"").trim().toLowerCase();if(!i)return void 0;let r;for(const o of e){const a=[o.testID,o.label,o.text,o.name].filter(Boolean).map(l=>l.toLowerCase());let s=0;for(const l of a){if(l===i)s=Math.max(s,100);else if(l.startsWith(i))s=Math.max(s,50);else if(l.includes(i))s=Math.max(s,25)}const c=o.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)s+=20;else if(c)s-=40;if(s<=0)continue;if(!r||s>r.score)r={el:o,score:s}}return r?.el}function Bh(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var $t=Un(zn());function ja(e,n,t,i,r,o={}){const{threshold:a=.1,alpha:s=.1,aaColor:c=[255,255,0],diffColor:l=[255,0,0],checkerboard:d=true,includeAA:u,diffColorAlt:h,diffMask:b}=o;if(!xr(e)||!xr(n)||t&&!xr(t))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(e.length!==n.length||t&&t.length!==e.length)throw new Error(`Image sizes do not match. Image 1 size: ${e.length}, image 2 size: ${n.length}`);if(e.length!==i*r*4)throw new Error(`Image data size does not match width/height. Expecting ${i*r*4}. Got ${e.length}`);const f=i*r;const w=new Uint32Array(e.buffer,e.byteOffset,f);const g=new Uint32Array(n.buffer,n.byteOffset,f);let m=true;for(let S=0;S<f;S++){if(w[S]!==g[S]){m=false;break}}if(m){if(t&&!b){for(let S=0,A=0;S<f;S++,A+=4)qa(e,A,s,t)}return 0}const p=35215*a*a;const[y,v,$]=c;const[T,k,D]=l;const[C,I,R]=h||l;let _=0;for(let S=0,A=0;S<f;S++,A+=4){const O=w[S]===g[S]?0:jh(e,n,A,A,d);if(Math.abs(O)>p){const F=S%i;const P=S/i|0;const K=!u&&(La(e,F,P,i,r,w,g,d)||La(n,F,P,i,r,g,w,d));if(K){if(t&&!b)ri(t,A,y,v,$)}else{if(t){if(O<0){ri(t,A,C,I,R)}else{ri(t,A,T,k,D)}}_++}}else if(t&&!b){qa(e,A,s,t)}}return _}function xr(e){return ArrayBuffer.isView(e)&&e.BYTES_PER_ELEMENT===1}function La(e,n,t,i,r,o,a,s){const c=Math.max(n-1,0);const l=Math.max(t-1,0);const d=Math.min(n+1,i-1);const u=Math.min(t+1,r-1);const h=(t*i+n)*4;const b=e[h];const f=e[h+1];const w=e[h+2];const g=e[h+3];let m=n===c||n===d||t===l||t===u?1:0;let p=0;let y=0;let v=0;let $=0;let T=0;let k=0;for(let D=c;D<=d;D++){for(let C=l;C<=u;C++){if(D===n&&C===t)continue;const I=Lh(e,h,(C*i+D)*4,b,f,w,g,s);if(I===0){m++;if(m>2)return false}else if(I<p){p=I;v=D;$=C}else if(I>y){y=I;T=D;k=C}}}if(p===0||y===0)return false;return ii(o,v,$,i,r)&&ii(a,v,$,i,r)||ii(o,T,k,i,r)&&ii(a,T,k,i,r)}function ii(e,n,t,i,r){const o=Math.max(n-1,0);const a=Math.max(t-1,0);const s=Math.min(n+1,i-1);const c=Math.min(t+1,r-1);const l=e[t*i+n];let d=n===o||n===s||t===a||t===c?1:0;for(let u=o;u<=s;u++){for(let h=a;h<=c;h++){if(u===n&&h===t)continue;d+=+(l===e[h*i+u]);if(d>2)return true}}return false}function jh(e,n,t,i,r){const o=e[t];const a=e[t+1];const s=e[t+2];const c=e[t+3];const l=n[i];const d=n[i+1];const u=n[i+2];const h=n[i+3];let b=o-l;let f=a-d;let w=s-u;const g=c-h;if(c<255||h<255){let $=255,T=255,k=255;if(r){$=48+159*(t%2);T=48+159*((t/1.618033988749895|0)%2);k=48+159*((t/2.618033988749895|0)%2)}b=(o*c-l*h-$*g)/255;f=(a*c-d*h-T*g)/255;w=(s*c-u*h-k*g)/255}const m=b*.29889531+f*.58662247+w*.11448223;const p=b*.59597799-f*.2741761-w*.32180189;const y=b*.21147017-f*.52261711+w*.31114694;const v=.5053*m*m+.299*p*p+.1957*y*y;return m>0?-v:v}function Lh(e,n,t,i,r,o,a,s){const c=e[t];const l=e[t+1];const d=e[t+2];const u=e[t+3];let h=i-c;let b=r-l;let f=o-d;const w=a-u;if(!h&&!b&&!f&&!w)return 0;if(a<255||u<255){let g=255,m=255,p=255;if(s){g=48+159*(n%2);m=48+159*((n/1.618033988749895|0)%2);p=48+159*((n/2.618033988749895|0)%2)}h=(i*a-c*u-g*w)/255;b=(r*a-l*u-m*w)/255;f=(o*a-d*u-p*w)/255}return h*.29889531+b*.58662247+f*.11448223}function ri(e,n,t,i,r){e[n]=t;e[n+1]=i;e[n+2]=r;e[n+3]=255}function qa(e,n,t,i){const r=255+(e[n]*.29889531+e[n+1]*.58662247+e[n+2]*.11448223-255)*t*e[n+3]/255;ri(i,n,r,r,r)}function wn(e){return $t.PNG.sync.read(e)}function Tr(e,n,t={}){const i=wn(e);const r=wn(n);if(i.width!==r.width||i.height!==r.height){throw new Error(`The screenshots are different sizes (${i.width}\xD7${i.height} vs ${r.width}\xD7${r.height}). Compare on the same device and orientation, or save a new baseline.`)}const{width:o,height:a}=i;const s=Math.round(a*(t.ignoreTop??0));for(const b of[i,r])b.data.fill(0,0,s*o*4);for(const b of t.masks??[]){const f=Math.max(0,Math.floor(b.x*o));const w=Math.min(o,Math.ceil((b.x+b.width)*o));const g=Math.max(0,Math.floor(b.y*a));const m=Math.min(a,Math.ceil((b.y+b.height)*a));for(const p of[i,r])for(let y=g;y<m;y++)p.data.fill(0,(y*o+f)*4,(y*o+w)*4)}const c=t.threshold??.1;const l=new $t.PNG({width:o,height:a});const d=ja(i.data,r.data,l.data,o,a,{threshold:c,includeAA:false,diffMask:true});const u=new $t.PNG({width:o,height:a});ja(i.data,r.data,u.data,o,a,{threshold:c,includeAA:false,alpha:.25,diffColor:[230,30,40]});const h=o*(a-s);return{width:o,height:a,changedPixels:d,percent:h?d/h*100:0,boxes:qh(l.data,o,a),diffPng:$t.PNG.sync.write(u)}}function qh(e,n,t){const i=Math.max(8,Math.round(n/48));const r=Math.ceil(n/i);const o=Math.ceil(t/i);const a=new Uint32Array(r*o);for(let d=0;d<t;d++){for(let u=0;u<n;u++){if(e[(d*n+u)*4+3]>0)a[Math.floor(d/i)*r+Math.floor(u/i)]++}}const s=1;const c=new Uint8Array(r*o);const l=[];for(let d=0;d<a.length;d++){if(c[d]||a[d]<s)continue;const u={x0:Infinity,y0:Infinity,x1:-1,y1:-1,pixels:0};const h=[d];c[d]=1;while(h.length){const b=h.pop();const f=b%r;const w=Math.floor(b/r);u.x0=Math.min(u.x0,f);u.y0=Math.min(u.y0,w);u.x1=Math.max(u.x1,f);u.y1=Math.max(u.y1,w);u.pixels+=a[b];for(let g=-2;g<=2;g++){for(let m=-2;m<=2;m++){const p=f+m;const y=w+g;if(p<0||y<0||p>=r||y>=o)continue;const v=y*r+p;if(!c[v]&&a[v]>=s){c[v]=1;h.push(v)}}}}l.push(u)}return l.sort((d,u)=>u.pixels-d.pixels).slice(0,8).map(d=>({x:d.x0*i/n,y:d.y0*i/t,width:Math.min(n,(d.x1+1)*i-d.x0*i)/n,height:Math.min(t,(d.y1+1)*i-d.y0*i)/t,pixels:d.pixels}))}function Uh(e,n,t,i=0){const r=wn(e);const o=wn(n);if(r.width!==o.width||r.height!==o.height)return null;const{width:a,height:s}=r;const c=Math.max(0,Math.floor(t.x*a));const l=Math.min(a,Math.ceil((t.x+t.width)*a));const d=Math.max(Math.round(s*i),Math.floor(t.y*s));const u=Math.min(s,Math.ceil((t.y+t.height)*s));const h=new Map;const b=new Map;for(let p=d;p<u;p++){for(let y=c;y<l;y++){const v=(p*a+y)*4;const $=r.data[v]<<16|r.data[v+1]<<8|r.data[v+2];const T=o.data[v]<<16|o.data[v+1]<<8|o.data[v+2];if($===T)continue;h.set($,(h.get($)??0)+1);b.set(T,(b.get(T)??0)+1)}}const f=p=>[...p].sort((y,v)=>v[1]-y[1])[0]?.[0];const w=f(h);const g=f(b);if(w===void 0||g===void 0)return null;const m=p=>`#${p.toString(16).padStart(6,"0").toUpperCase()}`;return{before:m(w),after:m(g)}}function Ua(e){const n=e.x+e.width/2;const t=e.y+e.height/2;const i=t<.33?"top":t<.66?"middle":"bottom";const r=n<.33?" left":n<.66?"":" right";return`${i}${r}`}function za(e,n){const t=wn(e);const i=Math.max(0,Math.floor(n.x*t.width));const r=Math.max(0,Math.floor(n.y*t.height));const o=Math.max(1,Math.min(t.width-i,Math.round(n.width*t.width)));const a=Math.max(1,Math.min(t.height-r,Math.round(n.height*t.height)));const s=new $t.PNG({width:o,height:a});$t.PNG.bitblt(t,s,i,r,o,a,0,0);return $t.PNG.sync.write(s)}import Wa from"os";import{constants as Ha,promises as Xe}from"fs";import gn from"path";var Ja=Un(zn());var Ht="highlight-updates";var zh=12;var Wh=6;var Ze=e=>({isError:true,content:[{type:"text",text:e}]});var Dr=e=>e instanceof Error?e.message:String(e);function Hh(e,n){switch(e.reason){case"no-renders":return"No components matched via highlight tracking or the live fiber walk. Pass a testID / name from describe_screen, then retry.";case"no-match":return n?`Nothing matched "${n}".`:"No query or nativeTag given \u2014 pass one, or a testID/name to search for.";case"no-measurement":return"Found the component but couldn't measure it \u2014 it may have just unmounted. Re-navigate to it and retry.";default:return e.reason?`Locate failed: ${e.reason}`:"Couldn't locate that component."}}function Ga(e){if(!e||e.length===0)return"";const n=e.map(t=>{const i=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${i?` (${i})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
58
58
 
59
59
  Closest matches (retry with a more precise query, or pass nativeTag directly):
60
- ${n.join("\n")}`}function ja(e,n){e.registerTool("screenshot_component",{_meta:He(),description:"Take a screenshot of the app. Pass compareTo with a saved PNG path to view both screens. It first says what changed, measured: where, which element, and the exact colors before and now. Then the saved file, then the live screen. Leave query and nativeTag out for the whole screen. To crop a React Native view on an iOS Sim, pass query or nativeTag. The tool may scroll that view into sight. Use describe_screen to read text or find controls. Pictures use more tokens. Run the MCP on the device host.",inputSchema:{compareTo:ue.string().min(1).optional().describe("A saved PNG file to view next to this screen. Use a full path or a path from the project root. The file stays as it is."),query:ue.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:ue.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:ue.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),serial:ue.string().optional().describe("Android: the adb serial, when several devices are connected."),baseline:ue.string().optional().describe('Whole screen only: a name like "home". The first call saves the screen as that baseline (in .buoy/baselines); later calls compare against it and report what changed, with a diff picture.'),updateBaseline:ue.boolean().optional().describe("With baseline: save the current screen as the new baseline instead of comparing."),steady:ue.boolean().optional().describe("With baseline: freeze the app's clock at the baseline's moment while capturing, so countdowns and times match (needs @buoy-gg/clock). Default true when the app has the clock tool."),threshold:ue.number().min(0).max(1).optional().describe("With baseline: per-pixel color tolerance, 0 to 1. Default 0.1."),ignore:ue.array(ue.string()).optional().describe('With baseline: leave these out of the comparison, by testID (e.g. ["live-map", "ad-banner"]), for parts that change on their own.'),ignoreAreas:ue.array(ue.object({x:ue.number(),y:ue.number(),width:ue.number(),height:ue.number()})).optional().describe("With baseline: areas to leave out, as fractions of the screen (0 to 1)."),deviceId:L}},async t=>{try{if(t.compareTo){if(t.query||t.nativeTag!=null)return Ye("compareTo uses the whole screen. Leave query and nativeTag out.");if(t.baseline||t.updateBaseline!=null||t.steady!=null||t.threshold!=null||t.ignore!=null||t.ignoreAreas!=null)return Ye("compareTo shows both pictures. Use baseline for diff options.");return await Oh(n,t.compareTo,t.deviceId,t.serial)}if(t.baseline){if(t.query||t.nativeTag!=null)return Ye("baseline works on the whole screen: leave query and nativeTag out.");return await jh(n,t.baseline,{deviceId:t.deviceId,serial:t.serial,update:t.updateBaseline,threshold:t.threshold,steady:t.steady,ignore:t.ignore,ignoreAreas:t.ignoreAreas})}if(!t.query&&t.nativeTag==null){const{id:v,host:$,online:T}=await ge(n,t.deviceId,{serial:t.serial},{allowOffline:true});let k=Buffer.alloc(0);const D=await Vn(n,v,T,async()=>k=await on($));const E={length:D.covered};const I=await Ct(D.png);const R=$.platform==="ios-device"&&Wd(k)?` ${zd}`:"";return{content:[{type:"image",data:I.data,mimeType:I.mimeType},{type:"text",text:`Whole screen of ${Rt($)}, ${I.width}\xD7${I.height}px.${E.length?` ${E.length} element(s) showing a secret were blacked out.`:""}${R}`}]}}const i=n.requireProDevice(t.deviceId);await n.callAction(qt,"setSilentTracking",{enabled:true},i).catch(()=>{});let r=t.nativeTag;const o=r==null&&t.query?await Eh(n,i,t.query):null;if(o)r=o.tag;const a=await n.callAction(qt,"locateComponent",{query:r==null?t.query:void 0,nativeTag:r,scrollIntoView:t.scrollIntoView??true,margin:Th},i);let s=a;if(!s.matched&&(s.reason==="no-renders"||s.reason==="no-match")){const v=await n.callAction(qt,"describeScreen",{},i).catch(()=>null);const $=v?.elements?yh(v.elements,t.query,r):void 0;if($?.frame&&v?.screen){s={matched:true,live:true,rect:$h($.frame,v.screen),screen:v.screen,nativeTag:$.nativeTag??void 0,componentName:$.name,testID:$.testID}}else if(s.reason==="no-renders"){s={matched:false,reason:"no-match",candidates:s.candidates}}}if(!s.matched||!s.rect||!s.screen){return Ye(Ih(s,t.query)+Ba(s.candidates))}if(r==null&&t.query){const v=t.query.toLowerCase();const $=s.candidates?.find(I=>I.nativeTag===s.nativeTag);const T=await n.callAction(qt,"describeScreen",{},i,{timeoutMs:4e3}).catch(()=>null);const k=T?.elements?.find(I=>I.nativeTag===s.nativeTag);const D=[s.testID,s.nativeID,s.componentName,$?.testID,$?.nativeID,$?.componentName,k?.testID,k?.nativeID,k?.text,k?.label].filter(Boolean).map(I=>String(I).toLowerCase());const E=D.some(I=>I.includes(v)||I.length>3&&v.includes(I));if(!E){return Ye(`"${t.query}" is not on the current screen (the closest component was ${s.componentName??"unknown"}${s.testID?` [${s.testID}]`:""}). Navigate to it first, or pass a testID from describe_screen.`+Ba(s.candidates))}}const{host:c}=await ge(n,i);if(c.platform!=="ios")return Ye("Cropping one component works on the iOS Simulator. Leave query out for a whole-screen picture.");let l={ok:false};const d=await Vn(n,i,true,async()=>{l=await Bo(c.udid);return l.ok&&l.srcPath?Ve.readFile(l.srcPath):Buffer.alloc(0)});if(d.covered&&l.ok&&l.srcPath)await Ve.writeFile(l.srcPath,d.png);if(!l.ok||!l.srcPath||!l.width||!l.height){return Ye(l.error??"Screenshot capture failed.")}const u=l.width/s.screen.width;const h=s.rect;const b=Dh;const f=await Ed(l.srcPath,{width:l.width,height:l.height},{x:(h.x-b)*u,y:(h.y-b)*u,width:(h.width+b*2)*u,height:(h.height+b*2)*u});if(!f.ok||!f.buffer){return Ye(f.error??"Crop failed.")}const w=s.componentName||o?.name||"component";const g=s.testID??o?.testID;const m=g?` (testID: "${g}")`:"";const p=s.inView===false?"\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge.":"";const y=`Captured "${w}"${m} \u2014 ${f.width}\xD7${f.height}px.${p}`;return{content:[{type:"image",data:f.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:y}]}}catch(i){return Ye(vr(i))}})}async function Vn(e,n,t,i){const r=typeof i==="function"?i:async()=>i;const o=ou();if(!o.length)return{png:await r(),covered:0};const a=t?await La(e,n,o):null;const s=await r();const c=t?await La(e,n,o):null;if(a===null||c===null){if(t||Ji().length){throw new Error("Buoy can't read the app's screen to hide secrets right now, so no picture was taken. Try again in a moment.")}return{png:s,covered:0}}const l=[...a,...c];if(!s.length)return{png:s,covered:0};return{png:l.length?Ah(s,l):s,covered:Math.max(a.length,c.length)}}async function La(e,n,t){const i=await e.callAction(qt,"describeScreen",{includeHidden:false,includeBuoy:true,fullText:true},n,{timeoutMs:4e3}).catch(()=>null);if(!i?.screen)return null;const r=Ji().length>0;const o=[];for(const a of i.elements??[]){const s=a.value;const c=[a.text,a.label,s].filter(h=>h!=null&&h!=="").map(String);const l=c.some(h=>t.some(b=>Mh(h,b)));const d=r&&a.control==="text"&&s===void 0;if(!l&&!d)continue;const u=a.frame?.width&&a.frame.height?Yn(a,i.screen):void 0;if(!u)return null;o.push({...u,pixels:0})}return o}function Mh(e,n){const t=e.replace(/\s+/g," ");const i=n.replace(/\s+/g," ").trim();if(t.includes(i))return true;const r=t.replace(/(…|\.\.\.)$/,"");for(let o=i.length-1;o>=Et;o--)if(r.endsWith(i.slice(0,o)))return true;return false}function Ah(e,n){const t=Fa.PNG.sync.read(e);for(const i of n){const r=Math.max(0,Math.floor((i.x-.01)*t.width));const o=Math.min(t.width,Math.ceil((i.x+i.width+.01)*t.width));const a=Math.max(0,Math.floor((i.y-.005)*t.height));const s=Math.min(t.height,Math.ceil((i.y+i.height+.005)*t.height));for(let c=a;c<s;c++){for(let l=r;l<o;l++){const d=(c*t.width+l)*4;t.data[d]=t.data[d+1]=t.data[d+2]=0;t.data[d+3]=255}}}return Fa.PNG.sync.write(t)}async function Eh(e,n,t){const i=await e.callAction(qt,"describeScreen",{},n,{timeoutMs:4e3}).catch(()=>null);const r=i?.elements??[];const o=l=>({tag:l.nativeTag,name:l.name,testID:l.testID});const a=r.filter(l=>l.testID===t&&l.nativeTag!=null);if(a.length===1)return o(a[0]);const s=t.trim().toLowerCase();const c=r.filter(l=>(l.text??l.label??"").trim().toLowerCase()===s&&l.nativeTag!=null);return c.length===1?o(c[0]):null}async function Ch(e){const n=32*1024*1024;const t=r=>{if(!r.isFile()||r.size>n)throw new Error("Use a PNG file under 32 MB.")};t(await Ve.stat(e));const i=await Ve.open(e,Na.O_RDONLY|Na.O_NONBLOCK);try{t(await i.stat());const r=[];let o=0;while(o<=n){const l=Buffer.alloc(Math.min(64*1024,n+1-o));const{bytesRead:d}=await i.read(l,0,l.length,null);if(!d)break;o+=d;if(o>n)throw new Error("Use a PNG file under 32 MB.");r.push(l.subarray(0,d))}const a=Buffer.concat(r,o);if(a.length<24||a.subarray(0,8).toString("hex")!=="89504e470d0a1a0a")throw new Error("The saved file must be a PNG.");const s=a.readUInt32BE(16),c=a.readUInt32BE(20);if(!s||!c||s*c>32e6)throw new Error("Use a PNG with at most 32 million pixels.");return a}finally{await i.close()}}async function Oh(e,n,t,i){const r=n==="~"?Pa.homedir():n.startsWith("~/")?un.join(Pa.homedir(),n.slice(2)):n;const o=await Ch(un.resolve(process.env.BUOY_PROJECT_ROOT||process.cwd(),r));const{id:a,host:s,online:c}=await ge(e,t,{serial:i},{allowOffline:true});const l=await Vn(e,a,c,()=>on(s));const d=await Ct(o);const u=await Ct(l.png);const h=await Rh(e,a,c,o,l.png);return{content:[...h?[{type:"text",text:h}]:[],{type:"text",text:`Saved file: ${n} (${d.width}x${d.height}).`},{type:"image",data:d.data,mimeType:d.mimeType},{type:"text",text:`Current screen (${u.width}x${u.height}). Compare it with the saved file above.${l.covered?" Fields with secrets are hidden in this screen.":""}`},{type:"image",data:u.data,mimeType:u.mimeType}]}}async function Rh(e,n,t,i,r){const o=.06;let a;try{a=br(i,r,{ignoreTop:o})}catch{return null}if(!a.boxes.length)return"Measured: no change outside the status bar.";let s=[];if(t){try{const l=await e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:3e3,silent:true});const d=l?.screen??{width:1,height:1};s=(l?.elements??[]).map(u=>({name:u.name,words:u.text??u.label,testID:u.testID,frame:Yn(u,d)}))}catch{}}const c=a.boxes.slice(0,3).map(l=>{const d=qa(l,s)[0];const u=xh(i,r,l,o);return`- ${Oa(l)}${d?`, ${d}`:""}${u?`: main color ${u.before} before, ${u.after} now`:""}.`});return[`Measured: ${a.percent.toFixed(1)}% of the screen changed, in ${a.boxes.length} area${a.boxes.length===1?"":"s"}.`,...c].join("\n")}function Ph(e){const n=e.trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"")||"screen";return un.join(process.env.BUOY_PROJECT_ROOT||process.cwd(),".buoy","baselines",`${n}.png`)}function Yn(e,n){if(e.frame&&e.frame.width&&e.frame.height){const t=e.frame.x+e.frame.width>1.5||e.frame.y+e.frame.height>1.5;return t?{x:e.frame.x/n.width,y:e.frame.y/n.height,width:e.frame.width/n.width,height:e.frame.height/n.height}:{...e.frame}}if(e.tap)return{x:e.tap.x-.01,y:e.tap.y-.01,width:.02,height:.02};return void 0}var vt=e=>`${e.name??"View"}${e.words?` "${e.words.length>40?`${e.words.slice(0,40)}\u2026`:e.words}"`:""}${e.testID?` [${e.testID}]`:""}`;function qa(e,n){const t=[];for(const i of n){const r=i.frame;if(!r)continue;const o=Math.max(0,Math.min(e.x+e.width,r.x+r.width)-Math.max(e.x,r.x));const a=Math.max(0,Math.min(e.y+e.height,r.y+r.height)-Math.max(e.y,r.y));const s=r.width*r.height;if(!s||o*a/s<.5)continue;t.push({label:vt(i),area:s,words:!!i.words})}return[...new Set(t.sort((i,r)=>Number(r.words)-Number(i.words)||i.area-r.area).map(i=>i.label))].slice(0,3)}function Nh(e,n){const t=u=>u.testID?`id:${u.testID}`:`${u.name??""}@${u.frame?`${Math.round(u.frame.x*40)},${Math.round(u.frame.y*40)}`:"?"}`;const i=new Map(e.filter(u=>u.words).map(u=>[t(u),u]));const r=new Map(n.filter(u=>u.words).map(u=>[t(u),u]));const o=new Set([...i.values()].map(u=>u.words));const a=new Set([...r.values()].map(u=>u.words));const s=[];for(const[u,h]of r){const b=i.get(u);if(b){if(b.words!==h.words)s.push(`changed: ${vt({...h,words:void 0})} "${b.words}" \u2192 "${h.words}"`)}else if(!o.has(h.words))s.push(`appeared: ${vt(h)}`)}for(const[u,h]of i)if(!r.has(u)&&!a.has(h.words))s.push(`gone: ${vt(h)}`);const c=[...i.entries()].filter(([u,h])=>!r.has(u)&&!a.has(h.words)).map(([,u])=>u);const l=[...r.entries()].filter(([u,h])=>!i.has(u)&&!o.has(h.words)).map(([,u])=>u);const d=(u,h)=>!!u&&!!h&&u.x<h.x+h.width&&h.x<u.x+u.width&&u.y<h.y+h.height&&h.y<u.y+u.height;for(const u of l){const h=c.find(f=>f.name===u.name&&d(f.frame,u.frame));if(!h)continue;c.splice(c.indexOf(h),1);const b=new Set([`appeared: ${vt(u)}`,`gone: ${vt(h)}`]);for(let f=s.length-1;f>=0;f--)if(b.has(s[f]))s.splice(f,1);s.push(`changed: ${vt({...u,words:void 0})} "${h.words}" \u2192 "${u.words}"`)}return s.slice(0,12)}async function Fh(e,n){try{const[t,i,r]=await Promise.all([e.callAction("highlight-updates","describeScreen",{includeBuoy:true},n,{timeoutMs:4e3}),e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:4e3}),e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:2e3}).catch(()=>null)]);const o=i?.screen??t?.screen;if(!o)return null;const a=new Set((i?.elements??[]).map(d=>d.nativeTag));const s=[];for(const d of t?.elements??[]){if(a.has(d.nativeTag))continue;const u=Yn(d,o);if(u)s.push({x:u.x-.01,y:u.y-.01,width:u.width+.02,height:u.height+.02,pixels:0})}const c=(i?.elements??[]).map(d=>{const u=xe((d.text??d.label??"").replace(/[\s\u200b\u00a0]+/g," ").trim());return{name:d.name,words:u||void 0,testID:d.testID?xe(d.testID):void 0,frame:Yn(d,o)}});const l=r?.path??r?.pathname;return{elements:c,masks:s,route:l?xe(l):void 0}}catch{return null}}async function Bh(e,n,t,i){let r;let o;let a;try{await e.callAction("clock","freeze",{time:t},n);await new Promise(c=>setTimeout(c,1300));r=await i()}catch(c){o=c}finally{try{await e.callAction("clock","reset",{},n)}catch(c){a=c}}const s=a?` WARNING: putting the app's clock back failed (${vr(a)}); run clock_action reset.`:"";if(o)throw new Error(`${vr(o)}${s}`);return{result:r,note:` The app's clock was frozen at ${new Date(t).toISOString()} for the capture.${s}`}}async function jh(...e){return(await Ua(...e)).result}async function Ua(e,n,t){const{id:i,host:r}=await ge(e,t.deviceId,{serial:t.serial});const o=Ph(n);const a=o.replace(/\.png$/,".json");const s=un.relative(process.env.BUOY_PROJECT_ROOT||process.cwd(),o);let c=null;try{c=await Ve.readFile(o)}catch{}const l=!c||!!t.update;const d=l?{}:JSON.parse(await Ve.readFile(a,"utf8").catch(()=>"{}"));const u=Rt(r);const h=e.connectedDevices().find(B=>B.deviceId===i);const b=h?.tools.some(B=>B.toolId==="clock")??false;const f=b&&fe(h);const w=t.steady??f;let g="";const m=async()=>{const{result:B,steadied:z}=await Ou(r,async()=>({png:(await Vn(e,i,true,()=>on(r))).png,screen:await Fh(e,i)}));return{...B,steadied:z}};let p;if(w&&f){const B=await e.callAction("clock","getState",{},i).catch(()=>null);if(B?.mode==="real"){const z=l||!d.steadyTime?Math.floor(Date.now()/6e4)*6e4:d.steadyTime;d.steadyTime=z;const re=await Bh(e,i,z,m);p=re.result;g+=re.note}else{p=await m();g+=" The app's clock was already overridden, so it was left as it was."}}else{p=await m();if(w&&!f)g+=b?" (steady freezes the app clock, which needs Buoy Pro; times on screen may differ.)":" (steady needs the Clock tool in the app; times on screen may differ.)"}const y={width:p.png.readUInt32BE(16),height:p.png.readUInt32BE(20)};if(l){await Ve.mkdir(un.dirname(o),{recursive:true});await Ve.writeFile(o,p.png);const B={...d,device:u,size:y,route:p.screen?.route,elements:p.screen?.elements};await Ve.writeFile(a,JSON.stringify(B,null,1));const z=await Ct(p.png);const re=`${c?"Replaced":"Saved"} baseline "${n}"`;return{outcome:{saved:true,changed:false,percent:0,texts:0,summary:`${re} (first run)`},result:{content:[{type:"image",data:z.data,mimeType:z.mimeType},{type:"text",text:`${c?"Replaced":"Saved"} baseline "${n}" (${s}) from ${u}${p.screen?.route?` on ${p.screen.route}`:""}. Call again with the same baseline to compare.${g}`}]}}}const v=[];if(d.device&&d.device!==u)v.push(`The baseline was saved on ${d.device}; this is ${u}.`);if(d.route&&p.screen?.route&&d.route!==p.screen.route)v.push(`The baseline was on ${d.route}; the app is now on ${p.screen.route}.`);const $=[...t.ignoreAreas??[]];const T=new Set(t.ignore??[]);const k=new Set(T);for(const B of[...d.elements??[],...p.screen?.elements??[]]){if(!B.testID||!T.has(B.testID)||!B.frame)continue;k.delete(B.testID);$.push({x:B.frame.x-.005,y:B.frame.y-.005,width:B.frame.width+.01,height:B.frame.height+.01})}if(k.size)v.push(`Not on screen, so not left out: ${[...k].join(", ")}.`);const D=r.platform==="android"?[]:[{x:.28,y:0,width:.44,height:.055}];const E=br(c,p.png,{threshold:t.threshold,masks:[...p.screen?.masks??[],...$,...D],ignoreTop:p.steadied?0:.065});const I=B=>!!B&&$.some(z=>B.x+B.width/2>=z.x&&B.x+B.width/2<=z.x+z.width&&B.y+B.height/2>=z.y&&B.y+B.height/2<=z.y+z.height);const R=B=>(!B.testID||!T.has(B.testID))&&!I(B.frame);const _=d.elements&&p.screen?Nh(d.elements.filter(R),p.screen.elements.filter(R)):[];const S=[p.steadied?"":"status bar",p.screen?.masks.length?"Buoy's overlay":"",$.length?"the parts you asked to ignore":""].filter(Boolean);const A=S.join(" and ");const O=A?`; ${A} left out`:"";const F=v.length?`
60
+ ${n.join("\n")}`}function Ka(e,n){e.registerTool("screenshot_component",{_meta:Ge(),description:"Take a screenshot of the app. Pass compareTo with a saved PNG path to view both screens. It first says what changed, measured: where, which element, and the exact colors before and now. Then the saved file, then the live screen. Leave query and nativeTag out for the whole screen. To crop a React Native view on an iOS Sim, pass query or nativeTag. The tool may scroll that view into sight. Use describe_screen to read text or find controls. Pictures use more tokens. Run the MCP on the device host.",inputSchema:{compareTo:ue.string().min(1).optional().describe("A saved PNG file to view next to this screen. Use a full path or a path from the project root. The file stays as it is."),query:ue.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:ue.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:ue.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),serial:ue.string().optional().describe("Android: the adb serial, when several devices are connected."),baseline:ue.string().optional().describe('Whole screen only: a name like "home". The first call saves the screen as that baseline (in .buoy/baselines); later calls compare against it and report what changed, with a diff picture.'),updateBaseline:ue.boolean().optional().describe("With baseline: save the current screen as the new baseline instead of comparing."),steady:ue.boolean().optional().describe("With baseline: freeze the app's clock at the baseline's moment while capturing, so countdowns and times match (needs @buoy-gg/clock). Default true when the app has the clock tool."),threshold:ue.number().min(0).max(1).optional().describe("With baseline: per-pixel color tolerance, 0 to 1. Default 0.1."),ignore:ue.array(ue.string()).optional().describe('With baseline: leave these out of the comparison, by testID (e.g. ["live-map", "ad-banner"]), for parts that change on their own.'),ignoreAreas:ue.array(ue.object({x:ue.number(),y:ue.number(),width:ue.number(),height:ue.number()})).optional().describe("With baseline: areas to leave out, as fractions of the screen (0 to 1)."),deviceId:L}},async t=>{try{if(t.compareTo){if(t.query||t.nativeTag!=null)return Ze("compareTo uses the whole screen. Leave query and nativeTag out.");if(t.baseline||t.updateBaseline!=null||t.steady!=null||t.threshold!=null||t.ignore!=null||t.ignoreAreas!=null)return Ze("compareTo shows both pictures. Use baseline for diff options.");return await Yh(n,t.compareTo,t.deviceId,t.serial)}if(t.baseline){if(t.query||t.nativeTag!=null)return Ze("baseline works on the whole screen: leave query and nativeTag out.");return await np(n,t.baseline,{deviceId:t.deviceId,serial:t.serial,update:t.updateBaseline,threshold:t.threshold,steady:t.steady,ignore:t.ignore,ignoreAreas:t.ignoreAreas})}if(!t.query&&t.nativeTag==null){const{id:v,host:$,online:T}=await ge(n,t.deviceId,{serial:t.serial},{allowOffline:true});let k=Buffer.alloc(0);const D=await oi(n,v,T,async()=>k=await un($));const C={length:D.covered};const I=await Nt(D.png);const R=$.platform==="ios-device"&&su(k)?` ${au}`:"";return{content:[{type:"image",data:I.data,mimeType:I.mimeType},{type:"text",text:`Whole screen of ${Bt($)}, ${I.width}\xD7${I.height}px.${C.length?` ${C.length} element(s) showing a secret were blacked out.`:""}${R}`}]}}const i=n.requireProDevice(t.deviceId);await n.callAction(Ht,"setSilentTracking",{enabled:true},i).catch(()=>{});let r=t.nativeTag;const o=r==null&&t.query?await Kh(n,i,t.query):null;if(o)r=o.tag;const a=await n.callAction(Ht,"locateComponent",{query:r==null?t.query:void 0,nativeTag:r,scrollIntoView:t.scrollIntoView??true,margin:zh},i);let s=a;if(!s.matched&&(s.reason==="no-renders"||s.reason==="no-match")){const v=await n.callAction(Ht,"describeScreen",{},i).catch(()=>null);const $=v?.elements?Fh(v.elements,t.query,r):void 0;if($?.frame&&v?.screen){s={matched:true,live:true,rect:Bh($.frame,v.screen),screen:v.screen,nativeTag:$.nativeTag??void 0,componentName:$.name,testID:$.testID}}else if(s.reason==="no-renders"){s={matched:false,reason:"no-match",candidates:s.candidates}}}if(!s.matched||!s.rect||!s.screen){return Ze(Hh(s,t.query)+Ga(s.candidates))}if(r==null&&t.query){const v=t.query.toLowerCase();const $=s.candidates?.find(I=>I.nativeTag===s.nativeTag);const T=await n.callAction(Ht,"describeScreen",{},i,{timeoutMs:4e3}).catch(()=>null);const k=T?.elements?.find(I=>I.nativeTag===s.nativeTag);const D=[s.testID,s.nativeID,s.componentName,$?.testID,$?.nativeID,$?.componentName,k?.testID,k?.nativeID,k?.text,k?.label].filter(Boolean).map(I=>String(I).toLowerCase());const C=D.some(I=>I.includes(v)||I.length>3&&v.includes(I));if(!C){return Ze(`"${t.query}" is not on the current screen (the closest component was ${s.componentName??"unknown"}${s.testID?` [${s.testID}]`:""}). Navigate to it first, or pass a testID from describe_screen.`+Ga(s.candidates))}}const{host:c}=await ge(n,i);if(c.platform!=="ios")return Ze("Cropping one component works on the iOS Simulator. Leave query out for a whole-screen picture.");let l={ok:false};const d=await oi(n,i,true,async()=>{l=await Go(c.udid);return l.ok&&l.srcPath?Xe.readFile(l.srcPath):Buffer.alloc(0)});if(d.covered&&l.ok&&l.srcPath)await Xe.writeFile(l.srcPath,d.png);if(!l.ok||!l.srcPath||!l.width||!l.height){return Ze(l.error??"Screenshot capture failed.")}const u=l.width/s.screen.width;const h=s.rect;const b=Wh;const f=await Kd(l.srcPath,{width:l.width,height:l.height},{x:(h.x-b)*u,y:(h.y-b)*u,width:(h.width+b*2)*u,height:(h.height+b*2)*u});if(!f.ok||!f.buffer){return Ze(f.error??"Crop failed.")}const w=s.componentName||o?.name||"component";const g=s.testID??o?.testID;const m=g?` (testID: "${g}")`:"";const p=s.inView===false?"\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge.":"";const y=`Captured "${w}"${m} \u2014 ${f.width}\xD7${f.height}px.${p}`;return{content:[{type:"image",data:f.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:y}]}}catch(i){return Ze(Dr(i))}})}async function oi(e,n,t,i){const r=typeof i==="function"?i:async()=>i;const o=ku();if(!o.length)return{png:await r(),covered:0};const a=t?await Va(e,n,o):null;const s=await r();const c=t?await Va(e,n,o):null;if(a===null||c===null){if(t||er().length){throw new Error("Buoy can't read the app's screen to hide secrets right now, so no picture was taken. Try again in a moment.")}return{png:s,covered:0}}const l=[...a,...c];if(!s.length)return{png:s,covered:0};return{png:l.length?Gh(s,l):s,covered:Math.max(a.length,c.length)}}async function Va(e,n,t){const i=await e.callAction(Ht,"describeScreen",{includeHidden:false,includeBuoy:true,fullText:true},n,{timeoutMs:4e3}).catch(()=>null);if(!i?.screen)return null;const r=er().length>0;const o=[];for(const a of i.elements??[]){const s=a.value;const c=[a.text,a.label,s].filter(h=>h!=null&&h!=="").map(String);const l=c.some(h=>t.some(b=>Jh(h,b)));const d=r&&a.control==="text"&&s===void 0;if(!l&&!d)continue;const u=a.frame?.width&&a.frame.height?ai(a,i.screen):void 0;if(!u)return null;o.push({...u,pixels:0})}return o}function Jh(e,n){const t=e.replace(/\s+/g," ");const i=n.replace(/\s+/g," ").trim();if(t.includes(i))return true;const r=t.replace(/(…|\.\.\.)$/,"");for(let o=i.length-1;o>=Pt;o--)if(r.endsWith(i.slice(0,o)))return true;return false}function Gh(e,n){const t=Ja.PNG.sync.read(e);for(const i of n){const r=Math.max(0,Math.floor((i.x-.01)*t.width));const o=Math.min(t.width,Math.ceil((i.x+i.width+.01)*t.width));const a=Math.max(0,Math.floor((i.y-.005)*t.height));const s=Math.min(t.height,Math.ceil((i.y+i.height+.005)*t.height));for(let c=a;c<s;c++){for(let l=r;l<o;l++){const d=(c*t.width+l)*4;t.data[d]=t.data[d+1]=t.data[d+2]=0;t.data[d+3]=255}}}return Ja.PNG.sync.write(t)}async function Kh(e,n,t){const i=await e.callAction(Ht,"describeScreen",{},n,{timeoutMs:4e3}).catch(()=>null);const r=i?.elements??[];const o=l=>({tag:l.nativeTag,name:l.name,testID:l.testID});const a=r.filter(l=>l.testID===t&&l.nativeTag!=null);if(a.length===1)return o(a[0]);const s=t.trim().toLowerCase();const c=r.filter(l=>(l.text??l.label??"").trim().toLowerCase()===s&&l.nativeTag!=null);return c.length===1?o(c[0]):null}async function Vh(e){const n=32*1024*1024;const t=r=>{if(!r.isFile()||r.size>n)throw new Error("Use a PNG file under 32 MB.")};t(await Xe.stat(e));const i=await Xe.open(e,Ha.O_RDONLY|Ha.O_NONBLOCK);try{t(await i.stat());const r=[];let o=0;while(o<=n){const l=Buffer.alloc(Math.min(64*1024,n+1-o));const{bytesRead:d}=await i.read(l,0,l.length,null);if(!d)break;o+=d;if(o>n)throw new Error("Use a PNG file under 32 MB.");r.push(l.subarray(0,d))}const a=Buffer.concat(r,o);if(a.length<24||a.subarray(0,8).toString("hex")!=="89504e470d0a1a0a")throw new Error("The saved file must be a PNG.");const s=a.readUInt32BE(16),c=a.readUInt32BE(20);if(!s||!c||s*c>32e6)throw new Error("Use a PNG with at most 32 million pixels.");return a}finally{await i.close()}}async function Yh(e,n,t,i){const r=n==="~"?Wa.homedir():n.startsWith("~/")?gn.join(Wa.homedir(),n.slice(2)):n;const o=await Vh(gn.resolve(process.env.BUOY_PROJECT_ROOT||process.cwd(),r));const{id:a,host:s,online:c}=await ge(e,t,{serial:i},{allowOffline:true});const l=await oi(e,a,c,()=>un(s));const d=await Nt(o);const u=await Nt(l.png);const h=await Xh(e,a,c,o,l.png);return{content:[...h?[{type:"text",text:h}]:[],{type:"text",text:`Saved file: ${n} (${d.width}x${d.height}).`},{type:"image",data:d.data,mimeType:d.mimeType},{type:"text",text:`Current screen (${u.width}x${u.height}). Compare it with the saved file above.${l.covered?" Fields with secrets are hidden in this screen.":""}`},{type:"image",data:u.data,mimeType:u.mimeType}]}}async function Xh(e,n,t,i,r){const o=.06;let a;try{a=Tr(i,r,{ignoreTop:o})}catch{return null}if(!a.boxes.length)return"Measured: no change outside the status bar.";let s=[];if(t){try{const l=await e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:3e3,silent:true});const d=l?.screen??{width:1,height:1};s=(l?.elements??[]).map(u=>({name:u.name,words:u.text??u.label,testID:u.testID,frame:ai(u,d)}))}catch{}}const c=a.boxes.slice(0,3).map(l=>{const d=Ya(l,s)[0];const u=Uh(i,r,l,o);return`- ${Ua(l)}${d?`, ${d}`:""}${u?`: main color ${u.before} before, ${u.after} now`:""}.`});return[`Measured: ${a.percent.toFixed(1)}% of the screen changed, in ${a.boxes.length} area${a.boxes.length===1?"":"s"}.`,...c].join("\n")}function Zh(e){const n=e.trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"")||"screen";return gn.join(process.env.BUOY_PROJECT_ROOT||process.cwd(),".buoy","baselines",`${n}.png`)}function ai(e,n){if(e.frame&&e.frame.width&&e.frame.height){const t=e.frame.x+e.frame.width>1.5||e.frame.y+e.frame.height>1.5;return t?{x:e.frame.x/n.width,y:e.frame.y/n.height,width:e.frame.width/n.width,height:e.frame.height/n.height}:{...e.frame}}if(e.tap)return{x:e.tap.x-.01,y:e.tap.y-.01,width:.02,height:.02};return void 0}var kt=e=>`${e.name??"View"}${e.words?` "${e.words.length>40?`${e.words.slice(0,40)}\u2026`:e.words}"`:""}${e.testID?` [${e.testID}]`:""}`;function Ya(e,n){const t=[];for(const i of n){const r=i.frame;if(!r)continue;const o=Math.max(0,Math.min(e.x+e.width,r.x+r.width)-Math.max(e.x,r.x));const a=Math.max(0,Math.min(e.y+e.height,r.y+r.height)-Math.max(e.y,r.y));const s=r.width*r.height;if(!s||o*a/s<.5)continue;t.push({label:kt(i),area:s,words:!!i.words})}return[...new Set(t.sort((i,r)=>Number(r.words)-Number(i.words)||i.area-r.area).map(i=>i.label))].slice(0,3)}function Qh(e,n){const t=u=>u.testID?`id:${u.testID}`:`${u.name??""}@${u.frame?`${Math.round(u.frame.x*40)},${Math.round(u.frame.y*40)}`:"?"}`;const i=new Map(e.filter(u=>u.words).map(u=>[t(u),u]));const r=new Map(n.filter(u=>u.words).map(u=>[t(u),u]));const o=new Set([...i.values()].map(u=>u.words));const a=new Set([...r.values()].map(u=>u.words));const s=[];for(const[u,h]of r){const b=i.get(u);if(b){if(b.words!==h.words)s.push(`changed: ${kt({...h,words:void 0})} "${b.words}" \u2192 "${h.words}"`)}else if(!o.has(h.words))s.push(`appeared: ${kt(h)}`)}for(const[u,h]of i)if(!r.has(u)&&!a.has(h.words))s.push(`gone: ${kt(h)}`);const c=[...i.entries()].filter(([u,h])=>!r.has(u)&&!a.has(h.words)).map(([,u])=>u);const l=[...r.entries()].filter(([u,h])=>!i.has(u)&&!o.has(h.words)).map(([,u])=>u);const d=(u,h)=>!!u&&!!h&&u.x<h.x+h.width&&h.x<u.x+u.width&&u.y<h.y+h.height&&h.y<u.y+u.height;for(const u of l){const h=c.find(f=>f.name===u.name&&d(f.frame,u.frame));if(!h)continue;c.splice(c.indexOf(h),1);const b=new Set([`appeared: ${kt(u)}`,`gone: ${kt(h)}`]);for(let f=s.length-1;f>=0;f--)if(b.has(s[f]))s.splice(f,1);s.push(`changed: ${kt({...u,words:void 0})} "${h.words}" \u2192 "${u.words}"`)}return s.slice(0,12)}async function ep(e,n){try{const[t,i,r]=await Promise.all([e.callAction("highlight-updates","describeScreen",{includeBuoy:true},n,{timeoutMs:4e3}),e.callAction("highlight-updates","describeScreen",{},n,{timeoutMs:4e3}),e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:2e3}).catch(()=>null)]);const o=i?.screen??t?.screen;if(!o)return null;const a=new Set((i?.elements??[]).map(d=>d.nativeTag));const s=[];for(const d of t?.elements??[]){if(a.has(d.nativeTag))continue;const u=ai(d,o);if(u)s.push({x:u.x-.01,y:u.y-.01,width:u.width+.02,height:u.height+.02,pixels:0})}const c=(i?.elements??[]).map(d=>{const u=Te((d.text??d.label??"").replace(/[\s\u200b\u00a0]+/g," ").trim());return{name:d.name,words:u||void 0,testID:d.testID?Te(d.testID):void 0,frame:ai(d,o)}});const l=r?.path??r?.pathname;return{elements:c,masks:s,route:l?Te(l):void 0}}catch{return null}}async function tp(e,n,t,i){let r;let o;let a;try{await e.callAction("clock","freeze",{time:t},n);await new Promise(c=>setTimeout(c,1300));r=await i()}catch(c){o=c}finally{try{await e.callAction("clock","reset",{},n)}catch(c){a=c}}const s=a?` WARNING: putting the app's clock back failed (${Dr(a)}); run clock_action reset.`:"";if(o)throw new Error(`${Dr(o)}${s}`);return{result:r,note:` The app's clock was frozen at ${new Date(t).toISOString()} for the capture.${s}`}}async function np(...e){return(await Xa(...e)).result}async function Xa(e,n,t){const{id:i,host:r}=await ge(e,t.deviceId,{serial:t.serial});const o=Zh(n);const a=o.replace(/\.png$/,".json");const s=gn.relative(process.env.BUOY_PROJECT_ROOT||process.cwd(),o);let c=null;try{c=await Xe.readFile(o)}catch{}const l=!c||!!t.update;const d=l?{}:JSON.parse(await Xe.readFile(a,"utf8").catch(()=>"{}"));const u=Bt(r);const h=e.connectedDevices().find(B=>B.deviceId===i);const b=h?.tools.some(B=>B.toolId==="clock")??false;const f=b&&fe(h);const w=t.steady??f;let g="";const m=async()=>{const{result:B,steadied:z}=await Yu(r,async()=>({png:(await oi(e,i,true,()=>un(r))).png,screen:await ep(e,i)}));return{...B,steadied:z}};let p;if(w&&f){const B=await e.callAction("clock","getState",{},i).catch(()=>null);if(B?.mode==="real"){const z=l||!d.steadyTime?Math.floor(Date.now()/6e4)*6e4:d.steadyTime;d.steadyTime=z;const re=await tp(e,i,z,m);p=re.result;g+=re.note}else{p=await m();g+=" The app's clock was already overridden, so it was left as it was."}}else{p=await m();if(w&&!f)g+=b?" (steady freezes the app clock, which needs Buoy Pro; times on screen may differ.)":" (steady needs the Clock tool in the app; times on screen may differ.)"}const y={width:p.png.readUInt32BE(16),height:p.png.readUInt32BE(20)};if(l){await Xe.mkdir(gn.dirname(o),{recursive:true});await Xe.writeFile(o,p.png);const B={...d,device:u,size:y,route:p.screen?.route,elements:p.screen?.elements};await Xe.writeFile(a,JSON.stringify(B,null,1));const z=await Nt(p.png);const re=`${c?"Replaced":"Saved"} baseline "${n}"`;return{outcome:{saved:true,changed:false,percent:0,texts:0,summary:`${re} (first run)`},result:{content:[{type:"image",data:z.data,mimeType:z.mimeType},{type:"text",text:`${c?"Replaced":"Saved"} baseline "${n}" (${s}) from ${u}${p.screen?.route?` on ${p.screen.route}`:""}. Call again with the same baseline to compare.${g}`}]}}}const v=[];if(d.device&&d.device!==u)v.push(`The baseline was saved on ${d.device}; this is ${u}.`);if(d.route&&p.screen?.route&&d.route!==p.screen.route)v.push(`The baseline was on ${d.route}; the app is now on ${p.screen.route}.`);const $=[...t.ignoreAreas??[]];const T=new Set(t.ignore??[]);const k=new Set(T);for(const B of[...d.elements??[],...p.screen?.elements??[]]){if(!B.testID||!T.has(B.testID)||!B.frame)continue;k.delete(B.testID);$.push({x:B.frame.x-.005,y:B.frame.y-.005,width:B.frame.width+.01,height:B.frame.height+.01})}if(k.size)v.push(`Not on screen, so not left out: ${[...k].join(", ")}.`);const D=r.platform==="android"?[]:[{x:.28,y:0,width:.44,height:.055}];const C=Tr(c,p.png,{threshold:t.threshold,masks:[...p.screen?.masks??[],...$,...D],ignoreTop:p.steadied?0:.065});const I=B=>!!B&&$.some(z=>B.x+B.width/2>=z.x&&B.x+B.width/2<=z.x+z.width&&B.y+B.height/2>=z.y&&B.y+B.height/2<=z.y+z.height);const R=B=>(!B.testID||!T.has(B.testID))&&!I(B.frame);const _=d.elements&&p.screen?Qh(d.elements.filter(R),p.screen.elements.filter(R)):[];const S=[p.steadied?"":"status bar",p.screen?.masks.length?"Buoy's overlay":"",$.length?"the parts you asked to ignore":""].filter(Boolean);const A=S.join(" and ");const O=A?`; ${A} left out`:"";const F=v.length?`
61
61
 
62
- \u26A0\uFE0F ${v.join(" ")}`:"";if(E.changedPixels===0&&_.length===0){return{outcome:{saved:false,changed:false,percent:0,texts:0,summary:`no change from baseline "${n}"`},result:M(`No change from baseline "${n}" (${s}${O}).${g}${F}`)}}const P=E.boxes.map((B,z)=>{const re=p.screen?qa(B,p.screen.elements):[];const le=`${Oa(B)} (x ${B.x.toFixed(2)}, y ${B.y.toFixed(2)}, ${B.width.toFixed(2)}\xD7${B.height.toFixed(2)})`;return`${z+1}. ${le}${re.length?`: ${re.join(", ")}`:""}`});const K=await Ct(E.diffPng);const N=E.percent<.01&&E.changedPixels>0?"<0.01":E.percent.toFixed(2);const q=P[0]?`; ${P[0].replace(/^1\. /,"")}`:"";const V=_[0]?`; ${_[0]}`:"";const pe=E.changedPixels===0?`no pixel change from baseline "${n}", but text changed`:`${N}% changed from baseline "${n}"`;const Q={saved:false,changed:true,percent:E.percent,texts:_.length,summary:`${pe}${q}${V}`};return{outcome:Q,result:{content:[{type:"image",data:K.data,mimeType:K.mimeType},{type:"text",text:`**Changed from baseline "${n}": ${E.percent<.01&&E.changedPixels>0?"<0.01":E.percent.toFixed(2)}% of the screen** (${E.changedPixels} px${O}). Changes are red in the picture.${g}`+(P.length?`
62
+ \u26A0\uFE0F ${v.join(" ")}`:"";if(C.changedPixels===0&&_.length===0){return{outcome:{saved:false,changed:false,percent:0,texts:0,summary:`no change from baseline "${n}"`},result:M(`No change from baseline "${n}" (${s}${O}).${g}${F}`)}}const P=C.boxes.map((B,z)=>{const re=p.screen?Ya(B,p.screen.elements):[];const le=`${Ua(B)} (x ${B.x.toFixed(2)}, y ${B.y.toFixed(2)}, ${B.width.toFixed(2)}\xD7${B.height.toFixed(2)})`;return`${z+1}. ${le}${re.length?`: ${re.join(", ")}`:""}`});const K=await Nt(C.diffPng);const N=C.percent<.01&&C.changedPixels>0?"<0.01":C.percent.toFixed(2);const q=P[0]?`; ${P[0].replace(/^1\. /,"")}`:"";const V=_[0]?`; ${_[0]}`:"";const pe=C.changedPixels===0?`no pixel change from baseline "${n}", but text changed`:`${N}% changed from baseline "${n}"`;const Q={saved:false,changed:true,percent:C.percent,texts:_.length,summary:`${pe}${q}${V}`};return{outcome:Q,result:{content:[{type:"image",data:K.data,mimeType:K.mimeType},{type:"text",text:`**Changed from baseline "${n}": ${C.percent<.01&&C.changedPixels>0?"<0.01":C.percent.toFixed(2)}% of the screen** (${C.changedPixels} px${O}). Changes are red in the picture.${g}`+(P.length?`
63
63
 
64
64
  Where:
65
65
  ${P.join("\n")}`:"")+(_.length?`
@@ -67,9 +67,9 @@ ${P.join("\n")}`:"")+(_.length?`
67
67
  Text:
68
68
  ${_.map(B=>`- ${B}`).join("\n")}`:"")+`${F}
69
69
 
70
- If this change is expected, save it with updateBaseline: true.`}]}}}import{execFile as Lh,spawn as hn}from"child_process";import{promisify as qh}from"util";import{promises as yr}from"fs";import Uh from"path";var Ut=3*6e4;var zt=new Set;async function zh(e,n){await yr.mkdir(Uh.dirname(n),{recursive:true});const t=Date.now();let i;let r;if(e.platform==="ios"){i=hn("xcrun",["simctl","io",e.udid,"recordVideo","--codec=h264","--force",n],{stdio:["ignore","ignore","pipe"]})}else if(e.platform==="ios-device"){i=hn("xcrun",Vd(e.udid,n,Ut/1e3),{stdio:["ignore","ignore","pipe"]})}else{r=`/sdcard/buoy-recording-${t}-${Math.random().toString(36).slice(2,8)}.mp4`;const u=tn(e.serial,["shell",`echo $$; exec screenrecord --time-limit ${Ut/1e3} ${de(r)}`]);i=hn(u.bin,u.args,{stdio:["ignore","pipe","pipe"]})}const o={host:e,file:n,startedAt:t,proc:i,remote:r};zt.add(o);let a="";let s=t;let c;i.stderr?.on("data",u=>{a+=String(u);if(/Recording started/i.test(String(u)))s=Date.now()});i.stdout?.on("data",u=>{const h=/^(\d+)/m.exec(String(u));if(h&&c===void 0)c=Number(h[1])});const l=await new Promise(u=>{const h=setTimeout(()=>u(null),e.platform==="ios"?3e3:e.platform==="ios-device"?4e3:1200);const b=setInterval(()=>{if(e.platform==="ios"&&s!==t||e.platform==="android"&&c!==void 0){clearTimeout(h);clearInterval(b);u(null)}},50);i.once("exit",f=>{clearTimeout(h);clearInterval(b);u(f??-1)});i.once("error",()=>{clearTimeout(h);clearInterval(b);u(-1)})});zt.delete(o);if(l!==null){if(/not supported by this device/i.test(a)){throw new Error("This iPhone can't record its screen through Xcode (devicectl says the device doesn't support it). Record on a simulator, or use the iPhone's own Screen Recording in Control Center.")}throw new Error(`The screen recorder stopped right away: ${a.trim().slice(0,300)||`exit ${l}`}`)}if(e.platform==="ios"&&i.pid){const u=i.pid;const h=hn("sh",["-c",`sleep ${Ut/1e3+15}; ps -p ${u} -o command= | grep -q 'simctl io .* recordVideo' && kill -INT ${u}`],{detached:true,stdio:"ignore"});h.on("error",()=>void 0);h.unref();i.once("exit",()=>{try{if(h.pid)process.kill(-h.pid,"SIGTERM")}catch{h.kill("SIGTERM")}})}const d={host:e,file:n,startedAt:s,proc:i,remote:r,remotePid:c};zt.add(d);return d}async function Wh(e){if(e.stopTimer)clearTimeout(e.stopTimer);const n=Date.now();const t=new Promise(o=>{if(e.proc.exitCode!==null||e.proc.signalCode!==null)return o();e.proc.once("exit",()=>o())});if(e.host.platform==="ios"||e.host.platform==="ios-device"){e.proc.kill("SIGINT")}else if(e.remotePid){await J(e.host.serial,`kill -INT ${e.remotePid}`).catch(()=>void 0)}await Promise.race([t,new Promise(o=>setTimeout(o,e.host.platform==="ios-device"?6e4:1e4))]);if(e.proc.exitCode===null&&e.proc.signalCode===null)e.proc.kill("SIGKILL");zt.delete(e);if(e.host.platform==="android"&&e.remote){await new Promise(o=>setTimeout(o,800));await Be(e.host.serial,["pull",e.remote,e.file],6e4);await J(e.host.serial,`rm -f ${de(e.remote)}`).catch(()=>void 0)}const i=await yr.stat(e.file).catch(()=>null);if(!i||i.size<1e3)throw new Error("The recording file is missing or empty. Was the screen showing anything?");const r=n-e.startedAt;if(e.host.platform==="ios"||e.host.platform==="ios-device")await Hh(e.file,r).catch(()=>void 0);return r}async function Hh(e,n){const t=(await Xn("ffprobe",["-v","error","-show_entries","format=duration","-of","csv=p=0",e],{timeout:2e4})).stdout;const i=Number(t.trim());const r=n/1e3-.3-i;if(!Number.isFinite(i)||r<.25)return;const o=e.replace(/\.mp4$/,".pad.mp4");const a=s=>Xn("ffmpeg",["-y","-v","error","-i",e,"-vf",`tpad=stop_mode=clone:stop_duration=${r.toFixed(2)},fps=30`,"-an",...s,"-pix_fmt","yuv420p","-movflags","+faststart",o],{timeout:24e4,maxBuffer:4*1024*1024});try{await a(["-c:v","h264_videotoolbox","-b:v","6M"])}catch{await a(["-c:v","libx264","-preset","veryfast","-crf","22"])}await yr.rename(o,e)}function Jh(){for(const e of zt){try{if(e.host.platform!=="android")e.proc.kill("SIGINT");else if(e.remotePid){const n=tn(e.host.serial,["shell",`kill -INT ${e.remotePid}`]);hn(n.bin,n.args,{stdio:"ignore",detached:true}).unref()}}catch{}}zt.clear()}function Wt(e,n,t,i){const r=n.width!==n.height&&t.width!==t.height&&n.width>n.height!==t.width>t.height;if(!r)return e;const o=i==="landscapeRight";const a=n.width>n.height?!o:o;return a?[1-e[1],e[0]]:[e[1],1-e[0]]}function za(e){if(e.kind!=="pinch"||e.from.length!==2||e.to?.length!==2)return null;const{width:n,height:t}=e.screen;const i=h=>[h[0]*n,h[1]*t];const[r,o,a,s]=[i(e.from[0]),i(e.from[1]),i(e.to[0]),i(e.to[1])];const c=[(r[0]+o[0])/2,(r[1]+o[1])/2];const l=[(a[0]+s[0])/2,(a[1]+s[1])/2];const d=Math.atan2(r[1]-c[1],r[0]-c[0]);let u=Math.atan2(a[1]-l[1],a[0]-l[0])-d;if(u>Math.PI)u-=2*Math.PI;if(u<-Math.PI)u+=2*Math.PI;return{c0:c,c1:l,r0:Math.hypot(r[0]-c[0],r[1]-c[1]),r1:Math.hypot(a[0]-l[0],a[1]-l[1]),a0:d,turn:u}}var Wa=300;function Gh(e){const n=Math.max(0,e.reduce((t,i)=>t+i.from.length,0)-Wa);return n?`${n} touch dots were left out of the MP4. The viewer keeps all dots.`:null}var Xn=qh(Lh);async function Kh(e,n,t){let i;try{i=(await Xn("ffprobe",["-v","error","-select_streams","v:0","-show_entries","stream=width,height","-of","csv=p=0:s=x",e],{timeout:2e4})).stdout}catch(w){if(w.code==="ENOENT")return null;throw w}const[r,o]=i.trim().split("x").map(Number);if(!r||!o)throw new Error("could not read the video's size");const a={width:r,height:o};const s=Math.max(8,Math.round(Math.min(r,o)*.045));const c=s*2;const l=Math.max(2,Math.round(s*.18));const d=w=>w.toFixed(3);const u=[];for(const w of n){const g=w.t/1e3;const m=Math.max(w.ms,1)/1e3;const p=`clip((t-${d(g)})/${d(m)},0,1)`;const y=`enable='between(t,${d(g)},${d(g+m+.35)})':eval=frame:shortest=1`;const v=za(w);if(v){const{width:$,height:T}=w.screen;const k=Wt([0,0],w.screen,a,w.orientation);const D=Wt([1,0],w.screen,a,w.orientation);const E=Wt([0,1],w.screen,a,w.orientation);[1,-1].forEach(I=>{const R=`(${d(v.r0)}+(${d(v.r1-v.r0)})*${p})`;const _=`(${d(v.a0)}+(${d(v.turn)})*${p})`;const S=`clip((${d(v.c0[0])}+(${d(v.c1[0]-v.c0[0])})*${p}+${I}*${R}*cos(${_}))/${d($)},0,1)`;const A=`clip((${d(v.c0[1])}+(${d(v.c1[1]-v.c0[1])})*${p}+${I}*${R}*sin(${_}))/${d(T)},0,1)`;const[O,F]=[0,1].map(P=>`(${k[P]}+(${D[P]-k[P]})*${S}+(${E[P]-k[P]})*${A})`);u.push(`overlay=x='${O}*${r}-${s}':y='${F}*${o}-${s}':${y}`)});continue}w.from.forEach(($,T)=>{const[k,D]=Wt($,w.screen,a,w.orientation);const[E,I]=Wt(w.to?.[T]??$,w.screen,a,w.orientation);u.push(`overlay=x='${d(k*r-s)}+(${d((E-k)*r)})*${p}':y='${d(D*o-s)}+(${d((I-D)*o)})*${p}':${y}`)})}const h=u.slice(0,Wa);const b=["[0:v]fps=30[base]",`[1:v]split=${h.length}${h.map((w,g)=>`[c${g}]`).join("")}`,...h.map((w,g)=>`${g===0?"[base]":`[v${g-1}]`}[c${g}]${w}[v${g}]`)].join(";");const f=`color=c=white@0:s=${c}x${c},format=rgba,geq=r='255':g='255':b='255':a='if(lte(hypot(X-${s},Y-${s}),${s}),if(gte(hypot(X-${s},Y-${s}),${s-l}),235,105),0)'`;await Xn("ffmpeg",["-y","-v","error","-i",e,"-f","lavfi","-i",f,"-filter_complex",b,"-map",`[v${h.length-1}]`,"-an","-c:v","libx264","-preset","veryfast","-crf","22","-pix_fmt","yuv420p","-movflags","+faststart",t],{timeout:24e4,maxBuffer:4*1024*1024});return t}function Zn(e){return e.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,"$1 [redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,"[redacted jwt]").replace(/((?:api[_-]?key|token|secret|password|passwd|authorization|cookie|session)["']?\s*[:=]\s*["']?)([^\s"'&,;}]{4,})/gi,"$1[redacted]")}function Vh(e){const n=(e.marks??[]).map(i=>({...i,arc:za(i)??void 0}));const t=JSON.stringify({...e,marks:n}).replace(/</g,"\\u003c");return`<!doctype html>
70
+ If this change is expected, save it with updateBaseline: true.`}]}}}import{execFile as ip,spawn as bn}from"child_process";import{promisify as rp}from"util";import{promises as Ir}from"fs";import op from"path";var Jt=3*6e4;var Gt=new Set;async function ap(e,n){await Ir.mkdir(op.dirname(n),{recursive:true});const t=Date.now();let i;let r;if(e.platform==="ios"){i=bn("xcrun",["simctl","io",e.udid,"recordVideo","--codec=h264","--force",n],{stdio:["ignore","ignore","pipe"]})}else if(e.platform==="ios-device"){i=bn("xcrun",hu(e.udid,n,Jt/1e3),{stdio:["ignore","ignore","pipe"]})}else{r=`/sdcard/buoy-recording-${t}-${Math.random().toString(36).slice(2,8)}.mp4`;const u=cn(e.serial,["shell",`echo $$; exec screenrecord --time-limit ${Jt/1e3} ${de(r)}`]);i=bn(u.bin,u.args,{stdio:["ignore","pipe","pipe"]})}const o={host:e,file:n,startedAt:t,proc:i,remote:r};Gt.add(o);let a="";let s=t;let c;i.stderr?.on("data",u=>{a+=String(u);if(/Recording started/i.test(String(u)))s=Date.now()});i.stdout?.on("data",u=>{const h=/^(\d+)/m.exec(String(u));if(h&&c===void 0)c=Number(h[1])});const l=await new Promise(u=>{const h=setTimeout(()=>u(null),e.platform==="ios"?3e3:e.platform==="ios-device"?4e3:1200);const b=setInterval(()=>{if(e.platform==="ios"&&s!==t||e.platform==="android"&&c!==void 0){clearTimeout(h);clearInterval(b);u(null)}},50);i.once("exit",f=>{clearTimeout(h);clearInterval(b);u(f??-1)});i.once("error",()=>{clearTimeout(h);clearInterval(b);u(-1)})});Gt.delete(o);if(l!==null){if(/not supported by this device/i.test(a)){throw new Error("This iPhone can't record its screen through Xcode (devicectl says the device doesn't support it). Record on a simulator, or use the iPhone's own Screen Recording in Control Center.")}throw new Error(`The screen recorder stopped right away: ${a.trim().slice(0,300)||`exit ${l}`}`)}if(e.platform==="ios"&&i.pid){const u=i.pid;const h=bn("sh",["-c",`sleep ${Jt/1e3+15}; ps -p ${u} -o command= | grep -q 'simctl io .* recordVideo' && kill -INT ${u}`],{detached:true,stdio:"ignore"});h.on("error",()=>void 0);h.unref();i.once("exit",()=>{try{if(h.pid)process.kill(-h.pid,"SIGTERM")}catch{h.kill("SIGTERM")}})}const d={host:e,file:n,startedAt:s,proc:i,remote:r,remotePid:c};Gt.add(d);return d}async function sp(e){if(e.stopTimer)clearTimeout(e.stopTimer);const n=Date.now();const t=new Promise(o=>{if(e.proc.exitCode!==null||e.proc.signalCode!==null)return o();e.proc.once("exit",()=>o())});if(e.host.platform==="ios"||e.host.platform==="ios-device"){e.proc.kill("SIGINT")}else if(e.remotePid){await J(e.host.serial,`kill -INT ${e.remotePid}`).catch(()=>void 0)}await Promise.race([t,new Promise(o=>setTimeout(o,e.host.platform==="ios-device"?6e4:1e4))]);if(e.proc.exitCode===null&&e.proc.signalCode===null)e.proc.kill("SIGKILL");Gt.delete(e);if(e.host.platform==="android"&&e.remote){await new Promise(o=>setTimeout(o,800));await Le(e.host.serial,["pull",e.remote,e.file],6e4);await J(e.host.serial,`rm -f ${de(e.remote)}`).catch(()=>void 0)}const i=await Ir.stat(e.file).catch(()=>null);if(!i||i.size<1e3)throw new Error("The recording file is missing or empty. Was the screen showing anything?");const r=n-e.startedAt;if(e.host.platform==="ios"||e.host.platform==="ios-device")await cp(e.file,r).catch(()=>void 0);return r}async function cp(e,n){const t=(await si("ffprobe",["-v","error","-show_entries","format=duration","-of","csv=p=0",e],{timeout:2e4})).stdout;const i=Number(t.trim());const r=n/1e3-.3-i;if(!Number.isFinite(i)||r<.25)return;const o=e.replace(/\.mp4$/,".pad.mp4");const a=s=>si("ffmpeg",["-y","-v","error","-i",e,"-vf",`tpad=stop_mode=clone:stop_duration=${r.toFixed(2)},fps=30`,"-an",...s,"-pix_fmt","yuv420p","-movflags","+faststart",o],{timeout:24e4,maxBuffer:4*1024*1024});try{await a(["-c:v","h264_videotoolbox","-b:v","6M"])}catch{await a(["-c:v","libx264","-preset","veryfast","-crf","22"])}await Ir.rename(o,e)}function lp(){for(const e of Gt){try{if(e.host.platform!=="android")e.proc.kill("SIGINT");else if(e.remotePid){const n=cn(e.host.serial,["shell",`kill -INT ${e.remotePid}`]);bn(n.bin,n.args,{stdio:"ignore",detached:true}).unref()}}catch{}}Gt.clear()}function Kt(e,n,t,i){const r=n.width!==n.height&&t.width!==t.height&&n.width>n.height!==t.width>t.height;if(!r)return e;const o=i==="landscapeRight";const a=n.width>n.height?!o:o;return a?[1-e[1],e[0]]:[e[1],1-e[0]]}function Za(e){if(e.kind!=="pinch"||e.from.length!==2||e.to?.length!==2)return null;const{width:n,height:t}=e.screen;const i=h=>[h[0]*n,h[1]*t];const[r,o,a,s]=[i(e.from[0]),i(e.from[1]),i(e.to[0]),i(e.to[1])];const c=[(r[0]+o[0])/2,(r[1]+o[1])/2];const l=[(a[0]+s[0])/2,(a[1]+s[1])/2];const d=Math.atan2(r[1]-c[1],r[0]-c[0]);let u=Math.atan2(a[1]-l[1],a[0]-l[0])-d;if(u>Math.PI)u-=2*Math.PI;if(u<-Math.PI)u+=2*Math.PI;return{c0:c,c1:l,r0:Math.hypot(r[0]-c[0],r[1]-c[1]),r1:Math.hypot(a[0]-l[0],a[1]-l[1]),a0:d,turn:u}}var Qa=300;function dp(e){const n=Math.max(0,e.reduce((t,i)=>t+i.from.length,0)-Qa);return n?`${n} touch dots were left out of the MP4. The viewer keeps all dots.`:null}var si=rp(ip);async function up(e,n,t){let i;try{i=(await si("ffprobe",["-v","error","-select_streams","v:0","-show_entries","stream=width,height","-of","csv=p=0:s=x",e],{timeout:2e4})).stdout}catch(w){if(w.code==="ENOENT")return null;throw w}const[r,o]=i.trim().split("x").map(Number);if(!r||!o)throw new Error("could not read the video's size");const a={width:r,height:o};const s=Math.max(8,Math.round(Math.min(r,o)*.045));const c=s*2;const l=Math.max(2,Math.round(s*.18));const d=w=>w.toFixed(3);const u=[];for(const w of n){const g=w.t/1e3;const m=Math.max(w.ms,1)/1e3;const p=`clip((t-${d(g)})/${d(m)},0,1)`;const y=`enable='between(t,${d(g)},${d(g+m+.35)})':eval=frame:shortest=1`;const v=Za(w);if(v){const{width:$,height:T}=w.screen;const k=Kt([0,0],w.screen,a,w.orientation);const D=Kt([1,0],w.screen,a,w.orientation);const C=Kt([0,1],w.screen,a,w.orientation);[1,-1].forEach(I=>{const R=`(${d(v.r0)}+(${d(v.r1-v.r0)})*${p})`;const _=`(${d(v.a0)}+(${d(v.turn)})*${p})`;const S=`clip((${d(v.c0[0])}+(${d(v.c1[0]-v.c0[0])})*${p}+${I}*${R}*cos(${_}))/${d($)},0,1)`;const A=`clip((${d(v.c0[1])}+(${d(v.c1[1]-v.c0[1])})*${p}+${I}*${R}*sin(${_}))/${d(T)},0,1)`;const[O,F]=[0,1].map(P=>`(${k[P]}+(${D[P]-k[P]})*${S}+(${C[P]-k[P]})*${A})`);u.push(`overlay=x='${O}*${r}-${s}':y='${F}*${o}-${s}':${y}`)});continue}w.from.forEach(($,T)=>{const[k,D]=Kt($,w.screen,a,w.orientation);const[C,I]=Kt(w.to?.[T]??$,w.screen,a,w.orientation);u.push(`overlay=x='${d(k*r-s)}+(${d((C-k)*r)})*${p}':y='${d(D*o-s)}+(${d((I-D)*o)})*${p}':${y}`)})}const h=u.slice(0,Qa);const b=["[0:v]fps=30[base]",`[1:v]split=${h.length}${h.map((w,g)=>`[c${g}]`).join("")}`,...h.map((w,g)=>`${g===0?"[base]":`[v${g-1}]`}[c${g}]${w}[v${g}]`)].join(";");const f=`color=c=white@0:s=${c}x${c},format=rgba,geq=r='255':g='255':b='255':a='if(lte(hypot(X-${s},Y-${s}),${s}),if(gte(hypot(X-${s},Y-${s}),${s-l}),235,105),0)'`;await si("ffmpeg",["-y","-v","error","-i",e,"-f","lavfi","-i",f,"-filter_complex",b,"-map",`[v${h.length-1}]`,"-an","-c:v","libx264","-preset","veryfast","-crf","22","-pix_fmt","yuv420p","-movflags","+faststart",t],{timeout:24e4,maxBuffer:4*1024*1024});return t}function ci(e){return e.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,"$1 [redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,"[redacted jwt]").replace(/((?:api[_-]?key|token|secret|password|passwd|authorization|cookie|session)["']?\s*[:=]\s*["']?)([^\s"'&,;}]{4,})/gi,"$1[redacted]")}function hp(e){const n=(e.marks??[]).map(i=>({...i,arc:Za(i)??void 0}));const t=JSON.stringify({...e,marks:n}).replace(/</g,"\\u003c");return`<!doctype html>
71
71
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
72
- <title>${pn(e.title)}</title>
72
+ <title>${vn(e.title)}</title>
73
73
  <style>
74
74
  :root{--bg:#f7f5f0;--fg:#1d1c1a;--muted:#6f6b63;--line:#dedad1;--card:#fff;--acc:#0f8a5f;--err:#c7362f;--warn:#b7791f;--now:#e8f4ee}
75
75
  @media (prefers-color-scheme:dark){:root{--bg:#141413;--fg:#efede8;--muted:#a19d94;--line:#2f2e2b;--card:#1c1c1a;--acc:#3fbf8c;--err:#ef6b62;--warn:#e0a84a;--now:#1f2e27}}
@@ -90,9 +90,9 @@ li.agent .s{color:var(--acc);font-weight:600}
90
90
  li.error .ti{color:var(--err);font-weight:600}li.warning .ti{color:var(--warn)}li .d{grid-column:3;color:var(--muted);font-size:12px;white-space:pre-wrap;word-break:break-word}
91
91
  .empty{padding:14px;color:var(--muted)}
92
92
  </style></head><body>
93
- <header><h1>${pn(e.title)}</h1><p>${pn(e.device)} \xB7 ${(e.durationMs/1e3).toFixed(1)} s \xB7 ${e.items.length} events \xB7 recorded by Buoy \xB7 secrets in logs are hidden</p>
94
- ${(e.notes??[]).map(i=>`<p class="note">${pn(i)}</p>`).join("")}</header>
95
- <main><div class="stage"><video id="v" src="${pn(e.video)}" controls playsinline></video><div id="marks"></div></div>
93
+ <header><h1>${vn(e.title)}</h1><p>${vn(e.device)} \xB7 ${(e.durationMs/1e3).toFixed(1)} s \xB7 ${e.items.length} events \xB7 recorded by Buoy \xB7 secrets in logs are hidden</p>
94
+ ${(e.notes??[]).map(i=>`<p class="note">${vn(i)}</p>`).join("")}</header>
95
+ <main><div class="stage"><video id="v" src="${vn(e.video)}" controls playsinline></video><div id="marks"></div></div>
96
96
  <div class="side"><div class="chips" id="chips"></div><ol id="list"></ol></div></main>
97
97
  <script>
98
98
  const D=${t};const v=document.getElementById('v'),list=document.getElementById('list'),chips=document.getElementById('chips');
@@ -109,7 +109,7 @@ v.ontimeupdate=()=>{const ms=v.currentTime*1000;let cur=null;for(const li of lis
109
109
  draw();
110
110
  const M=D.marks||[],layer=document.getElementById('marks');
111
111
  // A landscape app can be recorded as a portrait video: turn its points to match.
112
- const mapPoint=${Wt.toString()};
112
+ const mapPoint=${Kt.toString()};
113
113
  const place=(p,m)=>mapPoint(p,m.screen,{width:v.videoWidth,height:v.videoHeight},m.orientation);
114
114
  function marks(){const ms=v.currentTime*1000;layer.innerHTML='';
115
115
  const dot=(p)=>{const d=document.createElement('div');d.className='dot';d.style.left=p[0]*100+'%';d.style.top=p[1]*100+'%';layer.appendChild(d)};
@@ -119,7 +119,7 @@ const c=(n)=>Math.min(1,Math.max(0,n));for(const s of [1,-1])dot(place([c((cx+s*
119
119
  m.from.forEach((p,i)=>{const q=(m.to&&m.to[i])||p;const a=place(p,m),b=place(q,m);dot([a[0]+(b[0]-a[0])*k,a[1]+(b[1]-a[1])*k])})}}
120
120
  (function tick(){marks();requestAnimationFrame(tick)})();
121
121
  <\/script></body></html>
122
- `}function pn(e){return e.replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}import{z as Qn}from"zod";import{z as be}from"zod";var Yh=4e3;var Xh=6;function $r(e){if(typeof e==="number"){if(!Number.isFinite(e)||Number.isInteger(e)&&Math.abs(e)<10)return null;return`n:${e}`}if(typeof e!=="string")return null;const n=e.trim();if(n.length<3||n.length>80)return null;const t=/^[$€£]?-?[\d,]+(\.\d+)?$/.test(n)?Number(n.replace(/[$€£,]/g,"")):NaN;if(Number.isFinite(t)&&/[\d]/.test(n))return $r(t);return`s:${n}`}function Ha(e,n,t,i){if(i.length>=Yh||t>Xh||e==null)return;if(typeof e==="object"){const o=Array.isArray(e)?e.slice(0,50).map((a,s)=>[String(s),a]):Object.entries(e);for(const[a,s]of o)Ha(s,n?`${n}.${a}`:a,t+1,i);return}const r=$r(e);if(r)i.push([r,n])}function kr(e,n,t){const i=[];Ha(t,"",0,i);for(const[r,o]of i){const a=o?`${n}, ${o}`:n;const s=e.get(r)??[];if(s.length<3)s.push(a);e.set(r,s)}}async function Zh(e,n){const t=new Set(e.connectedDevices().find(a=>a.deviceId===n)?.tools.map(a=>a.toolId)??[]);const i={timeoutMs:1500,silent:true};const r=[];const o=new Map;if(t.has("zustand")){r.push(e.callAction("zustand","listStores",{includeValues:true},n,i).then(a=>{for(const s of a?.stores??[])kr(o,`${s.name} store`,s.currentState)}))}if(t.has("redux")){r.push(e.callAction("redux","getState",{includeValues:true},n,i).then(a=>{const s=a;if(s?.available)kr(o,"redux state",s.state)}))}if(t.has("jotai")){r.push(e.callAction("jotai","listAtoms",{includeValues:true},n,i).then(a=>{for(const s of a?.atoms??[])kr(o,`atom ${s.label}`,s.currentValue)}))}if(!r.length)return null;await Promise.all(r.map(a=>a.catch(()=>void 0)));return o.size?o:null}function Qh(e,n){if(!e||!n)return"";const t=$r(n);const i=t?e.get(t):void 0;return i?.length?` = live state: ${i.join("; ")}`:""}import{z as X}from"zod";import{execFile as ep,spawn as tp}from"child_process";import{promisify as np}from"util";import{promises as ae}from"fs";import _r from"os";import se from"path";import{execFile as ip}from"child_process";import{promisify as rp}from"util";var op=rp(ip);var Ja=/^ACEFADE/i;var Ga="unseen";function ap(e){let n=null;let t;const i=[];for(const r of e.split("\n")){const o=/digitizer attached \(sender ([0-9A-F]+)\)/i.exec(r);if(o){if(!i.includes(o[1]))i.push(o[1]);t=n;n=o[1];continue}if(/already have a main display digitizer/i.test(r)){if(t!==void 0)n=t??Ga;t=void 0;continue}t=void 0;const a=/digitizer detached \(sender ([0-9A-F]+)\)/i.exec(r);if(a){const s=i.indexOf(a[1]);if(s>=0)i.splice(s,1);if(n===a[1]||n===Ga&&s<0)n=i.find(c=>Ja.test(c))??i[i.length-1]??null}}if(!n)return"unknown";return Ja.test(n)?"idb":"other"}var sp="Real touches can't reach this simulator right now. On Xcode 27, DeviceHub (the app that shows simulators) takes over each booted simulator's touch screen, so a touch sent from here is dropped. Use tap_element for React elements (it presses them inside the app), or run_flow. Don't quit DeviceHub to fix this: quitting it shuts down every simulator.";var Ka=new Map;async function cp(e){const n=Ka.get(e);if(n&&Date.now()-n.at<6e4)return n.owner;let t="unknown";try{const{stdout:i}=await op("xcrun",["simctl","spawn",e,"log","show","--last","24h","--style","compact","--predicate",'process == "backboardd" AND category == "TouchEvents"'],{timeout:2e4,maxBuffer:16*1024*1024});t=ap(i.split("\n").filter(r=>/digitizer|already have a main/i.test(r)).join("\n"))}catch{t="unknown"}Ka.set(e,{owner:t,at:Date.now()});return t}async function Sr(e){if(await cp(e)==="other")throw new Error(sp)}import{execFile as lp}from"child_process";import{promisify as dp}from"util";import{promises as Ce}from"fs";import Va from"os";import Te from"path";var up=`import Foundation
122
+ `}function vn(e){return e.replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}import{z as li}from"zod";import{z as be}from"zod";var pp=4e3;var fp=6;function Mr(e){if(typeof e==="number"){if(!Number.isFinite(e)||Number.isInteger(e)&&Math.abs(e)<10)return null;return`n:${e}`}if(typeof e!=="string")return null;const n=e.trim();if(n.length<3||n.length>80)return null;const t=/^[$€£]?-?[\d,]+(\.\d+)?$/.test(n)?Number(n.replace(/[$€£,]/g,"")):NaN;if(Number.isFinite(t)&&/[\d]/.test(n))return Mr(t);return`s:${n}`}function es(e,n,t,i){if(i.length>=pp||t>fp||e==null)return;if(typeof e==="object"){const o=Array.isArray(e)?e.slice(0,50).map((a,s)=>[String(s),a]):Object.entries(e);for(const[a,s]of o)es(s,n?`${n}.${a}`:a,t+1,i);return}const r=Mr(e);if(r)i.push([r,n])}function Ar(e,n,t){const i=[];es(t,"",0,i);for(const[r,o]of i){const a=o?`${n}, ${o}`:n;const s=e.get(r)??[];if(s.length<3)s.push(a);e.set(r,s)}}async function mp(e,n){const t=new Set(e.connectedDevices().find(a=>a.deviceId===n)?.tools.map(a=>a.toolId)??[]);const i={timeoutMs:1500,silent:true};const r=[];const o=new Map;if(t.has("zustand")){r.push(e.callAction("zustand","listStores",{includeValues:true},n,i).then(a=>{for(const s of a?.stores??[])Ar(o,`${s.name} store`,s.currentState)}))}if(t.has("redux")){r.push(e.callAction("redux","getState",{includeValues:true},n,i).then(a=>{const s=a;if(s?.available)Ar(o,"redux state",s.state)}))}if(t.has("jotai")){r.push(e.callAction("jotai","listAtoms",{includeValues:true},n,i).then(a=>{for(const s of a?.atoms??[])Ar(o,`atom ${s.label}`,s.currentValue)}))}if(!r.length)return null;await Promise.all(r.map(a=>a.catch(()=>void 0)));return o.size?o:null}function wp(e,n){if(!e||!n)return"";const t=Mr(n);const i=t?e.get(t):void 0;return i?.length?` = live state: ${i.join("; ")}`:""}import{z as X}from"zod";import{execFile as gp,spawn as bp}from"child_process";import{promisify as vp}from"util";import{promises as ae}from"fs";import Cr from"os";import se from"path";import{execFile as yp}from"child_process";import{promisify as $p}from"util";var kp=$p(yp);var ts=/^ACEFADE/i;var ns="unseen";function _p(e){let n=null;let t;const i=[];for(const r of e.split("\n")){const o=/digitizer attached \(sender ([0-9A-F]+)\)/i.exec(r);if(o){if(!i.includes(o[1]))i.push(o[1]);t=n;n=o[1];continue}if(/already have a main display digitizer/i.test(r)){if(t!==void 0)n=t??ns;t=void 0;continue}t=void 0;const a=/digitizer detached \(sender ([0-9A-F]+)\)/i.exec(r);if(a){const s=i.indexOf(a[1]);if(s>=0)i.splice(s,1);if(n===a[1]||n===ns&&s<0)n=i.find(c=>ts.test(c))??i[i.length-1]??null}}if(!n)return"unknown";return ts.test(n)?"idb":"other"}var Sp="Real touches can't reach this simulator right now. On Xcode 27, DeviceHub (the app that shows simulators) takes over each booted simulator's touch screen, so a touch sent from here is dropped. Use tap_element for React elements (it presses them inside the app), or run_flow. Don't quit DeviceHub to fix this: quitting it shuts down every simulator.";var is=new Map;async function xp(e){const n=is.get(e);if(n&&Date.now()-n.at<6e4)return n.owner;let t="unknown";try{const{stdout:i}=await kp("xcrun",["simctl","spawn",e,"log","show","--last","24h","--style","compact","--predicate",'process == "backboardd" AND category == "TouchEvents"'],{timeout:2e4,maxBuffer:16*1024*1024});t=_p(i.split("\n").filter(r=>/digitizer|already have a main/i.test(r)).join("\n"))}catch{t="unknown"}is.set(e,{owner:t,at:Date.now()});return t}async function Er(e){if(await xp(e)==="other")throw new Error(Sp)}import{execFile as Tp}from"child_process";import{promisify as Dp}from"util";import{promises as Oe}from"fs";import rs from"os";import De from"path";var Ip=`import Foundation
123
123
  import Darwin
124
124
  import CryptoKit
125
125
 
@@ -592,70 +592,70 @@ struct Command {
592
592
  down = false
593
593
  }
594
594
  }
595
- `;var hp="// swift-interface-format-version: 1.0\n// swift-compiler-version: Apple Swift version 6.4\n// swift-module-flags: -target arm64-apple-macosx15.0 -enable-library-evolution -module-name CoreDevice\n// Reconstructed API subset. No replacement framework code is linked.\n// Non-frozen types and protocol/class calls resolve through Apple metadata and thunks.\nimport Foundation\nimport CoreGraphics\npublic struct Capability {}\npublic protocol CoreDeviceServiceConnection {}\npublic struct CoreDeviceService {\n public static var sharedConnection: any CoreDeviceServiceConnection { get }\n}\npublic enum DeviceVisibilityClass: Swift.Hashable {\n case `default`, wirelessPairableDevices, devicesBeingProvisioned, simulators\n public static func == (lhs: DeviceVisibilityClass, rhs: DeviceVisibilityClass) -> Swift.Bool\n public func hash(into hasher: inout Swift.Hasher)\n public var hashValue: Swift.Int { get }\n}\npublic class DeviceManager {\n public static var shared: DeviceManager { get }\n public init(serviceConnection: any CoreDeviceServiceConnection, allowedDeviceVisibilityClasses: Swift.Set<DeviceVisibilityClass>)\n public func awaitFullInitialization() async\n public func waitForPostPluginLoadCheckIn() async\n public var devices: [Foundation.UUID: RemoteDevice] { get }\n public func deviceNamed(_ name: Swift.String) throws -> RemoteDevice\n}\npublic class RemoteDevice {\n public var deviceIdentifier: Foundation.UUID { get }\n public func getImplementation<C: DeviceCapability>(for capability: CapabilityStaticMember<C>) async throws -> C.ProtocolType\n}\npublic protocol DeviceCapability {\n associatedtype ProtocolType\n static var capability: Capability { get }\n}\npublic struct CapabilityStaticMember<C> {\n public init()\n}\npublic struct DigitizerHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDDigitizer\n public static var capability: Capability { get }\n}\npublic protocol DeviceCapabilityProtocol {\n static var capability: Capability { get }\n static var identifier: Swift.String { get }\n static var identifierSuffix: Swift.String { get }\n static var priority: Swift.Int { get }\n}\npublic protocol HIDDigitizer: DeviceCapabilityProtocol {\n func send(pointOne: DigitizerPoint, pointTwo: DigitizerPoint?, eventType: DigitizerEventType, edge: DigitizerEdge, target: DigitizerTarget) throws\n func sendBarrier()\n}\npublic struct DigitizerPoint {\n public init(x: Swift.Double, y: Swift.Double)\n}\npublic enum DigitizerEventType { case start, position, end }\npublic enum DigitizerEdge { case none, top, left, bottom, right }\npublic enum DigitizerTarget { case mainScreen, display1, display2, display3, display4, display5, display6, display7, display8, display9, display10 }\npublic struct KeyboardHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDKeyboard\n public static var capability: Capability { get }\n}\npublic protocol HIDKeyboard: DeviceCapabilityProtocol {\n func send(key: HIDKeyboardUsageCode, state: HIDButtonState) throws\n func sendBarrier()\n}\npublic struct HIDKeyboardUsageCode {\n public init(rawValue: Swift.UInt16)\n}\npublic enum HIDButtonState { case down, up }\npublic struct ButtonHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDButton\n public static var capability: Capability { get }\n}\npublic protocol HIDButton: DeviceCapabilityProtocol {\n func sendCustomButton(usagePage: Swift.Int, usageCode: Swift.Int, state: HIDButtonState) throws\n func sendBarrier()\n}\n";var pp="718c261deb06";var yt=dp(lp);var Ya="/Library/Developer/PrivateFrameworks";var fn=null;var xr="";var Xa=0;var Tr;var Za;var mn=null;async function fp(){try{const{stdout:e}=await yt("xcode-select",["-p"],{timeout:1e4});const n=Te.join(process.env.DEVELOPER_DIR||e.trim(),"..","Info.plist");const{stdout:t}=await yt("plutil",["-extract","DTXcodeBuild","raw",n],{timeout:1e4});return t.trim().replace(/[^\w.-]/g,"")||"unknown"}catch{return"unknown"}}async function mp(e){if(process.platform!=="darwin")return null;const n=Te.join(Va.homedir(),".buoy","hidtouch",`${e}-${pp}`);const t=Te.join(n,"hidtouch");try{await Ce.access(t);return t}catch{}try{await Ce.access(Te.join(Ya,"CoreDevice.framework"))}catch{xr="CoreDevice is not installed (it ships with Xcode 15 and later).";return null}const i=await Ce.mkdtemp(Te.join(Va.tmpdir(),"buoy-hidtouch-"));try{await Ce.mkdir(Te.join(i,"modules"));await Ce.writeFile(Te.join(i,"hidtouch.swift"),up);await Ce.writeFile(Te.join(i,"modules","CoreDevice.swiftinterface"),hp);const r=Te.join(i,"hidtouch");await yt("xcrun",["swiftc","-O","-module-cache-path",Te.join(i,"cache"),"-I",Te.join(i,"modules"),"-F",Ya,"-framework","CoreDevice","-parse-as-library",Te.join(i,"hidtouch.swift"),"-o",r],{timeout:18e4,maxBuffer:8*1024*1024,env:{...process.env,TMPDIR:i}});await Ce.mkdir(n,{recursive:true});const o=`${t}.${process.pid}`;await Ce.copyFile(r,o);await Ce.chmod(o,493);await Ce.rename(o,t);return t}catch(r){const o=r;xr=(o.stderr||o.message||"").trim().split("\n").slice(-3).join(" ").slice(0,300);return null}finally{await Ce.rm(i,{recursive:true,force:true}).catch(()=>void 0)}}function De(){const e=Date.now();const n=!fn||e-Xa>=1e4||Tr!==process.env.DEVELOPER_DIR;Xa=e;if(mn)return mn;if(!n)return fn;Tr=process.env.DEVELOPER_DIR;const t=Tr;mn=fp().then(i=>{const r=JSON.stringify([t,i]);if(!fn||Za!==r){Za=r;xr="";fn=mp(i)}return fn}).finally(()=>{mn=null});return mn}async function wn(e){try{const{stdout:n}=await yt("xcrun",["devicectl","device","orientation","get","--device",e],{timeout:15e3});const t=/Orientation:\s*(\w+)/.exec(n)?.[1];return t==="portrait"||t==="portraitUpsideDown"||t==="landscapeLeft"||t==="landscapeRight"?t:null}catch{return null}}async function Dr(e,n){if(!n)return null;try{const{stdout:t}=await yt("xcrun",["simctl","get_app_container",e,n,"app"],{timeout:1e4});const{stdout:i}=await yt("plutil",["-convert","json","-o","-",Te.join(t.trim(),"Info.plist")],{timeout:1e4});const r=JSON.parse(i);const{stdout:o}=await yt("xcrun",["simctl","getenv",e,"SIMULATOR_MODEL_IDENTIFIER"],{timeout:1e4});const a=o.trim().startsWith("iPad")?"ipad":"iphone";const s=r[`UISupportedInterfaceOrientations~${a}`]??r.UISupportedInterfaceOrientations;if(!Array.isArray(s)||!s.every(c=>typeof c==="string"))return null;return s.includes("UIInterfaceOrientationPortraitUpsideDown")}catch{return null}}function ei(e,n,t,i,r){if(i==="portraitUpsideDown"){if(r==null)throw new Error("Upside-down touch needs the app turn list. Turn the device upright and try again.");return r?[1-e,1-n]:[e,n]}if(!t)return[e,n];if(i==="landscapeRight")return[n,1-e];return[1-n,e]}async function ti(e,n,t,i=false){try{const r=n[n.indexOf("--device")+1];await Ma(e,r,n,t)}catch(r){const o=r;const a=o.stderr||o.message||"";if(/HID Digitizer/i.test(a)&&/not supported/i.test(a)){console.error("[buoy:hid]",a);throw new Error(i?"Your iPhone isn't connected. Unlock it and plug it in, or put it on the same Wi-Fi. Then try again.":"Touch is not supported on this iOS Simulator.")}throw new Error(`The touch failed: ${a.replace(/^hidtouch:\s*/,"").split("\n")[0].slice(0,300)}`)}}var Ir=e=>Math.min(1,Math.max(0,e));var ct=e=>Ir(e).toFixed(5);async function Qa(e,n,t,i,r,o,a){const s=t.width>t.height;const c=a!==void 0?a:await wn(n);const l=c==="portraitUpsideDown"?await Dr(n,t.bundleId):null;const[d,u]=ei(i/t.width,r/t.height,s,c,l);await ti(e,o?["--device",n,"long-press",ct(d),ct(u),String(Math.round(o))]:["--device",n,"tap",ct(d),ct(u)],void 0,t.isPhone);return c}async function wp(e,n,t,i,r,o,a=0,s=0,c){const l=t.width>t.height;const d=c!==void 0?c:await wn(n);const u=d==="portraitUpsideDown"?await Dr(n,t.bundleId):null;const h=ei(i[0]/t.width,i[1]/t.height,l,d,u);const b=ei(r[0]/t.width,r[1]/t.height,l,d,u);const f=String(Math.round(Math.min(1e4,Math.max(1,o))));const w=[ct(h[0]),ct(h[1]),ct(b[0]),ct(b[1])];const g=a>0?["--device",n,"drag",...w,String(Math.round(Math.min(1e4,a))),f]:s>0?["--device",n,"scroll",...w,f,String(Math.round(Math.min(1e4,s)))]:["--device",n,"swipe",...w,f];await ti(e,g,void 0,t.isPhone);return d}async function gp(e,n,t,i,r,o,a,s){const c=t.width>t.height;const l=await wn(n);const d=l==="portraitUpsideDown"?await Dr(n,t.bundleId):null;const u=c?{w:t.height,h:t.width}:{w:t.width,h:t.height};const h=[i,r,o,a].flatMap(b=>{const[f,w]=ei(b[0]/t.width,b[1]/t.height,c,l,d);return[(Ir(f)*u.w).toFixed(2),(Ir(w)*u.h).toFixed(2)]});await ti(e,["--device",n,"--points",String(u.w),String(u.h),"two",...h,String(Math.round(Math.min(1e4,Math.max(1,s))))],void 0,t.isPhone);return l}var bp=/^[\x20-\x7e\n]*$/;async function es(e,n,t){if(!bp.test(t))throw new Error("The simulator keyboard types plain ASCII letters, digits, punctuation and newlines only. For other text use tap_element with text.");if(!t)return;await ti(e,["--device",n,"type","-"],t)}import{z as ni}from"zod";import{promises as ii}from"fs";import gn from"path";import{randomUUID as vp}from"crypto";import{z as Z}from"zod";import{promises as ri}from"fs";import oi from"path";var ai=()=>oi.join(process.cwd(),".buoy","flows");var ts=/^[A-Za-z0-9_-]{1,64}$/;async function yp(){try{return(await ri.readdir(ai())).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5))}catch{return[]}}var si="highlight-updates";var ns="app";var is=e=>({content:[{type:"text",text:e}]});var Ht=e=>({isError:true,content:[{type:"text",text:e}]});var rs=e=>e instanceof Error?e.message:String(e);var Xe=e=>new Promise(n=>setTimeout(n,e));var Jt=250;var $p=2e3;var kp=5e3;var ci=2e3;var _p=1200;var Sp=1500;var Mr=12e4;function li(e,n){if(e.testID&&e.testID===n)return true;const t=n.toLowerCase();return[e.testID,e.text,e.label,e.name].filter(Boolean).some(i=>String(i).toLowerCase().includes(t))}var os=["tap","waitFor","waitGone","expect","expectGone","reload","wait","swipe"];function xp(e){if(!e||typeof e!=="object"||Array.isArray(e))return e;const n={...e};const t=a=>{if(!a||typeof a!=="object"||Array.isArray(a))return a;const s=a;return[s.testID,s.text,s.query,s.label,s.name].find(c=>typeof c==="string")??a};for(const a of["tap","into","waitFor","waitGone","expect","expectGone"])n[a]=t(n[a]);for(const a of Object.keys(n))if(n[a]===void 0)delete n[a];if(typeof n.into==="string")return n;const i=typeof n.type==="string"&&os.includes(n.type)?"type":typeof n.action==="string"&&os.includes(n.action)?"action":null;if(!i)return n;const r=n[i];delete n[i];const o=[n.query,n.text,n.target,n.selector,n.element,n.testID].map(t).find(a=>typeof a==="string");for(const a of["query","text","target","selector","element","testID"])delete n[a];if(r==="reload")n.reload=true;else if(r==="swipe"){n.swipe=n.direction??o??"up";delete n.direction}else if(r==="wait")n.waitMs=n.waitMs??n.ms??n.durationMs??n.timeoutMs;else if(o!==void 0&&n[r]===void 0)n[r]=o;delete n.ms;delete n.durationMs;return n}var as=Z.preprocess(xp,Z.object({tap:Z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:Z.boolean().optional().describe("Make `tap` a long-press instead."),type:Z.string().optional().describe("Text to type (needs `into`)."),into:Z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:Z.union([Z.number(),Z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),swipe:Z.enum(["up","down","left","right"]).optional().describe('A real swipe, the way the finger moves: "up" scrolls down the list.'),untilEnd:Z.boolean().optional().describe("With swipe: keep swiping until the screen stops moving (the end of the list)."),real:Z.boolean().optional().describe("Make this step's tap a real touch. Default: the flow's `real`."),reload:Z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:Z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000). To wait for a result (a slow request, a new screen), use waitFor with timeoutMs instead: it stops as soon as the element shows."),waitFor:Z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:Z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:Z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:Z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),snapshot:Z.string().optional().describe("Compare the whole screen to the baseline with this name (saved on the first run, like screenshot_component baseline). Fails the step on a text change or a change bigger than maxChange."),maxChange:Z.number().min(0).max(100).optional().describe("snapshot: allowed changed share of the screen, in percent. Default 0.05."),timeoutMs:Z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")}).describe("One flow step. Order within a step: reload \u2192 tap/type/set/swipe \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect."));var Tp={steps:Z.array(as).min(1).optional().describe("Steps to run, in order. Leave out when replaying a saved flow."),flow:Z.string().optional().describe("Replay a saved flow by name (from .buoy/flows in the project) instead of passing steps."),save:Z.string().optional().describe('Save these steps under this name when the flow passes, so it can be replayed with flow:"name".'),include:Z.enum(["all","screen","none"]).optional().describe("What to report after the flow: 'all' (default) = available final element tree + newly observed captured network/console entries; 'screen' = available final element tree only; 'none' = step results only."),updateSnapshots:Z.boolean().optional().describe("Save every snapshot step's screen as its new baseline instead of comparing."),continueOnFailure:Z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),real:Z.boolean().optional().describe("Make every tap a real OS touch (iOS Simulator, Android), found by testID, text or native label. Default false."),deviceId:L};var Ar=async()=>Ht("run_flow is not ready.");function Dp(e,n){e.registerTool("run_flow",{_meta:He(),description:"Run supported UI actions and element checks in sequence, with per-step results. Stops on the first failed step unless continueOnFailure is true. Depending on include, attempts to return the final element tree and newly observed captured network/console entries. Background activity can appear in that output; it does not establish causation. Taps use exposed app handlers by default; a tap with no handler match (a native tab bar) falls back to a real touch on that native label. Pass real: true to make every tap a real touch. A tap waits up to 2 s for its target to show, so steps rarely need waitMs between them. A swipe step is a real swipe; add untilEnd: true to scroll to the end of a list. Review failures and check the expected behavior. To wait for something slow, use waitFor or waitGone with a timeoutMs, not a long waitMs. Needs the app interaction integration. Works on the free tier.",inputSchema:Tp},async t=>Ar(t));Ar=async t=>{const i=Date.now();try{let r=t.steps;if(t.flow){if(!ts.test(t.flow))return Ht(`Flow names use letters, numbers, - and _ (got "${t.flow}").`);try{r=JSON.parse(await ri.readFile(oi.join(ai(),`${t.flow}.json`),"utf8")).steps}catch{const _=await yp();return Ht(`No saved flow "${t.flow}". ${_.length?`Saved flows: ${_.join(", ")}.`:'No flows are saved yet; run one with save:"name" first.'}`)}}if(!r||r.length===0)return Ht('Pass steps, or flow:"name" to replay a saved flow.');if(t.save&&!ts.test(t.save))return Ht(`Flow names use letters, numbers, - and _ (got "${t.save}").`);const o=n.requireProDevice(t.deviceId);const a=t.include??"all";const s=Rr(n);let c=null;const l=()=>c??=ge(n,o).then(async _=>{await s.primeScreen(o,_.host,_.online);return _});let d;const u=async(_,S,A,O)=>{const F=S?await s.buoyCover(o,_,A):null;if(F)throw new Error(F);const P=O?600:void 0;const K=await s.touch(_,A,P,d);if(K!==void 0)d=K;await s.mark(o,_,P?"hold":"tap",[[A.x,A.y]],void 0,P??150,K);await Xe(120);return`${O?"long-pressed":"tapped"} ${A.what} (real touch)`};const h=async(_,S,A)=>{const{host:O,online:F}=await l();const P=Date.now();let K=false;for(;;){let N=await s.findSpot(o,O,_,F,"app");if("miss"in N&&!N.tie&&(K||!F||Date.now()-P>=600)){if(!K){K=true;N=await s.findSpot(o,O,_,F,"native")}if("miss"in N&&!N.tie)N=await s.findSpot(o,O,_,F,"loose")}if(!("miss"in N))return u(O,F,N,S);if(N.tie||Date.now()-P>=A)throw new Error(N.miss);await Xe(Jt)}};const b=async(_,S,A)=>{let O;try{O=await l()}catch{return null}if(O.host.platform==="ios-device")return null;const F=await s.findSpot(o,O.host,_,O.online,A?"nativeExact":"native").catch(()=>null);if(!F||"miss"in F)return null;return u(O.host,O.online,F,S)};const f=async()=>{const _=await n.callAction(si,"describeScreen",{},o);return _?.elements??[]};const w=async()=>{try{const _=await n.callAction(ns,"ping",{},o,{timeoutMs:Sp,silent:true});return typeof _?.startedAt==="number"?_.startedAt:null}catch{return null}};const g=async(_,S)=>{const A=Date.now();while(Date.now()-A<S){const O=await w();if(O!==null&&O!==_)return Date.now()-A;await Xe(Jt)}return null};const m=new Set;const p=new Set;const y=new Set((n.connectedDevices().find(_=>_.deviceId===o)?.tools??[]).map(_=>_.toolId));if(a==="all"){if(y.has("console")){const{payload:_}=await n.getSnapshot("console",o).catch(()=>({payload:null}));const S=_?.entries??[];for(const A of S)m.add(A.id)}if(y.has("network")){const{payload:_}=await n.getSnapshot("network",o).catch(()=>({payload:null}));const S=Array.isArray(_)?_:_?.events??[];for(const A of S)p.add(A.id)}}const v=async(_,S)=>{const A=Date.now();let O=false;for(;;){if(Date.now()-A>=S||Date.now()-i>Mr){return{ok:false,ms:Date.now()-A,unread:!O}}let F;try{F=await f();O=true}catch{await Xe(Jt);continue}if(_(F))return{ok:true,ms:Date.now()-A,unread:false};await Xe(Jt)}};const $=_=>`could not read the screen within ${_}ms, so this step is unknown`;const T=[];let k=null;let D=0;for(let _=0;_<r.length;_++){const S=r[_];const A=`${_+1}`;const O=S.timeoutMs??kp;if(Date.now()-i>Mr){T.push(`${A} \u2717 flow exceeded ${Mr/1e3}s \u2014 stopped here`);k=_;break}const F=[];const P=[];const K=S.reload||S.tap||S.swipe||S.type!==void 0||S.waitMs||S.waitFor||S.waitGone||S.expect||S.expectGone||S.snapshot;if(!K){T.push(`${_+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${S.set!==void 0?" (`set` needs `tap` naming the control)":""}`);k=k??_;if(!t.continueOnFailure)break;continue}try{if(S.reload){const N=await w();await n.callAction(ns,"reloadApp",{strategy:"auto"},o);await Xe(_p);const q=await g(N,Math.max(O,2e4));if(q==null){F.push("app did not come back after reload")}else{P.push(`reloaded (${q}ms)`);await v(V=>V.length>0,1e4)}}if(S.type!==void 0){const N=S.into??S.tap;if(!N){F.push("`type` needs `into` (which input to type into)")}else{const q=await n.callAction(si,"tapElement",{...ss(N),text:Bn(S.type)},o);if(!q?.tapped){F.push(q?.reason??`could not type into "${N}"`)}else if(q.matched?.via&&q.matched.via!=="onChangeText"){F.push(`"${N}" is not a text input \u2014 it ran ${q.matched.via} instead, and the text was not entered`)}else{P.push(`typed ${JSON.stringify(S.type)} into "${N}"`)}}}else if(S.tap&&(S.real??t.real)&&S.set===void 0){P.push(await h(S.tap,S.longPress,S.timeoutMs??3e3))}else if(S.tap){const N={...ss(S.tap)};if(S.longPress)N.longPress=true;if(S.set!==void 0)N.value=S.set;let q=await n.callAction(si,"tapElement",N,o);for(let B=0;!q?.tapped&&/No element matched/.test(q?.reason??"")&&B<$p;B+=Jt){await Xe(Jt);q=await n.callAction(si,"tapElement",N,o)}const V=/isn't showing|No element matched|no JS press handler|can't be driven in JS/;const pe=/equally well/.test(q?.reason??"");const Q=!q?.tapped&&S.set===void 0&&(pe||V.test(q?.reason??""))?await b(S.tap,S.longPress,pe):null;if(Q){P.push(Q)}else if(!q?.tapped){F.push(q?.reason??`no element matched "${S.tap}"`)}else{const B=q.matched;const z=B?.text??B?.label??B?.name??S.tap;if(S.set!==void 0&&B?.via&&B.via!=="onValueChange"){F.push(`"${z}" is not a toggle/slider \u2014 it ran ${B.via} instead, so it was not set to ${JSON.stringify(S.set)}`)}else{P.push(S.set!==void 0?`set "${z}" \u2192 ${JSON.stringify(B?.value??S.set)}`:`${S.longPress?"long-pressed":"tapped"} "${z}"`)}}}if(!F.length&&S.swipe){const{host:N,online:q}=await l();const V=await s.screenOf(N);const pe=S.swipe;const{from:Q,to:B}=us(pe,!!S.untilEnd);const z=[Q[0]*V.width,Q[1]*V.height];const re=[B[0]*V.width,B[1]*V.height];const le=q?await s.buoyCover(o,N,{x:z[0],y:z[1]}):null;if(le)throw new Error(le);if(S.untilEnd){const{swipes:Ie,atEnd:Di,lastRow:Dt}=await s.scrollToEnd(o,N,z,re,250);if(Di)P.push(`swiped ${pe} to the end (${Ie} swipes)${Dt?`; the last row is "${Dt}"`:""}`);else F.push(`swiped ${pe} ${Ie} times and the screen was still moving`)}else{const Ie=await s.swipe(N,z,re,300);await s.mark(o,N,"swipe",[z],[re],300,Ie);await Xe(400);P.push(`swiped ${pe}`)}}if(!F.length&&S.waitMs){await Xe(S.waitMs);P.push(`waited ${S.waitMs}ms`)}if(!F.length&&S.waitFor){const N=await v(q=>q.some(V=>li(V,S.waitFor)),O);if(N.ok)P.push(`"${S.waitFor}" appeared (${N.ms}ms)`);else F.push(N.unread?$(O):`"${S.waitFor}" never appeared within ${O}ms`)}if(!F.length&&S.waitGone){const N=await v(q=>!q.some(V=>li(V,S.waitGone)),O);if(N.ok)P.push(`"${S.waitGone}" gone (${N.ms}ms)`);else F.push(N.unread?$(O):`"${S.waitGone}" was still there after ${O}ms`)}if(!F.length&&S.expect){const N=await v(q=>q.some(V=>li(V,S.expect)),S.timeoutMs??ci);if(N.ok)P.push(`"${S.expect}" is on screen`);else F.push(N.unread?$(S.timeoutMs??ci):`expected "${S.expect}" on screen \u2014 not found`)}if(!F.length&&S.expectGone){const N=await v(q=>!q.some(V=>li(V,S.expectGone)),S.timeoutMs??ci);if(N.ok)P.push(`"${S.expectGone}" is gone`);else F.push(N.unread?$(S.timeoutMs??ci):`expected "${S.expectGone}" to be gone \u2014 still on screen`)}if(!F.length&&S.snapshot){const{outcome:N}=await Ua(n,S.snapshot,{deviceId:o,update:t.updateSnapshots});const q=S.maxChange??.05;if(N.changed&&(N.texts>0||N.percent>q)){F.push(`${N.summary}${N.texts>0?"":` (more than ${q}%)`}. Run screenshot_component with baseline "${S.snapshot}" to see the diff, or updateSnapshots: true if it is expected.`)}else P.push(N.summary)}}catch(N){F.push(rs(N))}if(F.length){T.push(`${A} \u2717 ${cs(S)} \u2014 ${F.join("; ")}`);k=k??_;if(!t.continueOnFailure)break}else{D++;T.push(`${A} \u2713 ${P.join(", ")||cs(S)}`)}}const E=((Date.now()-i)/1e3).toFixed(1);const I=k===null?`**Flow: ${D}/${r.length} steps OK (${E}s)**`:`**Flow FAILED at step ${k+1} \u2014 ${D}/${r.length} steps OK (${E}s)**`;const R=[I,T.join("\n")];if(a!=="none"){R.push(`**Screen now:**${await Re(n,o,void 0,0)}`)}if(a==="all"){if(y.has("network")){const{payload:_}=await n.getSnapshot("network",o).catch(()=>({payload:null}));const S=Array.isArray(_)?_:_?.events??[];const A=S.filter(O=>!p.has(O.id)).slice(-15);if(A.length){R.push(`**Network during this flow (${A.length}):**
595
+ `;var Mp="// swift-interface-format-version: 1.0\n// swift-compiler-version: Apple Swift version 6.4\n// swift-module-flags: -target arm64-apple-macosx15.0 -enable-library-evolution -module-name CoreDevice\n// Reconstructed API subset. No replacement framework code is linked.\n// Non-frozen types and protocol/class calls resolve through Apple metadata and thunks.\nimport Foundation\nimport CoreGraphics\npublic struct Capability {}\npublic protocol CoreDeviceServiceConnection {}\npublic struct CoreDeviceService {\n public static var sharedConnection: any CoreDeviceServiceConnection { get }\n}\npublic enum DeviceVisibilityClass: Swift.Hashable {\n case `default`, wirelessPairableDevices, devicesBeingProvisioned, simulators\n public static func == (lhs: DeviceVisibilityClass, rhs: DeviceVisibilityClass) -> Swift.Bool\n public func hash(into hasher: inout Swift.Hasher)\n public var hashValue: Swift.Int { get }\n}\npublic class DeviceManager {\n public static var shared: DeviceManager { get }\n public init(serviceConnection: any CoreDeviceServiceConnection, allowedDeviceVisibilityClasses: Swift.Set<DeviceVisibilityClass>)\n public func awaitFullInitialization() async\n public func waitForPostPluginLoadCheckIn() async\n public var devices: [Foundation.UUID: RemoteDevice] { get }\n public func deviceNamed(_ name: Swift.String) throws -> RemoteDevice\n}\npublic class RemoteDevice {\n public var deviceIdentifier: Foundation.UUID { get }\n public func getImplementation<C: DeviceCapability>(for capability: CapabilityStaticMember<C>) async throws -> C.ProtocolType\n}\npublic protocol DeviceCapability {\n associatedtype ProtocolType\n static var capability: Capability { get }\n}\npublic struct CapabilityStaticMember<C> {\n public init()\n}\npublic struct DigitizerHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDDigitizer\n public static var capability: Capability { get }\n}\npublic protocol DeviceCapabilityProtocol {\n static var capability: Capability { get }\n static var identifier: Swift.String { get }\n static var identifierSuffix: Swift.String { get }\n static var priority: Swift.Int { get }\n}\npublic protocol HIDDigitizer: DeviceCapabilityProtocol {\n func send(pointOne: DigitizerPoint, pointTwo: DigitizerPoint?, eventType: DigitizerEventType, edge: DigitizerEdge, target: DigitizerTarget) throws\n func sendBarrier()\n}\npublic struct DigitizerPoint {\n public init(x: Swift.Double, y: Swift.Double)\n}\npublic enum DigitizerEventType { case start, position, end }\npublic enum DigitizerEdge { case none, top, left, bottom, right }\npublic enum DigitizerTarget { case mainScreen, display1, display2, display3, display4, display5, display6, display7, display8, display9, display10 }\npublic struct KeyboardHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDKeyboard\n public static var capability: Capability { get }\n}\npublic protocol HIDKeyboard: DeviceCapabilityProtocol {\n func send(key: HIDKeyboardUsageCode, state: HIDButtonState) throws\n func sendBarrier()\n}\npublic struct HIDKeyboardUsageCode {\n public init(rawValue: Swift.UInt16)\n}\npublic enum HIDButtonState { case down, up }\npublic struct ButtonHIDCapability: DeviceCapability {\n public typealias ProtocolType = any HIDButton\n public static var capability: Capability { get }\n}\npublic protocol HIDButton: DeviceCapabilityProtocol {\n func sendCustomButton(usagePage: Swift.Int, usageCode: Swift.Int, state: HIDButtonState) throws\n func sendBarrier()\n}\n";var Ap="718c261deb06";var _t=Dp(Tp);var os="/Library/Developer/PrivateFrameworks";var yn=null;var Or="";var as=0;var Rr;var ss;var $n=null;async function Cp(){try{const{stdout:e}=await _t("xcode-select",["-p"],{timeout:1e4});const n=De.join(process.env.DEVELOPER_DIR||e.trim(),"..","Info.plist");const{stdout:t}=await _t("plutil",["-extract","DTXcodeBuild","raw",n],{timeout:1e4});return t.trim().replace(/[^\w.-]/g,"")||"unknown"}catch{return"unknown"}}async function Ep(e){if(process.platform!=="darwin")return null;const n=De.join(rs.homedir(),".buoy","hidtouch",`${e}-${Ap}`);const t=De.join(n,"hidtouch");try{await Oe.access(t);return t}catch{}try{await Oe.access(De.join(os,"CoreDevice.framework"))}catch{Or="CoreDevice is not installed (it ships with Xcode 15 and later).";return null}const i=await Oe.mkdtemp(De.join(rs.tmpdir(),"buoy-hidtouch-"));try{await Oe.mkdir(De.join(i,"modules"));await Oe.writeFile(De.join(i,"hidtouch.swift"),Ip);await Oe.writeFile(De.join(i,"modules","CoreDevice.swiftinterface"),Mp);const r=De.join(i,"hidtouch");await _t("xcrun",["swiftc","-O","-module-cache-path",De.join(i,"cache"),"-I",De.join(i,"modules"),"-F",os,"-framework","CoreDevice","-parse-as-library",De.join(i,"hidtouch.swift"),"-o",r],{timeout:18e4,maxBuffer:8*1024*1024,env:{...process.env,TMPDIR:i}});await Oe.mkdir(n,{recursive:true});const o=`${t}.${process.pid}`;await Oe.copyFile(r,o);await Oe.chmod(o,493);await Oe.rename(o,t);return t}catch(r){const o=r;Or=(o.stderr||o.message||"").trim().split("\n").slice(-3).join(" ").slice(0,300);return null}finally{await Oe.rm(i,{recursive:true,force:true}).catch(()=>void 0)}}function Ie(){const e=Date.now();const n=!yn||e-as>=1e4||Rr!==process.env.DEVELOPER_DIR;as=e;if($n)return $n;if(!n)return yn;Rr=process.env.DEVELOPER_DIR;const t=Rr;$n=Cp().then(i=>{const r=JSON.stringify([t,i]);if(!yn||ss!==r){ss=r;Or="";yn=Ep(i)}return yn}).finally(()=>{$n=null});return $n}async function kn(e){try{const{stdout:n}=await _t("xcrun",["devicectl","device","orientation","get","--device",e],{timeout:15e3});const t=/Orientation:\s*(\w+)/.exec(n)?.[1];return t==="portrait"||t==="portraitUpsideDown"||t==="landscapeLeft"||t==="landscapeRight"?t:null}catch{return null}}async function Pr(e,n){if(!n)return null;try{const{stdout:t}=await _t("xcrun",["simctl","get_app_container",e,n,"app"],{timeout:1e4});const{stdout:i}=await _t("plutil",["-convert","json","-o","-",De.join(t.trim(),"Info.plist")],{timeout:1e4});const r=JSON.parse(i);const{stdout:o}=await _t("xcrun",["simctl","getenv",e,"SIMULATOR_MODEL_IDENTIFIER"],{timeout:1e4});const a=o.trim().startsWith("iPad")?"ipad":"iphone";const s=r[`UISupportedInterfaceOrientations~${a}`]??r.UISupportedInterfaceOrientations;if(!Array.isArray(s)||!s.every(c=>typeof c==="string"))return null;return s.includes("UIInterfaceOrientationPortraitUpsideDown")}catch{return null}}function di(e,n,t,i,r){if(i==="portraitUpsideDown"){if(r==null)throw new Error("Upside-down touch needs the app turn list. Turn the device upright and try again.");return r?[1-e,1-n]:[e,n]}if(!t)return[e,n];if(i==="landscapeRight")return[n,1-e];return[1-n,e]}async function ui(e,n,t,i=false){try{const r=n[n.indexOf("--device")+1];await Ba(e,r,n,t)}catch(r){const o=r;const a=o.stderr||o.message||"";if(/HID Digitizer/i.test(a)&&/not supported/i.test(a)){console.error("[buoy:hid]",a);throw new Error(i?"Your iPhone isn't connected. Unlock it and plug it in, or put it on the same Wi-Fi. Then try again.":"Touch is not supported on this iOS Simulator.")}throw new Error(`The touch failed: ${a.replace(/^hidtouch:\s*/,"").split("\n")[0].slice(0,300)}`)}}var Nr=e=>Math.min(1,Math.max(0,e));var dt=e=>Nr(e).toFixed(5);async function cs(e,n,t,i,r,o,a){const s=t.width>t.height;const c=a!==void 0?a:await kn(n);const l=c==="portraitUpsideDown"?await Pr(n,t.bundleId):null;const[d,u]=di(i/t.width,r/t.height,s,c,l);await ui(e,o?["--device",n,"long-press",dt(d),dt(u),String(Math.round(o))]:["--device",n,"tap",dt(d),dt(u)],void 0,t.isPhone);return c}async function Op(e,n,t,i,r,o,a=0,s=0,c){const l=t.width>t.height;const d=c!==void 0?c:await kn(n);const u=d==="portraitUpsideDown"?await Pr(n,t.bundleId):null;const h=di(i[0]/t.width,i[1]/t.height,l,d,u);const b=di(r[0]/t.width,r[1]/t.height,l,d,u);const f=String(Math.round(Math.min(1e4,Math.max(1,o))));const w=[dt(h[0]),dt(h[1]),dt(b[0]),dt(b[1])];const g=a>0?["--device",n,"drag",...w,String(Math.round(Math.min(1e4,a))),f]:s>0?["--device",n,"scroll",...w,f,String(Math.round(Math.min(1e4,s)))]:["--device",n,"swipe",...w,f];await ui(e,g,void 0,t.isPhone);return d}async function Rp(e,n,t,i,r,o,a,s){const c=t.width>t.height;const l=await kn(n);const d=l==="portraitUpsideDown"?await Pr(n,t.bundleId):null;const u=c?{w:t.height,h:t.width}:{w:t.width,h:t.height};const h=[i,r,o,a].flatMap(b=>{const[f,w]=di(b[0]/t.width,b[1]/t.height,c,l,d);return[(Nr(f)*u.w).toFixed(2),(Nr(w)*u.h).toFixed(2)]});await ui(e,["--device",n,"--points",String(u.w),String(u.h),"two",...h,String(Math.round(Math.min(1e4,Math.max(1,s))))],void 0,t.isPhone);return l}var Pp=/^[\x20-\x7e\n]*$/;async function ls(e,n,t){if(!Pp.test(t))throw new Error("The simulator keyboard types plain ASCII letters, digits, punctuation and newlines only. For other text use tap_element with text.");if(!t)return;await ui(e,["--device",n,"type","-"],t)}import{z as hi}from"zod";import{promises as pi}from"fs";import _n from"path";import{randomUUID as Np}from"crypto";import{z as Z}from"zod";import{promises as fi}from"fs";import mi from"path";var wi=()=>mi.join(process.cwd(),".buoy","flows");var ds=/^[A-Za-z0-9_-]{1,64}$/;async function Fp(){try{return(await fi.readdir(wi())).filter(e=>e.endsWith(".json")).map(e=>e.slice(0,-5))}catch{return[]}}var gi="highlight-updates";var us="app";var hs=e=>({content:[{type:"text",text:e}]});var Vt=e=>({isError:true,content:[{type:"text",text:e}]});var ps=e=>e instanceof Error?e.message:String(e);var Qe=e=>new Promise(n=>setTimeout(n,e));var Yt=250;var Bp=2e3;var jp=5e3;var bi=2e3;var Lp=1200;var qp=1500;var Fr=12e4;function vi(e,n){if(e.testID&&e.testID===n)return true;const t=n.toLowerCase();return[e.testID,e.text,e.label,e.name].filter(Boolean).some(i=>String(i).toLowerCase().includes(t))}var fs=["tap","waitFor","waitGone","expect","expectGone","reload","wait","swipe"];function Up(e){if(!e||typeof e!=="object"||Array.isArray(e))return e;const n={...e};const t=a=>{if(!a||typeof a!=="object"||Array.isArray(a))return a;const s=a;return[s.testID,s.text,s.query,s.label,s.name].find(c=>typeof c==="string")??a};for(const a of["tap","into","waitFor","waitGone","expect","expectGone"])n[a]=t(n[a]);for(const a of Object.keys(n))if(n[a]===void 0)delete n[a];if(typeof n.into==="string")return n;const i=typeof n.type==="string"&&fs.includes(n.type)?"type":typeof n.action==="string"&&fs.includes(n.action)?"action":null;if(!i)return n;const r=n[i];delete n[i];const o=[n.query,n.text,n.target,n.selector,n.element,n.testID].map(t).find(a=>typeof a==="string");for(const a of["query","text","target","selector","element","testID"])delete n[a];if(r==="reload")n.reload=true;else if(r==="swipe"){n.swipe=n.direction??o??"up";delete n.direction}else if(r==="wait")n.waitMs=n.waitMs??n.ms??n.durationMs??n.timeoutMs;else if(o!==void 0&&n[r]===void 0)n[r]=o;delete n.ms;delete n.durationMs;return n}var ms=Z.preprocess(Up,Z.object({tap:Z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:Z.boolean().optional().describe("Make `tap` a long-press instead."),type:Z.string().optional().describe("Text to type (needs `into`)."),into:Z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:Z.union([Z.number(),Z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),swipe:Z.enum(["up","down","left","right"]).optional().describe('A real swipe, the way the finger moves: "up" scrolls down the list.'),untilEnd:Z.boolean().optional().describe("With swipe: keep swiping until the screen stops moving (the end of the list)."),real:Z.boolean().optional().describe("Make this step's tap a real touch. Default: the flow's `real`."),reload:Z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:Z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000). To wait for a result (a slow request, a new screen), use waitFor with timeoutMs instead: it stops as soon as the element shows."),waitFor:Z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:Z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:Z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:Z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),snapshot:Z.string().optional().describe("Compare the whole screen to the baseline with this name (saved on the first run, like screenshot_component baseline). Fails the step on a text change or a change bigger than maxChange."),maxChange:Z.number().min(0).max(100).optional().describe("snapshot: allowed changed share of the screen, in percent. Default 0.05."),timeoutMs:Z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")}).describe("One flow step. Order within a step: reload \u2192 tap/type/set/swipe \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect."));var zp={steps:Z.array(ms).min(1).optional().describe("Steps to run, in order. Leave out when replaying a saved flow."),flow:Z.string().optional().describe("Replay a saved flow by name (from .buoy/flows in the project) instead of passing steps."),save:Z.string().optional().describe('Save these steps under this name when the flow passes, so it can be replayed with flow:"name".'),include:Z.enum(["all","screen","none"]).optional().describe("What to report after the flow: 'all' (default) = available final element tree + newly observed captured network/console entries; 'screen' = available final element tree only; 'none' = step results only."),updateSnapshots:Z.boolean().optional().describe("Save every snapshot step's screen as its new baseline instead of comparing."),continueOnFailure:Z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),real:Z.boolean().optional().describe("Make every tap a real OS touch (iOS Simulator, Android), found by testID, text or native label. Default false."),deviceId:L};var Br=async()=>Vt("run_flow is not ready.");function Wp(e,n){e.registerTool("run_flow",{_meta:Ge(),description:"Run supported UI actions and element checks in sequence, with per-step results. Stops on the first failed step unless continueOnFailure is true. Depending on include, attempts to return the final element tree and newly observed captured network/console entries. Background activity can appear in that output; it does not establish causation. Taps use exposed app handlers by default; a tap with no handler match (a native tab bar) falls back to a real touch on that native label. Pass real: true to make every tap a real touch. A tap waits up to 2 s for its target to show, so steps rarely need waitMs between them. A swipe step is a real swipe; add untilEnd: true to scroll to the end of a list. Review failures and check the expected behavior. To wait for something slow, use waitFor or waitGone with a timeoutMs, not a long waitMs. Needs the app interaction integration. Works on the free tier.",inputSchema:zp},async t=>Br(t));Br=async t=>{const i=Date.now();try{let r=t.steps;if(t.flow){if(!ds.test(t.flow))return Vt(`Flow names use letters, numbers, - and _ (got "${t.flow}").`);try{r=JSON.parse(await fi.readFile(mi.join(wi(),`${t.flow}.json`),"utf8")).steps}catch{const _=await Fp();return Vt(`No saved flow "${t.flow}". ${_.length?`Saved flows: ${_.join(", ")}.`:'No flows are saved yet; run one with save:"name" first.'}`)}}if(!r||r.length===0)return Vt('Pass steps, or flow:"name" to replay a saved flow.');if(t.save&&!ds.test(t.save))return Vt(`Flow names use letters, numbers, - and _ (got "${t.save}").`);const o=n.requireProDevice(t.deviceId);const a=t.include??"all";const s=Ur(n);let c=null;const l=()=>c??=ge(n,o).then(async _=>{await s.primeScreen(o,_.host,_.online);return _});let d;const u=async(_,S,A,O)=>{const F=S?await s.buoyCover(o,_,A):null;if(F)throw new Error(F);const P=O?600:void 0;const K=await s.touch(_,A,P,d);if(K!==void 0)d=K;await s.mark(o,_,P?"hold":"tap",[[A.x,A.y]],void 0,P??150,K);await Qe(120);return`${O?"long-pressed":"tapped"} ${A.what} (real touch)`};const h=async(_,S,A)=>{const{host:O,online:F}=await l();const P=Date.now();let K=false;for(;;){let N=await s.findSpot(o,O,_,F,"app");if("miss"in N&&!N.tie&&(K||!F||Date.now()-P>=600)){if(!K){K=true;N=await s.findSpot(o,O,_,F,"native")}if("miss"in N&&!N.tie)N=await s.findSpot(o,O,_,F,"loose")}if(!("miss"in N))return u(O,F,N,S);if(N.tie||Date.now()-P>=A)throw new Error(N.miss);await Qe(Yt)}};const b=async(_,S,A)=>{let O;try{O=await l()}catch{return null}if(O.host.platform==="ios-device")return null;const F=await s.findSpot(o,O.host,_,O.online,A?"nativeExact":"native").catch(()=>null);if(!F||"miss"in F)return null;return u(O.host,O.online,F,S)};const f=async()=>{const _=await n.callAction(gi,"describeScreen",{},o);return _?.elements??[]};const w=async()=>{try{const _=await n.callAction(us,"ping",{},o,{timeoutMs:qp,silent:true});return typeof _?.startedAt==="number"?_.startedAt:null}catch{return null}};const g=async(_,S)=>{const A=Date.now();while(Date.now()-A<S){const O=await w();if(O!==null&&O!==_)return Date.now()-A;await Qe(Yt)}return null};const m=new Set;const p=new Set;const y=new Set((n.connectedDevices().find(_=>_.deviceId===o)?.tools??[]).map(_=>_.toolId));if(a==="all"){if(y.has("console")){const{payload:_}=await n.getSnapshot("console",o).catch(()=>({payload:null}));const S=_?.entries??[];for(const A of S)m.add(A.id)}if(y.has("network")){const{payload:_}=await n.getSnapshot("network",o).catch(()=>({payload:null}));const S=Array.isArray(_)?_:_?.events??[];for(const A of S)p.add(A.id)}}const v=async(_,S)=>{const A=Date.now();let O=false;for(;;){if(Date.now()-A>=S||Date.now()-i>Fr){return{ok:false,ms:Date.now()-A,unread:!O}}let F;try{F=await f();O=true}catch{await Qe(Yt);continue}if(_(F))return{ok:true,ms:Date.now()-A,unread:false};await Qe(Yt)}};const $=_=>`could not read the screen within ${_}ms, so this step is unknown`;const T=[];let k=null;let D=0;for(let _=0;_<r.length;_++){const S=r[_];const A=`${_+1}`;const O=S.timeoutMs??jp;if(Date.now()-i>Fr){T.push(`${A} \u2717 flow exceeded ${Fr/1e3}s \u2014 stopped here`);k=_;break}const F=[];const P=[];const K=S.reload||S.tap||S.swipe||S.type!==void 0||S.waitMs||S.waitFor||S.waitGone||S.expect||S.expectGone||S.snapshot;if(!K){T.push(`${_+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${S.set!==void 0?" (`set` needs `tap` naming the control)":""}`);k=k??_;if(!t.continueOnFailure)break;continue}try{if(S.reload){const N=await w();await n.callAction(us,"reloadApp",{strategy:"auto"},o);await Qe(Lp);const q=await g(N,Math.max(O,2e4));if(q==null){F.push("app did not come back after reload")}else{P.push(`reloaded (${q}ms)`);await v(V=>V.length>0,1e4)}}if(S.type!==void 0){const N=S.into??S.tap;if(!N){F.push("`type` needs `into` (which input to type into)")}else{const q=await n.callAction(gi,"tapElement",{...ws(N),text:Kn(S.type)},o);if(!q?.tapped){F.push(q?.reason??`could not type into "${N}"`)}else if(q.matched?.via&&q.matched.via!=="onChangeText"){F.push(`"${N}" is not a text input \u2014 it ran ${q.matched.via} instead, and the text was not entered`)}else{P.push(`typed ${JSON.stringify(S.type)} into "${N}"`)}}}else if(S.tap&&(S.real??t.real)&&S.set===void 0){P.push(await h(S.tap,S.longPress,S.timeoutMs??3e3))}else if(S.tap){const N={...ws(S.tap)};if(S.longPress)N.longPress=true;if(S.set!==void 0)N.value=S.set;let q=await n.callAction(gi,"tapElement",N,o);for(let B=0;!q?.tapped&&/No element matched/.test(q?.reason??"")&&B<Bp;B+=Yt){await Qe(Yt);q=await n.callAction(gi,"tapElement",N,o)}const V=/isn't showing|No element matched|no JS press handler|can't be driven in JS/;const pe=/equally well/.test(q?.reason??"");const Q=!q?.tapped&&S.set===void 0&&(pe||V.test(q?.reason??""))?await b(S.tap,S.longPress,pe):null;if(Q){P.push(Q)}else if(!q?.tapped){F.push(q?.reason??`no element matched "${S.tap}"`)}else{const B=q.matched;const z=B?.text??B?.label??B?.name??S.tap;if(S.set!==void 0&&B?.via&&B.via!=="onValueChange"){F.push(`"${z}" is not a toggle/slider \u2014 it ran ${B.via} instead, so it was not set to ${JSON.stringify(S.set)}`)}else{P.push(S.set!==void 0?`set "${z}" \u2192 ${JSON.stringify(B?.value??S.set)}`:`${S.longPress?"long-pressed":"tapped"} "${z}"`)}}}if(!F.length&&S.swipe){const{host:N,online:q}=await l();const V=await s.screenOf(N);const pe=S.swipe;const{from:Q,to:B}=ys(pe,!!S.untilEnd);const z=[Q[0]*V.width,Q[1]*V.height];const re=[B[0]*V.width,B[1]*V.height];const le=q?await s.buoyCover(o,N,{x:z[0],y:z[1]}):null;if(le)throw new Error(le);if(S.untilEnd){const{swipes:Me,atEnd:Fi,lastRow:At}=await s.scrollToEnd(o,N,z,re,250);if(Fi)P.push(`swiped ${pe} to the end (${Me} swipes)${At?`; the last row is "${At}"`:""}`);else F.push(`swiped ${pe} ${Me} times and the screen was still moving`)}else{const Me=await s.swipe(N,z,re,300);await s.mark(o,N,"swipe",[z],[re],300,Me);await Qe(400);P.push(`swiped ${pe}`)}}if(!F.length&&S.waitMs){await Qe(S.waitMs);P.push(`waited ${S.waitMs}ms`)}if(!F.length&&S.waitFor){const N=await v(q=>q.some(V=>vi(V,S.waitFor)),O);if(N.ok)P.push(`"${S.waitFor}" appeared (${N.ms}ms)`);else F.push(N.unread?$(O):`"${S.waitFor}" never appeared within ${O}ms`)}if(!F.length&&S.waitGone){const N=await v(q=>!q.some(V=>vi(V,S.waitGone)),O);if(N.ok)P.push(`"${S.waitGone}" gone (${N.ms}ms)`);else F.push(N.unread?$(O):`"${S.waitGone}" was still there after ${O}ms`)}if(!F.length&&S.expect){const N=await v(q=>q.some(V=>vi(V,S.expect)),S.timeoutMs??bi);if(N.ok)P.push(`"${S.expect}" is on screen`);else F.push(N.unread?$(S.timeoutMs??bi):`expected "${S.expect}" on screen \u2014 not found`)}if(!F.length&&S.expectGone){const N=await v(q=>!q.some(V=>vi(V,S.expectGone)),S.timeoutMs??bi);if(N.ok)P.push(`"${S.expectGone}" is gone`);else F.push(N.unread?$(S.timeoutMs??bi):`expected "${S.expectGone}" to be gone \u2014 still on screen`)}if(!F.length&&S.snapshot){const{outcome:N}=await Xa(n,S.snapshot,{deviceId:o,update:t.updateSnapshots});const q=S.maxChange??.05;if(N.changed&&(N.texts>0||N.percent>q)){F.push(`${N.summary}${N.texts>0?"":` (more than ${q}%)`}. Run screenshot_component with baseline "${S.snapshot}" to see the diff, or updateSnapshots: true if it is expected.`)}else P.push(N.summary)}}catch(N){F.push(ps(N))}if(F.length){T.push(`${A} \u2717 ${gs(S)} \u2014 ${F.join("; ")}`);k=k??_;if(!t.continueOnFailure)break}else{D++;T.push(`${A} \u2713 ${P.join(", ")||gs(S)}`)}}const C=((Date.now()-i)/1e3).toFixed(1);const I=k===null?`**Flow: ${D}/${r.length} steps OK (${C}s)**`:`**Flow FAILED at step ${k+1} \u2014 ${D}/${r.length} steps OK (${C}s)**`;const R=[I,T.join("\n")];if(a!=="none"){R.push(`**Screen now:**${await Pe(n,o,void 0,0)}`)}if(a==="all"){if(y.has("network")){const{payload:_}=await n.getSnapshot("network",o).catch(()=>({payload:null}));const S=Array.isArray(_)?_:_?.events??[];const A=S.filter(O=>!p.has(O.id)).slice(-15);if(A.length){R.push(`**Network during this flow (${A.length}):**
596
596
  `+A.map(O=>`${O.error||(O.status??0)>=400?"\u274C":"\u2705"} ${O.method??"GET"} ${O.path??O.url??"?"} \u2192 ${O.error??O.status??"pending"}`).join("\n"))}}if(y.has("console")){const{payload:_}=await n.getSnapshot("console",o).catch(()=>({payload:null}));const S=_?.entries??[];const A=S.filter(O=>!m.has(O.id)&&(O.level==="error"||O.level==="warning")).slice(-10);if(A.length){R.push(`**Console during this flow (${A.length}):**
597
- `+A.map(O=>`[${O.level==="error"?"ERR":"warn"}] ${Ip(Mp(xe(O.message??"")),220)}`).join("\n"))}}}if(t.save&&k===null){await ri.mkdir(ai(),{recursive:true});const _=oi.join(ai(),`${t.save}.json`);await ri.writeFile(_,JSON.stringify({steps:r},null,2)+"\n");R.push(`Saved as flow "${t.save}" (${oi.relative(process.cwd(),_)}). Replay with run_flow({flow:"${t.save}"}).`)}else if(t.save){R.push(`Not saved: the flow failed. Fix it, then run it again with save:"${t.save}".`)}return k===null?is(R.join("\n\n")):{...is(R.join("\n\n")),isError:true}}catch(r){return Ht(rs(r))}}}function ss(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function cs(e){if(e.tap)return`tap "${e.tap}"`;if(e.type!==void 0)return`type into "${e.into??"?"}"`;if(e.reload)return"reload";if(e.swipe)return`swipe ${e.swipe}${e.untilEnd?" to the end":""}`;if(e.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;if(e.expectGone)return`expectGone "${e.expectGone}"`;if(e.snapshot)return`snapshot "${e.snapshot}"`;return"step"}var Ip=(e,n=160)=>{const t=xe(e);return t.length>n?`${t.slice(0,n)}\u2026`:t};var Mp=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ");var ke=new Map;var bn=new Map;var Ap=e=>new Promise(n=>setTimeout(n,e));function ls(){return process.env.BUOY_PROJECT_ROOT||process.cwd()}function Ep(e=new Date){const n=t=>String(t).padStart(2,"0");return`${e.getFullYear()}${n(e.getMonth()+1)}${n(e.getDate())}-${n(e.getHours())}${n(e.getMinutes())}${n(e.getSeconds())}`}var Cp=/^(get_|list_|describe_screen$|screenshot_component$|screen_recording$|wait_for_automation$|compare_reports$)/;function Op(e,n,t){if(Cp.test(n)||ke.size===0)return;let i=typeof t?.deviceId==="string"?t.deviceId:void 0;if(!i){try{i=e.resolveDevice()}catch{return}}const r=i?ke.get(i):void 0;if(!r?.rec)return;const o=Object.entries(t??{}).filter(([a,s])=>a!=="deviceId"&&s!==void 0&&typeof s!=="object").map(([a,s])=>`${a}: ${String(s).slice(0,60)}`).join(", ");r.agent.push({at:Date.now(),t:0,source:"agent",status:"neutral",title:n,detail:o?Zn(o):void 0});clearTimeout(r.afterAction);r.afterAction=setTimeout(()=>void Er(e,r),700);r.afterAction.unref?.()}function Rp(e){return!!ke.get(e)?.rec}function Pp(e,n){const t=ke.get(e);if(t?.rec&&t.marks.length<500)t.marks.push(n)}function Er(e,n){return eo({tool:"screen_recording",level:"free"},()=>Np(e,n)).catch(()=>{n.failedPolls++})}async function Np(e,n){const t=e.connectedDevices().some(r=>r.deviceId===n.id);if(!t){n.failedPolls++;ds(n,(e.lastSync(n.id,"console")?.payload??{}).entries??[]);return}if(!e.connectedDevices().some(r=>r.deviceId===n.id&&fe(r))){n.notes.add("The events timeline needs Buoy Pro; this video shows console lines and agent steps only.")}else try{const r=await e.callAction("events","exportEvents",{settings:{format:"json",includeEventData:false},limit:500},n.id,{timeoutMs:4e3,silent:true});const o=r?.output?JSON.parse(r.output):null;const a=Array.isArray(o)?o:o?.events??[];for(const s of a){const c=s.timestamp?Date.parse(s.timestamp):NaN;if(!Number.isFinite(c))continue;const l=s.status==="error"||s.status==="success"||s.status==="pending"?s.status:"neutral";const d={at:c,t:0,source:s.source??"event",status:l,title:xe(Zn(s.title??"")),detail:s.subtitle?xe(Zn(s.subtitle)):void 0};n.events.set(`${c}|${d.source}|${d.title}|${d.detail??""}|${l}`,d)}if((r?.returned??0)>=500)n.notes.add("Busy app: some events may be missing from busy stretches.")}catch{n.failedPolls++}let i=[];try{const{payload:r}=await e.getSnapshot("console",n.id,{timeoutMs:2500,silent:true});i=r?.entries??[]}catch{i=(e.lastSync(n.id,"console")?.payload??{}).entries??[]}ds(n,i)}function ds(e,n){for(const t of n){if(typeof t.timestamp!=="number")continue;const[i,...r]=xe(Zn(t.message??"")).split("\n");const o=t.level==="error"?"error":t.level==="warning"?"warning":"neutral";e.events.set(`console|${t.timestamp}|${t.id??i}`,{at:t.timestamp,t:0,source:"console",status:o,title:i.slice(0,200),detail:r.join("\n").trim().slice(0,600)||void 0})}}async function Cr(e,n){const t=n.rec;if(n.poll)clearInterval(n.poll);clearTimeout(n.afterAction);n.stopped??={durationMs:await Wh(t)};const{durationMs:i}=n.stopped;await Er(e,n);if(n.failedPolls)n.notes.add(`The app's events could not be read ${n.failedPolls} time(s) during the recording (the app may have been reloading or stuck), so parts of the timeline may be missing.`);const r=t.startedAt;const o=r+i;const a=[...n.events.values(),...n.agent].filter(g=>g.at>=r-500&&g.at<=o+500).map(({at:g,...m})=>({...m,title:xe(m.title),detail:m.detail?xe(m.detail):void 0,t:Math.max(0,g-r)})).sort((g,m)=>g.t-m.t);if(!a.some(g=>g.source!=="agent"))n.notes.add("No app events were captured. Is the Events tool installed in the app?");const s=t.file.replace(/\.mp4$/,"");const c=Rt(t.host);const l=n.marks.filter(g=>g.at>=r-500&&g.at<=o).map(({at:g,...m})=>({...m,t:Math.max(0,g-r)})).sort((g,m)=>g.t-m.t);let d=null;if(l.length){try{d=await Kh(t.file,l,`${s}-taps.mp4`);if(d){const g=Gh(l);if(g)n.notes.add(g)}if(!d)n.notes.add("Install ffmpeg to also get a copy of the video with the taps drawn in. The viewer shows them either way.")}catch(g){n.notes.add(`Could not draw the taps into a copy of the video: ${j(g).slice(0,160)}. The viewer shows them either way.`)}}const u=[...n.notes];await ii.writeFile(`${s}.timeline.json`,JSON.stringify({device:c,startedAt:new Date(r).toISOString(),durationMs:i,notes:u,items:a,touches:l},null,1));await ii.writeFile(`${s}.html`,Vh({title:n.name,video:gn.basename(t.file),durationMs:i,items:a,device:c,notes:u,marks:l}));ke.delete(n.id);const h=g=>gn.relative(ls(),g);const b=new Map;for(const g of a)b.set(g.source,(b.get(g.source)??0)+1);const f=g=>`${(g/1e3).toFixed(1)}s`;const w=a.filter(g=>g.status==="error"||g.source==="route"||g.source==="agent").slice(0,14);return[`Saved a ${f(i)} recording of ${c}.`,`- Video: ${h(t.file)}`,`- Viewer (video + timeline, click an event to jump there): ${h(`${s}.html`)}`,`- Timeline data: ${h(`${s}.timeline.json`)}`,...d?[`- Video with touch dots: ${h(d)}`]:l.length?[`- The viewer draws the ${l.length} real touch(es) on the video.`]:[],"",a.length?`Events: ${[...b].map(([g,m])=>`${g} ${m}`).join(", ")}.`:"No events in this window.",...w.length?["What happened:",...w.map(g=>`- ${f(g.t)} [${g.source}]${g.status==="error"?" ERROR":""} ${g.title}${g.detail&&g.source!=="route"?` (${g.detail.split("\n")[0].slice(0,80)})`:""}`)]:[],...u.map(g=>`Note: ${g}`),"Recordings are kept out of git (.buoy/recordings/.gitignore). Secrets in logs are hidden, but the video shows the screen as it was: check it before sharing."].join("\n")}var Fp=1200;var Bp=3e3;function jp(e,n){tu(()=>ke.size>0);e.registerTool("screen_recording",{_meta:He(),description:"Record the screen of the iOS Simulator or Android device (start, do the steps, stop). On stop, saves an MP4 plus a viewer page that plays it beside a timeline of what the app did (web calls, query fetches, route changes, store writes, console errors) and what you did (your taps and other actions), and returns a short written summary. Real touches (native_tap, native_swipe, native_pinch) show as marks on the video, and with ffmpeg installed a copy of the MP4 has them drawn in. In .buoy/recordings. Use it to show a bug or a fix, e.g. for a pull request. Max 3 minutes. To record a set of taps or swipes, pass them as steps with start: Buoy records, runs them with real touches, stops and saves, all in one call.",inputSchema:{action:ni.enum(["start","stop"]),name:ni.string().optional().describe('start: a short name for the files, e.g. "checkout-timeout".'),steps:ni.array(as).min(1).optional().describe('start: run_flow steps to record, e.g. [{tap:"One"},{tap:"Two"}]. Taps and swipes are real touches, so they show on the video. Each tap waits for its target, so no waitMs is needed between taps. The video stops and saves when they finish; no stop call needed.'),serial:ni.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{if(t.action==="start"){const{id:o,host:a}=await ge(n,t.deviceId,{serial:t.serial});if(ke.has(o))return x("A recording is already running on this device. Stop it first.");const s=(t.name??"recording").trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"recording";const c={id:o,name:s,events:new Map,agent:[],notes:new Set,failedPolls:0,marks:[]};ke.set(o,c);let l;c.starting=new Promise(u=>l=u);try{const u=gn.join(ls(),".buoy","recordings");await ii.mkdir(u,{recursive:true});await ii.writeFile(gn.join(u,".gitignore"),"*\n",{flag:"wx"}).catch(()=>void 0);if(n.connectedDevices().some(b=>b.deviceId===o&&fe(b)))await n.getSnapshot("events",o,{timeoutMs:2e3,silent:true}).catch(()=>void 0);const h=gn.join(u,`${s}-${Ep()}-${vp().slice(0,6)}.mp4`);c.rec=await zh(a,h)}catch(u){ke.delete(o);throw u}finally{l()}c.poll=setInterval(()=>void Er(n,c),2e3);c.poll.unref?.();c.rec.stopTimer=setTimeout(()=>{c.finishing??=Cr(n,c);c.finishing.then(u=>bn.set(o,`The recording hit the ${Ut/6e4}-minute limit and was saved.
598
- ${u}`),u=>bn.set(o,`The recording hit the time limit and could not be saved: ${j(u)}`))},Ut);c.rec.stopTimer.unref?.();if(t.steps){const u=Date.now();const h=await Ar({steps:t.steps,real:true,include:"none",deviceId:o});const b=Math.max(Fp,Bp-(Date.now()-u));const[f]=await Promise.all([Re(n,o,void 0,0).catch(()=>""),Ap(b)]);c.finishing??=Cr(n,c);let w;try{w=await c.finishing}catch(m){c.finishing=void 0;return x(`${h.content[0]?.text??""}
597
+ `+A.map(O=>`[${O.level==="error"?"ERR":"warn"}] ${Hp(Jp(Te(O.message??"")),220)}`).join("\n"))}}}if(t.save&&k===null){await fi.mkdir(wi(),{recursive:true});const _=mi.join(wi(),`${t.save}.json`);await fi.writeFile(_,JSON.stringify({steps:r},null,2)+"\n");R.push(`Saved as flow "${t.save}" (${mi.relative(process.cwd(),_)}). Replay with run_flow({flow:"${t.save}"}).`)}else if(t.save){R.push(`Not saved: the flow failed. Fix it, then run it again with save:"${t.save}".`)}return k===null?hs(R.join("\n\n")):{...hs(R.join("\n\n")),isError:true}}catch(r){return Vt(ps(r))}}}function ws(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function gs(e){if(e.tap)return`tap "${e.tap}"`;if(e.type!==void 0)return`type into "${e.into??"?"}"`;if(e.reload)return"reload";if(e.swipe)return`swipe ${e.swipe}${e.untilEnd?" to the end":""}`;if(e.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;if(e.expectGone)return`expectGone "${e.expectGone}"`;if(e.snapshot)return`snapshot "${e.snapshot}"`;return"step"}var Hp=(e,n=160)=>{const t=Te(e);return t.length>n?`${t.slice(0,n)}\u2026`:t};var Jp=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ");var _e=new Map;var Sn=new Map;var Gp=e=>new Promise(n=>setTimeout(n,e));function bs(){return process.env.BUOY_PROJECT_ROOT||process.cwd()}function Kp(e=new Date){const n=t=>String(t).padStart(2,"0");return`${e.getFullYear()}${n(e.getMonth()+1)}${n(e.getDate())}-${n(e.getHours())}${n(e.getMinutes())}${n(e.getSeconds())}`}var Vp=/^(get_|list_|describe_screen$|screenshot_component$|screen_recording$|wait_for_automation$|compare_reports$)/;function Yp(e,n,t){if(Vp.test(n)||_e.size===0)return;let i=typeof t?.deviceId==="string"?t.deviceId:void 0;if(!i){try{i=e.resolveDevice()}catch{return}}const r=i?_e.get(i):void 0;if(!r?.rec)return;const o=Object.entries(t??{}).filter(([a,s])=>a!=="deviceId"&&s!==void 0&&typeof s!=="object").map(([a,s])=>`${a}: ${String(s).slice(0,60)}`).join(", ");r.agent.push({at:Date.now(),t:0,source:"agent",status:"neutral",title:n,detail:o?ci(o):void 0});clearTimeout(r.afterAction);r.afterAction=setTimeout(()=>void jr(e,r),700);r.afterAction.unref?.()}function Xp(e){return!!_e.get(e)?.rec}function Zp(e,n){const t=_e.get(e);if(t?.rec&&t.marks.length<500)t.marks.push(n)}function jr(e,n){return lo({tool:"screen_recording",level:"free"},()=>Qp(e,n)).catch(()=>{n.failedPolls++})}async function Qp(e,n){const t=e.connectedDevices().some(r=>r.deviceId===n.id);if(!t){n.failedPolls++;vs(n,(e.lastSync(n.id,"console")?.payload??{}).entries??[]);return}if(!e.connectedDevices().some(r=>r.deviceId===n.id&&fe(r))){n.notes.add("The events timeline needs Buoy Pro; this video shows console lines and agent steps only.")}else try{const r=await e.callAction("events","exportEvents",{settings:{format:"json",includeEventData:false},limit:500},n.id,{timeoutMs:4e3,silent:true});const o=r?.output?JSON.parse(r.output):null;const a=Array.isArray(o)?o:o?.events??[];for(const s of a){const c=s.timestamp?Date.parse(s.timestamp):NaN;if(!Number.isFinite(c))continue;const l=s.status==="error"||s.status==="success"||s.status==="pending"?s.status:"neutral";const d={at:c,t:0,source:s.source??"event",status:l,title:Te(ci(s.title??"")),detail:s.subtitle?Te(ci(s.subtitle)):void 0};n.events.set(`${c}|${d.source}|${d.title}|${d.detail??""}|${l}`,d)}if((r?.returned??0)>=500)n.notes.add("Busy app: some events may be missing from busy stretches.")}catch{n.failedPolls++}let i=[];try{const{payload:r}=await e.getSnapshot("console",n.id,{timeoutMs:2500,silent:true});i=r?.entries??[]}catch{i=(e.lastSync(n.id,"console")?.payload??{}).entries??[]}vs(n,i)}function vs(e,n){for(const t of n){if(typeof t.timestamp!=="number")continue;const[i,...r]=Te(ci(t.message??"")).split("\n");const o=t.level==="error"?"error":t.level==="warning"?"warning":"neutral";e.events.set(`console|${t.timestamp}|${t.id??i}`,{at:t.timestamp,t:0,source:"console",status:o,title:i.slice(0,200),detail:r.join("\n").trim().slice(0,600)||void 0})}}async function Lr(e,n){const t=n.rec;if(n.poll)clearInterval(n.poll);clearTimeout(n.afterAction);n.stopped??={durationMs:await sp(t)};const{durationMs:i}=n.stopped;await jr(e,n);if(n.failedPolls)n.notes.add(`The app's events could not be read ${n.failedPolls} time(s) during the recording (the app may have been reloading or stuck), so parts of the timeline may be missing.`);const r=t.startedAt;const o=r+i;const a=[...n.events.values(),...n.agent].filter(g=>g.at>=r-500&&g.at<=o+500).map(({at:g,...m})=>({...m,title:Te(m.title),detail:m.detail?Te(m.detail):void 0,t:Math.max(0,g-r)})).sort((g,m)=>g.t-m.t);if(!a.some(g=>g.source!=="agent"))n.notes.add("No app events were captured. Is the Events tool installed in the app?");const s=t.file.replace(/\.mp4$/,"");const c=Bt(t.host);const l=n.marks.filter(g=>g.at>=r-500&&g.at<=o).map(({at:g,...m})=>({...m,t:Math.max(0,g-r)})).sort((g,m)=>g.t-m.t);let d=null;if(l.length){try{d=await up(t.file,l,`${s}-taps.mp4`);if(d){const g=dp(l);if(g)n.notes.add(g)}if(!d)n.notes.add("Install ffmpeg to also get a copy of the video with the taps drawn in. The viewer shows them either way.")}catch(g){n.notes.add(`Could not draw the taps into a copy of the video: ${j(g).slice(0,160)}. The viewer shows them either way.`)}}const u=[...n.notes];await pi.writeFile(`${s}.timeline.json`,JSON.stringify({device:c,startedAt:new Date(r).toISOString(),durationMs:i,notes:u,items:a,touches:l},null,1));await pi.writeFile(`${s}.html`,hp({title:n.name,video:_n.basename(t.file),durationMs:i,items:a,device:c,notes:u,marks:l}));_e.delete(n.id);const h=g=>_n.relative(bs(),g);const b=new Map;for(const g of a)b.set(g.source,(b.get(g.source)??0)+1);const f=g=>`${(g/1e3).toFixed(1)}s`;const w=a.filter(g=>g.status==="error"||g.source==="route"||g.source==="agent").slice(0,14);return[`Saved a ${f(i)} recording of ${c}.`,`- Video: ${h(t.file)}`,`- Viewer (video + timeline, click an event to jump there): ${h(`${s}.html`)}`,`- Timeline data: ${h(`${s}.timeline.json`)}`,...d?[`- Video with touch dots: ${h(d)}`]:l.length?[`- The viewer draws the ${l.length} real touch(es) on the video.`]:[],"",a.length?`Events: ${[...b].map(([g,m])=>`${g} ${m}`).join(", ")}.`:"No events in this window.",...w.length?["What happened:",...w.map(g=>`- ${f(g.t)} [${g.source}]${g.status==="error"?" ERROR":""} ${g.title}${g.detail&&g.source!=="route"?` (${g.detail.split("\n")[0].slice(0,80)})`:""}`)]:[],...u.map(g=>`Note: ${g}`),"Recordings are kept out of git (.buoy/recordings/.gitignore). Secrets in logs are hidden, but the video shows the screen as it was: check it before sharing."].join("\n")}var ef=1200;var tf=3e3;function nf(e,n){bu(()=>_e.size>0);e.registerTool("screen_recording",{_meta:Ge(),description:"Record the screen of the iOS Simulator or Android device (start, do the steps, stop). On stop, saves an MP4 plus a viewer page that plays it beside a timeline of what the app did (web calls, query fetches, route changes, store writes, console errors) and what you did (your taps and other actions), and returns a short written summary. Real touches (native_tap, native_swipe, native_pinch) show as marks on the video, and with ffmpeg installed a copy of the MP4 has them drawn in. In .buoy/recordings. Use it to show a bug or a fix, e.g. for a pull request. Max 3 minutes. To record a set of taps or swipes, pass them as steps with start: Buoy records, runs them with real touches, stops and saves, all in one call.",inputSchema:{action:hi.enum(["start","stop"]),name:hi.string().optional().describe('start: a short name for the files, e.g. "checkout-timeout".'),steps:hi.array(ms).min(1).optional().describe('start: run_flow steps to record, e.g. [{tap:"One"},{tap:"Two"}]. Taps and swipes are real touches, so they show on the video. Each tap waits for its target, so no waitMs is needed between taps. The video stops and saves when they finish; no stop call needed.'),serial:hi.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{if(t.action==="start"){const{id:o,host:a}=await ge(n,t.deviceId,{serial:t.serial});if(_e.has(o))return x("A recording is already running on this device. Stop it first.");const s=(t.name??"recording").trim().replace(/[^\w.-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"recording";const c={id:o,name:s,events:new Map,agent:[],notes:new Set,failedPolls:0,marks:[]};_e.set(o,c);let l;c.starting=new Promise(u=>l=u);try{const u=_n.join(bs(),".buoy","recordings");await pi.mkdir(u,{recursive:true});await pi.writeFile(_n.join(u,".gitignore"),"*\n",{flag:"wx"}).catch(()=>void 0);if(n.connectedDevices().some(b=>b.deviceId===o&&fe(b)))await n.getSnapshot("events",o,{timeoutMs:2e3,silent:true}).catch(()=>void 0);const h=_n.join(u,`${s}-${Kp()}-${Np().slice(0,6)}.mp4`);c.rec=await ap(a,h)}catch(u){_e.delete(o);throw u}finally{l()}c.poll=setInterval(()=>void jr(n,c),2e3);c.poll.unref?.();c.rec.stopTimer=setTimeout(()=>{c.finishing??=Lr(n,c);c.finishing.then(u=>Sn.set(o,`The recording hit the ${Jt/6e4}-minute limit and was saved.
598
+ ${u}`),u=>Sn.set(o,`The recording hit the time limit and could not be saved: ${j(u)}`))},Jt);c.rec.stopTimer.unref?.();if(t.steps){const u=Date.now();const h=await Br({steps:t.steps,real:true,include:"none",deviceId:o});const b=Math.max(ef,tf-(Date.now()-u));const[f]=await Promise.all([Pe(n,o,void 0,0).catch(()=>""),Gp(b)]);c.finishing??=Lr(n,c);let w;try{w=await c.finishing}catch(m){c.finishing=void 0;return x(`${h.content[0]?.text??""}
599
599
 
600
600
  Could not save the recording: ${j(m)}. Call screen_recording with action "stop" to retry.`)}const g=M(`${h.content.map(m=>m.text).join("\n")}
601
601
 
602
- ${w}${f}`);return h.isError?{...g,isError:true}:g}const d=Ji().length?" Note: a secret was typed earlier this session; if it is still on screen, the video will show it. Typing secrets is blocked until the recording stops.":" Typing secrets ({{secret:NAME}}) is blocked until the recording stops, because a video can't hide them.";return M(`Recording ${Rt(a)}. Do the steps, then call screen_recording with action "stop". It stops by itself after ${Ut/6e4} minutes.${d}`)}let i=t.deviceId;if(!i){try{i=n.requireProDevice()}catch{i=void 0}}if(!i||!ke.has(i)){if(i&&bn.has(i)){const o=bn.get(i);bn.delete(i);return M(o)}if(ke.size===1)i=[...ke.keys()][0];else return x(ke.size?"Several recordings are running. Pass deviceId.":'No recording is running. Start one with action "start".')}const r=ke.get(i);await r.starting;if(!r.rec)return x("The recording did not start.");r.finishing??=Cr(n,r);try{return M(await r.finishing)}catch(o){r.finishing=void 0;return x(`Could not save the recording: ${j(o)}. Call stop again to retry.`)}}catch(i){return x(j(i))}})}var vn=np(ep);var Gt="highlight-updates";var je=e=>new Promise(n=>setTimeout(n,e));var Lp="Native touch on the iOS Simulator needs idb on this Mac (brew install facebook/fb/idb-companion && pipx install fb-idb).";var yn="Real touches on a physical iPhone need Xcode's CoreDevice touch support, and Buoy couldn't set it up on this Mac. Use tap_element (it calls the element's handler) or run_flow; screenshots, recording and app control work on the iPhone.";var di=new Map;async function qp(e){const n=di.get(e);if(n&&Date.now()-n.at<hs)return n.size;const t=await Uo(e);const i={width:t.readUInt32BE(16),height:t.readUInt32BE(20)};di.set(e,{size:i,at:Date.now()});return i}var Kt=new Map;function us(e,n){if(n){if(e==="up")return{from:[.5,.88],to:[.5,.06]};if(e==="down")return{from:[.5,.3],to:[.5,.94]};if(e==="left")return{from:[.9,.5],to:[.1,.5]};return{from:[.1,.5],to:[.9,.5]}}const t=e==="left"?-.35:e==="right"?.35:0;const i=e==="up"?-.3:e==="down"?.3:0;return{from:[.5-t/2,.5-i/2],to:[.5+t/2,.5+i/2]}}var Up=60;var hs=3e3;function zp(e){Ze.delete(e);Kt.delete(e);di.delete(e)}function $n(e){if(e.platform==="android")return;Kt.delete(e.udid);di.delete(e.udid)}var ps=/idb on this Mac|SimulatorKit|not booted|connection refused|Failed to connect to companion/i;async function fs(){const e=(await vn("xcode-select",["-p"],{timeout:5e3}).catch(()=>({stdout:""}))).stdout.trim();if(!e.endsWith("/Contents/Developer"))return null;const n=se.dirname(e);const t=se.join(e,"Library","PrivateFrameworks","SimulatorKit.framework");const i=se.join(n,"SharedFrameworks","SimulatorKit.framework");const r=l=>ae.access(l).then(()=>true,()=>false);if(await r(t)||!await r(i))return null;const o=(await vn("plutil",["-extract","DTXcodeBuild","raw","-o","-",se.join(n,"Info.plist")]).catch(()=>({stdout:"x"}))).stdout.trim();const a=se.join(_r.homedir(),".buoy","xcode-shim",o.replace(/[^\w.-]/g,"_"),"Contents");const s=se.join(a,"Developer");if(await r(se.join(s,"Library","PrivateFrameworks","SimulatorKit.framework")))return s;await ae.mkdir(se.join(s,"Library","PrivateFrameworks"),{recursive:true});const c=async(l,d,u)=>{for(const h of await ae.readdir(l)){if(h===u)continue;await ae.symlink(se.join(l,h),se.join(d,h)).catch(()=>void 0)}};await c(n,a,"Developer");await c(e,s,"Library");await c(se.join(e,"Library"),se.join(s,"Library"),"PrivateFrameworks");await c(se.join(e,"Library","PrivateFrameworks"),se.join(s,"Library","PrivateFrameworks"),"").catch(()=>void 0);await ae.symlink(i,se.join(s,"Library","PrivateFrameworks","SimulatorKit.framework")).catch(()=>void 0);return s}async function ms(e){const n=`/tmp/idb/${e}.buoy-restart`;await ae.mkdir("/tmp/idb",{recursive:true}).catch(()=>void 0);const t=async()=>{try{await(await ae.open(n,"wx")).close();return true}catch{const i=await ae.stat(n).then(r=>Date.now()-r.mtimeMs,()=>0);if(i>15e3){const r=`${n}.stale-${process.pid}-${Date.now()}`;await ae.rename(n,r).then(()=>ae.unlink(r).catch(()=>void 0),()=>void 0);return t()}return false}};if(!await t()){for(let i=0;i<60;i++){await je(250);if(!await ae.stat(n).then(()=>true,()=>false))return}return}try{await vn("pkill",["-f",`idb_companion --udid ${e}`],{timeout:5e3}).catch(()=>void 0);await je(300);const i=await fs().catch(()=>null);const r=tp("idb_companion",["--udid",e,"--grpc-domain-sock",`/tmp/idb/${e}_companion.sock`,"--only","simulator"],{detached:true,stdio:"ignore",env:i?{...process.env,DEVELOPER_DIR:i}:process.env});r.on("error",()=>void 0);r.unref();await je(1800)}finally{await ae.unlink(n).catch(()=>void 0)}}async function Oe(e,n=15e3,t=false){try{const{stdout:i}=await vn("idb",e,{timeout:n});return i}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(Lp);const o=(r.stderr||r.message||"").slice(0,300);const a=e.includes("--udid")?e[e.indexOf("--udid")+1]:void 0;if(!t&&a&&/not booted|connection refused|Failed to connect to companion/i.test(o)){await ms(a);return Oe(e,n,true)}if(/SimulatorKit is required/i.test(o)){if(!t&&a&&await fs().catch(()=>null)){await ms(a);return Oe(e,n,true)}throw new Error("idb can't send touches with this Xcode: it can't load SimulatorKit, and Buoy could not work around it. Use tap_element (it calls the element's handler) for now. Reading the screen (describe_screen, screenshot_component) still works.")}throw new Error(`idb ${e[0]} ${e[1]??""} failed: ${o}`)}}var Or=class extends Error{constructor(e){super(`idb can only see an empty app shell on this simulator, so native labels and screen size can't be read. This happens after Maestro or another XCTest tool ran on it, and lasts until the simulator reboots. To fix it, reboot just this simulator: xcrun simctl shutdown ${e} && xcrun simctl boot ${e}. Until then, tap_element and taps by testID or query still work.`);this.name="AxShellOnlyError"}};async function ui(e){const n=JSON.parse(await Oe(["ui","describe-all","--udid",e,"--json","--nested"]));const t=[];const i=o=>{t.push(o);for(const a of o.children??[])i(a)};n.forEach(i);const r=t.find(o=>o.type==="Application"&&o.frame);if(!t.some(o=>(o.frame?.width??0)>0))throw new Or(e);return{all:t,screen:r?.frame?{width:r.frame.width,height:r.frame.height}:{width:0,height:0}}}var Ze=new Map;var hi=se.join(_r.homedir(),".buoy","cache","native-bars.json");var ws=false;async function gs(){if(ws||process.env.VITEST)return;ws=true;try{const e=JSON.parse(await ae.readFile(hi,"utf8"));for(const[n,t]of Object.entries(e))if(!Ze.has(n)&&t?.screen&&Array.isArray(t.els))Ze.set(n,t)}catch{}}function bs(){if(process.env.VITEST)return;const e=Object.fromEntries([...Ze].map(([t,i])=>[t,{screen:i.screen,els:i.els.map(r=>({AXLabel:r.AXLabel,type:r.type,frame:r.frame}))}]));const n=`${hi}.${process.pid}`;void ae.mkdir(se.dirname(hi),{recursive:true}).then(()=>ae.writeFile(n,JSON.stringify(e))).then(()=>ae.rename(n,hi)).catch(()=>void 0)}async function Wp(e,n){await gs();const t=Ze.get(e);const i=n.trim().toLowerCase();const r=t?.els.find(o=>(o.AXLabel??"").trim().toLowerCase()===i&&o.frame);if(!t||!r?.frame)return null;try{const o=Math.round(r.frame.x+r.frame.width/2);const a=Math.round(r.frame.y+r.frame.height/2);const s=JSON.parse(await Oe(["ui","describe-point","--udid",e,String(o),String(a),"--json"],3e3));if((s.AXLabel??"").trim().toLowerCase()===i&&s.frame)return{el:s,screen:t.screen}}catch{}Ze.delete(e);bs();return null}async function vs(e,n,t,i=false,r=[.95,.97,.09,.06]){const o=n.trim().toLowerCase();const a=Date.now()+8e3;const s=f=>{const w=(f.AXLabel??"").trim().toLowerCase();if(!w)return 0;if(w===o)return 3;if(w.startsWith(o))return 2;if(w.includes(o))return 1;return 0};const{width:c,height:l}=t.screen;const d=f=>{if(!c||!f.frame)return true;const w=f.frame.x+f.frame.width/2;const g=f.frame.y+f.frame.height/2;return w>=0&&w<=c&&g>=0&&g<=l};let u=t.all.filter(f=>s(f)>0&&f.frame&&f.type!=="Application"&&d(f));if(!u.some(f=>s(f)===3)&&t.screen.width){const f=r.map(w=>{const g=[];for(let m=20;m<t.screen.width;m+=40)g.push([m,Math.round(t.screen.height*w)]);return g});for(const w of f){if(Date.now()>=a)break;const g=await Promise.allSettled(w.map(async([p,y])=>JSON.parse(await Oe(["ui","describe-point","--udid",e,String(p),String(y),"--json"],3e3))));const m=[];for(const p of g){if(p.status==="rejected"){if(ps.test(String(p.reason)))throw p.reason;continue}const y=p.value;if(y?.frame&&y.AXLabel&&y.type&&y.type!=="Application")m.push(y);if(s(y)>0&&y.frame)u.push(y)}if(m.length){const p=Ze.get(e);const y=p&&p.screen.width===t.screen.width&&p.screen.height===t.screen.height;const v=[...y?p.els:[],...m].filter(($,T,k)=>k.findIndex(D=>D.AXLabel===$.AXLabel&&JSON.stringify(D.frame)===JSON.stringify($.frame))===T);Ze.set(e,{screen:t.screen,els:v});bs()}if(u.some(p=>s(p)===3))break}}const h=Math.max(0,...u.map(s));if(i&&h<3)return[];u=u.filter(f=>s(f)===h);let b=u.filter((f,w)=>u.findIndex(g=>JSON.stringify(g.frame)===JSON.stringify(f.frame))===w);if(b.length>1){const f=b.filter(w=>(w.AXLabel??"").trim()===n.trim());if(f.length)b=f}if(b.length>1){const f=b.filter(w=>w.type&&!/StaticText|Text$|Other|Image/.test(w.type));if(f.length)b=f}return b}async function Hp(e){const n=await ui(e);const t=n.all.filter(s=>s.frame&&s.frame.width>0&&s.type!=="Application");const i=s=>`${s.AXLabel??""}|${JSON.stringify(s.frame)}`;const r=new Set(t.map(i));const{width:o,height:a}=n.screen;if(o){const s=t.filter(u=>!!u.type&&!/StaticText|Text$|Other|Image|Group|Heading/.test(u.type));const c=(u,h)=>s.some(b=>u>=b.frame.x&&u<=b.frame.x+b.frame.width&&h>=b.frame.y&&h<=b.frame.y+b.frame.height);const l=[];for(const u of[.95,.97,.09,.06])for(let h=20;h<o;h+=40){const b=Math.round(a*u);if(!c(h,b))l.push([h,b])}const d=Date.now()+6e3;for(let u=0;u<l.length&&Date.now()<d;u+=8){const h=await Promise.all(l.slice(u,u+8).map(([b,f])=>Oe(["ui","describe-point","--udid",e,String(b),String(f),"--json"],3e3).then(w=>JSON.parse(w),w=>{if(ps.test(String(w)))throw w;return null})));for(const b of h){if(!b?.type||!b.frame?.width||b.type==="Application"||r.has(i(b)))continue;r.add(i(b));t.push(b)}}}t.sort((s,c)=>s.frame.y-c.frame.y||s.frame.x-c.frame.x);return{elements:t,screen:n.screen}}function ys(e){const n=e.frame;const t=[e.type??"Element",e.AXLabel?JSON.stringify(e.AXLabel.slice(0,80)):"",e.AXUniqueId?`id=${e.AXUniqueId}`:"",e.AXValue?`value=${JSON.stringify(String(e.AXValue).slice(0,40))}`:"",e.enabled===false?"(disabled)":""].filter(Boolean);return`${t.join(" ")} at (${Math.round(n.x)}, ${Math.round(n.y)}) ${Math.round(n.width)}x${Math.round(n.height)}`}function Rr(e){async function n(p,y,v){if(y.platform!=="ios"||!v)return;try{const $=await e.callAction(Gt,"describeScreen",{},p,{timeoutMs:2e3,silent:true});const T=$?.screen?.width??0;const k=$?.screen?.height??0;const D=Math.max(T,k)/Math.max(1,Math.min(T,k));if(T>0&&k>0&&D>1.7)Kt.set(y.udid,{size:{width:T,height:k},at:Date.now()})}catch{}}async function t(p,y){if(p.platform==="android")return jn(p.serial);if(p.platform==="ios-device")return{...await qp(p.udid),isPhone:true};const v=Kt.get(p.udid);if(!y&&v&&Date.now()-v.at<hs)return v.size;try{const $=(y??await ui(p.udid)).screen;if(!$.width)throw new Error("Could not read the simulator's screen size from idb.");Kt.set(p.udid,{size:$,at:Date.now()});return $}catch($){if(!y&&v&&$ instanceof Or)return v.size;if(y||!await De())throw $;const T=se.join(_r.tmpdir(),`buoy-size-${process.pid}-${Date.now()}.png`);try{await vn("xcrun",["simctl","io",p.udid,"screenshot","--type=png",T],{timeout:2e4});const k=await ae.readFile(T);return{width:k.readUInt32BE(16),height:k.readUInt32BE(20)}}finally{await ae.rm(T,{force:true}).catch(()=>void 0)}}}async function i(p,y,v,$){if(p.platform==="android")await mu(p.serial,y.x,y.y,v);else if(p.platform==="ios-device"){const T=await De();if(!T)throw new Error(yn);return Qa(T,await Fn(p.udid),await t(p),y.x,y.y,v,$)}else if(await De()){return Qa(await De(),p.udid,{...await t(p),bundleId:p.bundleId},y.x,y.y,v,$)}else{await Sr(p.udid);await Oe(["ui","tap","--udid",p.udid,...v?["--duration",(v/1e3).toFixed(2)]:[],String(Math.round(y.x)),String(Math.round(y.y))])}}async function r(p,y){if(p.platform==="android"){const $=await Ki(p.serial,p.packageName);const T=$??{x:0,y:0,...await jn(p.serial)};return[(y.x-T.x)/T.width,(y.y-T.y)/T.height]}const v=await t(p);return[y.x/v.width,y.y/v.height]}async function o(p,y,v){try{const[$,T]=await r(y,v);if(!($>=0&&$<=1&&T>=0&&T<=1))return null;return await e.callAction(Gt,"hitTest",{x:$,y:T},p,{timeoutMs:2500,silent:true})}catch{return null}}async function a(p,y,v){const $=await o(p,y,v);if(!$?.found||!$.buoy)return null;const T=$.path?.length?` (${$.path.slice(0,3).join(" in ")})`:"";return`Buoy's own overlay${T} is on top at that spot, so a real touch would land on Buoy, not the app. Nothing was touched. Close or minimize the Buoy panel first, or pass allowBuoy: true to touch Buoy on purpose.`}async function s(p,y,v,$,T,k,D){if(!Rp(p))return;try{const E=await t(y);const I=S=>[S[0]/E.width,S[1]/E.height];const R=Date.now()-k-(y.platform==="android"?50:80);const _=D!==void 0?D:y.platform==="android"?null:await wn(y.udid);Pp(p,{at:R,kind:v,ms:k,from:$.map(I),to:T?.map(I),screen:E,orientation:_})}catch{}}async function c(p,y,v){const $=await e.callAction(Gt,"describeScreen",v.allowBuoy?{includeBuoy:true}:{},p);const T=(v.query??"").toLowerCase();const k=($.elements??[]).filter(_=>v.nativeTag!=null?_.nativeTag===v.nativeTag:v.testID?_.testID===v.testID:(_.text??_.label??"").toLowerCase()===T);const D=k.filter(_=>_.tap);if(D.length===0)return"No element on screen matches that. Call describe_screen, or use label for native controls.";if(D.length>1&&v.nativeTag==null)return`${D.length} elements match equally well, so nothing was tapped. Use nativeTag: ${D.map(_=>`#${_.nativeTag}`).join(", ")}.`;const E=D[0].tap;const I=`"${D[0].text??D[0].label??D[0].name}"`;if(y.platform==="android"){const _=await Ki(y.serial,y.packageName);const S=_??{x:0,y:0,...await jn(y.serial)};return{x:S.x+E.x*S.width,y:S.y+E.y*S.height,what:I}}const R=await t(y);return{x:E.x*R.width,y:E.y*R.height,what:I}}async function l(p,y,v=false,$){if(p.platform==="android"){const I=$u(await Vi(p.serial),y).filter(_=>!v||[_.desc,_.text].some(S=>(S??"").trim().toLowerCase()===y.trim().toLowerCase()));if(I.length===0)return`No native element labeled "${y}" was found on screen.`;if(I.length>1)return`${I.length} native elements match "${y}" equally well, so nothing was tapped: ${I.map(_=>`"${_.desc||_.text||_.id}" at (${_.bounds.x}, ${_.bounds.y})`).join(", ")}. Tap one by x/y.`;const R=I[0].bounds;return{x:R.x+R.width/2,y:R.y+R.height/2,what:`"${I[0].desc||I[0].text||I[0].id}" (${I[0].cls.split(".").pop()})`}}const T=await Wp(p.udid,y);if(T){Kt.set(p.udid,{size:T.screen,at:Date.now()});const I=T.el.frame;return{x:I.x+I.width/2,y:I.y+I.height/2,what:`"${T.el.AXLabel}" (${T.el.type})`}}let k;try{k=await ui(p.udid)}catch(I){if(!(I instanceof Or))throw I;if($?.online){const R=await m($.id,p,y,true,"app");if(!("miss"in R))return{...R,what:`${R.what} (found in the app's own screen, because idb can only see an empty shell on this simulator)`};if(R.tie)return R.miss}return I.message}await t(p,k);const D=await vs(p.udid,y,k,v);if(D.length===0)return`No native element labeled "${y}" was found on screen.`;if(D.length>1)return`${D.length} native elements match "${y}" equally well, so nothing was tapped: ${D.map(I=>`"${I.AXLabel}" at (${Math.round(I.frame.x)}, ${Math.round(I.frame.y)})`).join(", ")}. Tap one by x/y.`;const E=D[0].frame;return{x:E.x+E.width/2,y:E.y+E.height/2,what:`"${D[0].AXLabel}" (${D[0].type})`}}async function d(p,y,v,$,T=false,k=0,D){const E=[...y,...v].map(R=>String(Math.round(R)));const I=p.platform==="android"?null:await De();if(p.platform!=="android"&&!I)await Sr(p.udid);if(p.platform==="android"){if(T)await J(p.serial,`input draganddrop ${E.join(" ")} ${Math.round($)}`);else await wu(p.serial,y,v,$);return void 0}if(I){const R=p.platform==="ios-device"?await Fn(p.udid):p.udid;const _=D?.screen??await t(p);return wp(I,R,{..._,bundleId:p.platform==="ios"?p.bundleId:null},y,v,$,T?700:0,T?0:k,D?.orientation)}if(T)await Oe(["ui","swipe","--udid",p.udid,...E,"--duration",($/1e3).toFixed(2),"--delta","4"]);else await Oe(["ui","swipe","--udid",p.udid,...E,"--duration",($/1e3).toFixed(2)]);return void 0}async function u(p){try{const y=await e.callAction(Gt,"describeScreen",{},p,{timeoutMs:3e3,silent:true});const v=new Map;for(const $ of y?.elements??[]){if(!$.tap)continue;const T=`${$.name??""}|${$.text??$.label??""}|${$.nativeTag??""}`;if(v.has(T))v.set(T,{x:NaN,y:NaN});else v.set(T,$.tap)}return v}catch{return null}}function h(p,y,v,$,T=true){let k=0;let D=0;for(const[E,I]of y){const R=p.get(E);if(!R)D++;else if(Number.isFinite(R[v])&&Number.isFinite(I[v])&&Math.abs(I[v]-R[v])>$)k++}return k+(T&&D>=2?D:0)}function b(p,y,v){const $=[];for(const[T,k]of y){const D=p.get(T);if(D&&Number.isFinite(D[v])&&Number.isFinite(k[v])&&Math.abs(k[v]-D[v])>.002)$.push(k[v]-D[v])}if($.length<2)return 0;$.sort((T,k)=>T-k);return $[Math.floor($.length/2)]}async function f(p,y){await je(120);let v=await u(p);const $=Date.now()+2500;while(v&&Date.now()<$){await je(100);const T=await u(p);if(!T)break;const k=h(v,T,y,.002,false)===0;v=T;if(k)break}return v}async function w(p,y,v,$,T,k=15){const D=Math.abs($[1]-v[1])>=Math.abs($[0]-v[0])?"y":"x";let E=await u(p);if(E===null)throw new Error("Scrolling to the end needs the app connected, to see when the list stops moving.");let I=0;let R=false;const _={screen:await t(y)};const S=D==="y"?1:0;const A=D==="y"?_.screen.height:_.screen.width;const O=Math.sign($[S]-v[S]);const F=Math.abs($[S]-v[S])/A;const P=new Set;const K=(Q,B)=>{for(const[z,re]of B){const le=Q.get(z);if(le&&Number.isFinite(le[D])&&Number.isFinite(re[D])&&Math.abs(re[D]-le[D])>.01)P.add(z)}};let N=E;while(I<k){const Q=await d(y,v,$,T,false,Up,_);if(Q!==void 0)_.orientation=Q;I++;await s(p,y,"swipe",[v],[$],T,Q);await je(90);const B=await u(p);await je(70);const z=await u(p);if(!B||!z)throw new Error("The app stopped answering while scrolling.");const re=b(B,z,D)*O;K(E,z);N=z;if(re>.004||re>=-.004&&h(B,z,D,.002,false)===0&&h(E,z,D,.01)>0){E=z;continue}const le=await f(p,D);if(!le)throw new Error("The app stopped answering while scrolling.");K(E,le);N=le;if(h(E,le,D,.01)===0){R=true;break}const Ie=b(E,le,D)*O;if(Ie>.01&&Ie<F*.85){R=true;break}E=le}const q=[...N].filter(([Q,B])=>P.has(Q)&&B[D]>=0&&B[D]<=1&&Q.split("|")[1]);const V=Q=>Q.sort((B,z)=>(z[1][D]-B[1][D])*-O)[0];const pe=V(q.filter(([Q])=>!Q.startsWith("Text|")))??V(q);return{swipes:I,atEnd:R,lastRow:pe?pe[0].split("|")[1]:void 0}}async function g(p,y){if(p.platform==="android"){const $=await Ki(p.serial,p.packageName);const T=$??{x:0,y:0,...await jn(p.serial)};return{x:T.x+y.x*T.width,y:T.y+y.y*T.height}}const v=await t(p);return{x:y.x*v.width,y:y.y*v.height}}async function m(p,y,v,$,T){if($&&(T==="app"||T==="loose")){const k=await e.callAction(Gt,"describeScreen",{},p,{timeoutMs:4e3,silent:true}).catch(()=>null);const D=(k?.elements??[]).filter(_=>_.tap&&_.tap.x>=0&&_.tap.x<=1&&_.tap.y>=0&&_.tap.y<=1);const E=v.trim().toLowerCase();const I=_=>(_.text??_.label??"").trim().toLowerCase();const R=T==="app"?[D.filter(_=>_.testID===v),D.filter(_=>I(_)===E)]:[D.filter(_=>!!E&&I(_).includes(E))];for(const _ of R){if(!_.length)continue;const S=_.filter(P=>P.interactive);const A=S.length?S:_;const O=A.filter((P,K)=>A.findIndex(N=>Math.abs(N.tap.x-P.tap.x)<.02&&Math.abs(N.tap.y-P.tap.y)<.02)===K);if(O.length>1){return{tie:true,miss:`${O.length} elements match "${v}", so nothing was touched: ${O.slice(0,5).map(P=>P.testID?`testID ${P.testID}`:JSON.stringify(P.text??P.label??P.name)).join(", ")}. Use an exact testID.`}}const F=O[0];return{...await g(y,F.tap),what:`"${F.text??F.label??F.name}"${F.testID?` [testID=${F.testID}]`:""}`}}}if((T==="native"||T==="nativeExact")&&y.platform!=="ios-device"){const k=await l(y,v,T==="nativeExact",{id:p,online:$});if(typeof k!=="string")return k;if(/equally well/.test(k))return{miss:k,tie:true};if(/empty app shell/.test(k))return{miss:k}}return{miss:`Nothing on screen matches "${v}".`}}return{primeScreen:n,screenOf:t,touch:i,hitAt:o,buoyCover:a,mark:s,reactSpot:c,labelSpot:l,swipe:d,scrollToEnd:w,findSpot:m}}async function Jp(e,n,t,i,r=false){const o=Rr(e);const{id:a,host:s,online:c}=await ge(e,n);if(s.platform==="ios-device")return null;$n(s);await o.primeScreen(a,s,c);const l=await o.findSpot(a,s,t,c,r?"nativeExact":"native");if("miss"in l)return null;if(c&&await o.buoyCover(a,s,l))return null;const d=i?600:void 0;const u=await o.touch(s,l,d);await o.mark(a,s,d?"hold":"tap",[[l.x,l.y]],void 0,d??150,u);return`${d?"Long-pressed":"Tapped"} ${l.what} with a real touch at (${Math.round(l.x)}, ${Math.round(l.y)}).`}function Gp(e){if(process.platform!=="darwin"||process.env.BUOY_WARM_TOUCH==="0")return;const n=Date.now();const t=async()=>{try{const i=e.peekProjectDevice();const r=i?e.listDevices().find(o=>o.deviceId===i)?.platform:void 0;if(i&&r==="ios"){const{host:o}=await ge(e,i);if(o.platform!=="ios")return;const a=await De();if(a)await Ma(a,o.udid,["--device",o.udid,"ping"]);await wn(o.udid);await gs();if(!Ze.has(o.udid)){const s=await ui(o.udid).catch(()=>null);if(s?.screen.width)await vs(o.udid,"\0no such label",s,false,[.95]).catch(()=>void 0)}return}if(i&&r)return}catch{}if(Date.now()-n<3e4)setTimeout(()=>void t(),500).unref()};setTimeout(()=>void t(),300).unref()}function Kp(e,n){Gp(n);const{primeScreen:t,screenOf:i,touch:r,hitAt:o,buoyCover:a,mark:s,reactSpot:c,labelSpot:l,swipe:d,scrollToEnd:u}=Rr(n);const h=X.string().optional().describe("Android: the adb serial, when several devices are connected.");const b=f=>f.platform==="ios"?"pt":"px";e.registerTool("native_tap",{_meta:He(),description:"Tap the screen for real (an OS touch: CoreDevice on the iOS Simulator, the way Xcode sends it, or idb on older Xcode; adb on Android), for native-only controls like a native tab bar or a system dialog, or when tap_element finds no handler. Pass holdMs for a long press. label and x/y work even when the app's JS is stuck.",inputSchema:{label:X.string().optional().describe('Native accessibility label, e.g. "Rewards" for a native tab.'),query:X.string().optional().describe("Text of an element from describe_screen."),testID:X.string().optional(),nativeTag:X.number().optional(),x:X.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:X.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),holdMs:X.number().min(100).max(1e4).optional().describe("Hold the touch this long: a long press (context menus, Paste, reorder handles). 600 is a typical long press."),allowBuoy:X.boolean().optional().describe("Touch even if Buoy's own panel covers the spot. By default the tap stops there, because it would hit Buoy instead of the app."),serial:h,deviceId:L}},async f=>{try{const w=f.nativeTag!=null||!!f.testID||!!f.query;const{id:g,host:m,online:p}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:!w});$n(m);await t(g,m,p);if(m.platform==="ios-device"&&!await De())return x(yn);let y;if(f.x!=null&&f.y!=null){const T=await i(m);y={x:f.x*T.width,y:f.y*T.height,what:`(${f.x.toFixed(2)}, ${f.y.toFixed(2)})`}}else if(w){y=await c(g,m,f)}else if(f.label){if(m.platform==="ios-device")return x("label works on simulators and Android. On a physical iPhone, tap by query, testID or nativeTag (from describe_screen), or by x and y.");y=await l(m,f.label,false,{id:g,online:p})}else{return x("Give label, query, testID, nativeTag, or x and y.")}if(typeof y==="string")return x(y);if(p&&!f.allowBuoy){const T=await a(g,m,y);if(T)return x(T)}const v=await r(m,y,f.holdMs);await s(g,m,f.holdMs?"hold":"tap",[[y.x,y.y]],void 0,f.holdMs??150,v);const $=p?await Re(n,g):" (The app is not connected, so its screen was not read back.)";return M(`${f.holdMs?`Long-pressed (${f.holdMs}ms)`:"Tapped"} ${y.what} with a real touch at (${Math.round(y.x)}, ${Math.round(y.y)}) ${b(m)}.${$}`)}catch(w){return x(j(w))}});e.registerTool("native_type",{description:"Type text on the real keyboard into the focused field (CoreDevice or idb on the iOS Simulator, adb on Android). Tap the field first (tap_element or native_tap). Unlike tap_element's text, this goes through the keyboard like a person typing. It types plain ASCII only (letters, digits, punctuation, newlines).",inputSchema:{text:X.string().describe("The text to type. Write {{secret:NAME}} for a password: Buoy types BUOY_SECRET_NAME from its environment and hides it in what it returns. Prefer real password fields (shown as dots)."),serial:h,deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});if(g.platform==="ios-device"&&!await De())return x(yn);const p=Bn(f.text);let y=false;if(g.platform==="android")await gu(g.serial,p);else if(g.platform==="ios-device")await es(await De(),await Fn(g.udid),p);else{const $=await De();if($){await es($,g.udid,p);y=true}else{await Sr(g.udid);await Oe(["ui","text","--udid",g.udid,p])}}if(!y)await je(300);const v=m?await Re(n,w):"";return M(`Typed ${JSON.stringify(f.text)} on the keyboard.${v}`)}catch(w){return x(j(w))}});e.registerTool("native_swipe",{_meta:He(),description:"Swipe the screen for real (CoreDevice or idb on the iOS Simulator, adb on Android): scroll a list, dismiss a sheet, change a page. Give a direction (the way your finger moves) or from/to points as fractions of the screen. To reach the bottom of a list, pass untilEnd: true. It swipes again until the screen stops moving, in one call.",inputSchema:{direction:X.enum(["up","down","left","right"]).optional().describe('"up" scrolls content down the list (finger moves up).'),fromX:X.number().min(0).max(1).optional(),fromY:X.number().min(0).max(1).optional(),toX:X.number().min(0).max(1).optional(),toY:X.number().min(0).max(1).optional(),durationMs:X.number().min(50).max(5e3).optional().describe("Default 300 (1200 with drag)."),drag:X.boolean().optional().describe("Drag instead of flick: press, hold, then move (reorder handles, drag and drop, sliders). On the iOS Simulator with older Xcode (idb) it is a slow swipe only."),untilEnd:X.boolean().optional().describe("Keep swiping until the screen stops moving (the end of the list), up to 15 swipes. Returns the last screen."),allowBuoy:X.boolean().optional().describe("Swipe even if Buoy's own panel covers the starting point."),serial:h,deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});$n(g);await t(w,g,m);if(g.platform==="ios-device"&&!await De())return x(yn);const p=await i(g);let y;let v;if(f.fromX!=null&&f.fromY!=null&&f.toX!=null&&f.toY!=null){y=[f.fromX,f.fromY];v=[f.toX,f.toY]}else{({from:y,to:v}=us(f.direction??"up",!!f.untilEnd))}const $=[y[0]*p.width,y[1]*p.height];const T=[v[0]*p.width,v[1]*p.height];let k=0;if(f.drag&&g.platform==="android"){const _=Number((await J(g.serial,"settings get secure long_press_timeout").catch(()=>"")).trim())||500;k=_*1.5+600}const D=Math.max(f.durationMs??(f.drag?1200:300),k);if(m&&!f.allowBuoy){const _=await a(w,g,{x:$[0],y:$[1]});if(_)return x(_)}if(f.untilEnd){if(f.drag)return x("untilEnd scrolls with flicks. Leave out drag.");if(!m)return x("untilEnd needs the app connected, to see when the list stops moving.");const _=await u(w,g,$,T,f.durationMs??250);const{swipes:S,atEnd:A}=_;const O=await Re(n,w,void 0,0);const{lastRow:F}=_;return M(`${A?`Reached the end of the list after ${S} swipe(s)${F?`. The last row is "${F}"`:""}`:`Swiped ${S} times and the screen was still moving`}.${O}`)}const E=g.platform==="android"?null:await De();const I=await d(g,$,T,D,f.drag);await s(w,g,f.drag?"drag":"swipe",[$],[T],D+(f.drag&&E?700:0),I);await je(400);const R=m?await Re(n,w):"";return M(`${f.drag?"Dragged":"Swiped"} from (${$.map(Math.round).join(", ")}) to (${T.map(Math.round).join(", ")}) ${b(g)}.${R}`)}catch(w){return x(j(w))}});e.registerTool("native_inspect",{description:"Read native controls on the screen. Pass pointX and pointY for points on iOS Sims, pixels elsewhere. Use x and y for screen fractions (0 to 1). Leave all points out to list controls, with types, labels, IDs, and frames. This includes tabs, alerts, and sheets. A point read also checks the app view and Buoy's cover. iOS Sims use idb; Android uses uiautomator. Real iPhones show just the app view.",inputSchema:{pointX:X.number().finite().min(0).optional().describe("From the left. iOS Sims use points. Other hosts use pixels."),pointY:X.number().finite().min(0).optional().describe("From the top. iOS Sims use points. Other hosts use pixels."),x:X.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:X.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),serial:h,deviceId:L}},async f=>{try{const w=f.pointX!=null||f.pointY!=null;if(w&&(f.pointX==null||f.pointY==null))return x("Use pointX and pointY together.");if(w&&(f.x!=null||f.y!=null))return x("Use pointX/pointY or x/y, not both.");if(!w&&f.x==null!==(f.y==null))return x("Use x and y together.");const{id:g,host:m,online:p}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});$n(m);await t(g,m,p);const y=b(m);if(!w&&(f.x==null||f.y==null)){if(m.platform==="ios-device")return x("A physical iPhone doesn't share its native element tree with this Mac. Use describe_screen for the app's elements, or native_inspect with x and y to see what is at a point.");let k;let D=0;if(m.platform==="android"){const I=await Vi(m.serial);k=I.filter(R=>R.text||R.desc||R.id||R.clickable).map(R=>{const _=R.bounds;const S=[R.cls.split(".").pop(),R.text?JSON.stringify(R.text.slice(0,80)):"",R.desc?`desc=${JSON.stringify(R.desc.slice(0,60))}`:"",R.id?`id=${R.id.split("/").pop()}`:"",R.clickable?"(clickable)":""];return`${S.filter(Boolean).join(" ")} at (${_.x}, ${_.y}) ${_.width}x${_.height}`})}else{const{elements:I,screen:R}=await Hp(m.udid);const _=I.filter(S=>!R.width||S.frame.y<R.height&&S.frame.y+S.frame.height>0&&S.frame.x<R.width&&S.frame.x+S.frame.width>0);D=I.length-_.length;k=_.map(ys)}const E=k.slice(0,150);return M([`${k.length} native element(s) on screen, top to bottom (${y}; frames are x, y, width x height)${D?`. ${D} more are scrolled out of view`:""}:`,...E.map(I=>`- ${I}`),...k.length>E.length?[`(${k.length-E.length} more not shown.)`]:[],"Tap one with native_tap label or x/y. For the app's React elements (testID, handlers), use describe_screen."].join("\n"))}let v;if(w){const k=await i(m);v={x:f.pointX,y:f.pointY};if(v.x>=k.width||v.y>=k.height){return x(`That point is outside the screen (${k.width} x ${k.height} ${y}).`)}}else{const k=await i(m);v={x:f.x*k.width,y:f.y*k.height}}const $=[w?`At (${Math.round(v.x)}, ${Math.round(v.y)}) ${y}:`:`At (${f.x.toFixed(2)}, ${f.y.toFixed(2)}) = (${Math.round(v.x)}, ${Math.round(v.y)}) ${y}:`];const T=p?o(g,m,v):Promise.resolve(null);if(m.platform==="android"){const k=(await Vi(m.serial)).filter(E=>{const I=E.bounds;return v.x>=I.x&&v.x<=I.x+I.width&&v.y>=I.y&&v.y<=I.y+I.height});const D=k.sort((E,I)=>E.bounds.width*E.bounds.height-I.bounds.width*I.bounds.height)[0];$.push(D?`- Native: ${D.cls.split(".").pop()}${D.text?` ${JSON.stringify(D.text.slice(0,80))}`:""}${D.desc?` desc=${JSON.stringify(D.desc.slice(0,60))}`:""}${D.id?` id=${D.id.split("/").pop()}`:""} at (${D.bounds.x}, ${D.bounds.y}) ${D.bounds.width}x${D.bounds.height}`:"- Native: nothing with a label or id here.")}else if(m.platform==="ios"){const k=JSON.parse(await Oe(["ui","describe-point","--udid",m.udid,String(Math.round(v.x)),String(Math.round(v.y)),"--json"],5e3));$.push(k?.type&&k.frame?.width?`- Native: ${ys(k)}`:"- Native: no accessible element here (plain views and canvases often have none).")}else{$.push("- Native: not available on a physical iPhone.")}if(!p)$.push("- App: not connected, so its views can't be read.");else{const k=await T;if(!k)$.push("- App: no answer (an older Buoy without hitTest, or the JS thread is busy).");else if(!k.found)$.push(`- App: ${k.reason??"no React view here (native UI, or outside the app's views)."}`);else{const D=k.view;const E=[D.name,D.testID?`testID=${D.testID}`:"",D.label?`label=${JSON.stringify(D.label)}`:"",D.text?JSON.stringify(D.text.slice(0,60)):""].filter(Boolean).join(" ");$.push(`- App: ${E}${k.path?.length?` in ${k.path.join(" < ")}`:""}.`);if(k.buoy)$.push("- This spot is Buoy's own overlay. A real touch here hits Buoy, not the app; close or minimize the panel first.");else if(k.target){const I=k.target;$.push(`- A tap here presses ${I.testID?`testID ${I.testID}`:JSON.stringify(I.text??I.label??I.name)} (${I.name}${I.nativeTag!=null?`, nativeTag ${I.nativeTag}`:""}).`)}else $.push("- Nothing tappable in the app here.")}}return M($.join("\n"))}catch(w){return x(j(w))}});e.registerTool("native_pinch",{description:"Pinch, spread or turn with two real fingers (CoreDevice on the iOS Simulator and a physical iPhone): zoom a map or photo, or rotate, in one call. scale above 1 zooms in (fingers spread), below 1 zooms out. The reply says how far a map turned and zoomed, read from the map itself. For other views it says how much of the area changed and shows it. No screenshot is needed. Not on Android or older Xcode, which can't send two fingers.",_meta:He(),inputSchema:{x:X.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),y:X.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),scale:X.number().min(.1).max(10).optional().describe("How far the fingers spread: 2 doubles the gap (zoom in), 0.5 halves it (zoom out). Default 2."),rotate:X.number().min(-180).max(180).optional().describe("Turn the fingers this many degrees while pinching, clockwise. Default 0."),durationMs:X.number().min(100).max(5e3).optional().describe("Default 500."),allowBuoy:X.boolean().optional().describe("Pinch even if a finger starts on Buoy."),deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{},{allowOffline:true});$n(g);await t(w,g,m);if(g.platform==="android")return x("Android's input command sends one finger only, so Buoy can't pinch there yet. Use the app's zoom buttons, or native_swipe for one-finger moves.");const p=await De();if(!p)return x(g.platform==="ios-device"?yn:"Pinching needs Xcode's CoreDevice touch support (Xcode 26 or later); idb sends one finger only.");const y=await i(g);const v=[(f.x??.5)*y.width,(f.y??.5)*y.height];const $=f.scale??2;const T=Math.min(y.width,y.height);const k=T*.4;const D=$>=1?Math.max(T*.04,k/$):k;const E=$>=1?k:Math.max(T*.04,k*$);const I=(f.rotate??0)*Math.PI/180;const R=ve=>[Math.min(y.width-1,Math.max(1,ve[0])),Math.min(y.height-1,Math.max(1,ve[1]))];const _=(ve,ye)=>[R([v[0]-ve*Math.cos(ye),v[1]-ve*Math.sin(ye)]),R([v[0]+ve*Math.cos(ye),v[1]+ve*Math.sin(ye)])];const[S,A]=_(D,0);const[O,F]=_(E,I);if(m&&!f.allowBuoy){for(const[ve,ye]of[S,A]){const Xr=await a(w,g,{x:ve,y:ye});if(Xr)return x(Xr)}}const P=f.durationMs??500;const K={x:Math.max(0,(v[0]-k)/y.width),y:Math.max(0,(v[1]-k)/y.height),width:Math.min(1,2*k/y.width),height:Math.min(1,2*k/y.height)};const N=m?await $s(n,w):[];const q=ve=>ve.find(ye=>!ye.frame||(f.x??.5)>=ye.frame.x&&(f.x??.5)<=ye.frame.x+ye.frame.width&&(f.y??.5)>=ye.frame.y&&(f.y??.5)<=ye.frame.y+ye.frame.height);const V=q(N);const pe=V?null:await on(g).catch(()=>null);const Q=g.platform==="ios-device"?await Fn(g.udid):g.udid;const B=await gp(p,Q,{...y,bundleId:g.platform==="ios"?g.bundleId:null},S,A,O,F,P);await s(w,g,"pinch",[S,A],[O,F],P,B);await je(400);const[z,re,le]=await Promise.all([m?Re(n,w,void 0,0):Promise.resolve(""),pe?on(g).catch(()=>null):Promise.resolve(null),V?$s(n,w):Promise.resolve([])]);const Ie=E/D;const Di=Ie>1.01?`Spread (zoom in, x${Ie.toFixed(2)})`:Ie<.99?`Pinched (zoom out, x${Ie.toFixed(2)})`:"Turned";const Dt=`${Di}${f.rotate?`, turning ${f.rotate} degrees,`:""} around (${Math.round(v[0])}, ${Math.round(v[1])}) ${b(g)} with two real fingers.`;const Vr=V?le.find(ve=>ve.name===V.name&&ve.testID===V.testID):void 0;const Yr=V&&Vr?Yp(V,Vr):null;if(Yr)return M(`${Dt}
603
- ${Yr}${z}`);const Dn=pe&&re?await Vp(pe,re,K):null;if(!Dn)return M(`${Dt}${z}`);return{content:[{type:"text",text:`${Dt}
604
- ${Dn.line}${z}`},{type:"image",data:Dn.image.data,mimeType:Dn.image.mimeType}]}}catch(w){return x(j(w))}})}async function Vp(e,n,t){try{const i=Ra(e,t);const r=Ra(n,t);const{percent:o}=br(i,r);const a=o<2?`Checked: the area under the fingers looks the same as before (${o.toFixed(1)}% changed). The pinch did not move anything there.`:`Checked: ${o.toFixed(0)}% of the area under the fingers changed, so it moved. It looks like this now:`;return{line:a,image:await Ct(r,480)}}catch{return null}}async function $s(e,n){try{const t=await e.callAction(Gt,"readMaps",{},n,{timeoutMs:2e3,silent:true});return t?.maps??[]}catch{return[]}}function Yp(e,n){const t=`The map${e.testID?` [${e.testID}]`:""}`;const i=[];const r=e.camera.heading,o=n.camera.heading;if(typeof r==="number"&&typeof o==="number"){const u=f=>(f%360+360)%360;const h=f=>Math.round(Math.min(u(f),360-u(f)));const b=Math.round(Math.abs((u(o)-u(r)+540)%360-180));i.push(`heading ${Math.round(u(r))}\xB0 before, ${Math.round(u(o))}\xB0 now (turned ${b}\xB0, now ${h(o)}\xB0 from north)`)}const a=e.camera.altitude,s=n.camera.altitude;const c=e.camera.zoom,l=n.camera.zoom;const d=typeof a==="number"&&typeof s==="number"&&a>0&&s>0?a/s:typeof c==="number"&&typeof l==="number"?Math.pow(2,l-c):null;if(d!=null&&Number.isFinite(d)){const u=d*d;const h=d>1.01?`zoomed in x${d.toFixed(2)}, so it shows about 1/${u.toFixed(1)} of the area it did`:d<.99?`zoomed out x${(1/d).toFixed(2)}, so it shows about ${u>0?(1/u).toFixed(1):"?"} times the area`:"same zoom";const b=typeof a==="number"&&typeof s==="number"?` (altitude ${Math.round(a).toLocaleString("en-US")} m to ${Math.round(s).toLocaleString("en-US")} m)`:typeof c==="number"&&typeof l==="number"?` (zoom ${c.toFixed(1)} to ${l.toFixed(1)})`:"";i.push(`${h}${b}`)}return i.length?`Checked in the app: ${t} ${i.join("; ")}.`:null}var pi="highlight-updates";var fi=e=>({content:[{type:"text",text:e}]});var mi=e=>({isError:true,content:[{type:"text",text:e}]});var ks=e=>e instanceof Error?e.message:String(e);var Xp=e=>new Promise(n=>setTimeout(n,e));var Zp=40;async function Re(e,n,t,i=300){let r="";for(let a=0;a<(t?6:1);a++){await Xp(a===0?i:250);try{const s=await e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:3e3,silent:true});r=String(s?.pathname??s?.path??"")}catch{break}if(!t||r.replace(/\(.*?\)\//g,"").replace(/\/$/,"")===t.replace(/\/$/,"")||r.endsWith(t))break}let o="";try{const a=await e.callAction(pi,"describeScreen",{},n,{timeoutMs:5e3,silent:true});const s=a?.elements??[];const c=new Set(s.filter(w=>w.name!=="Text").map(w=>w.text??w.label).filter(Boolean));const l=s.filter(w=>w.name!=="Text"&&w.tap&&(w.text??w.label));const d=(w,g)=>!!w.tap&&l.some(m=>Math.abs(m.tap.y-w.tap.y)<.06&&(m.text??m.label).includes(g));const u=s.filter(w=>{const g=w.text??w.label;if(w.name==="Text"&&(!g||/^[\s\uE000-\uF8FF]*$/.test(g)))return false;if(w.name==="Text"&&c.has(g))return false;if(w.name==="Text"&&g&&d(w,g))return false;return true});const h=w=>!w.tap||w.tap.x>=0&&w.tap.x<=1&&w.tap.y>=0&&w.tap.y<=1;const b=[...u.filter(h),...u.filter(w=>!h(w))];const f=b.slice(0,Zp);o=f.map(_s).join("\n");if(b.length>f.length)o+=`
602
+ ${w}${f}`);return h.isError?{...g,isError:true}:g}const d=er().length?" Note: a secret was typed earlier this session; if it is still on screen, the video will show it. Typing secrets is blocked until the recording stops.":" Typing secrets ({{secret:NAME}}) is blocked until the recording stops, because a video can't hide them.";return M(`Recording ${Bt(a)}. Do the steps, then call screen_recording with action "stop". It stops by itself after ${Jt/6e4} minutes.${d}`)}let i=t.deviceId;if(!i){try{i=n.requireProDevice()}catch{i=void 0}}if(!i||!_e.has(i)){if(i&&Sn.has(i)){const o=Sn.get(i);Sn.delete(i);return M(o)}if(_e.size===1)i=[..._e.keys()][0];else return x(_e.size?"Several recordings are running. Pass deviceId.":'No recording is running. Start one with action "start".')}const r=_e.get(i);await r.starting;if(!r.rec)return x("The recording did not start.");r.finishing??=Lr(n,r);try{return M(await r.finishing)}catch(o){r.finishing=void 0;return x(`Could not save the recording: ${j(o)}. Call stop again to retry.`)}}catch(i){return x(j(i))}})}var xn=vp(gp);var Xt="highlight-updates";var qe=e=>new Promise(n=>setTimeout(n,e));var rf="Native touch on the iOS Simulator needs idb on this Mac (brew install facebook/fb/idb-companion && pipx install fb-idb).";var Tn="Real touches on a physical iPhone need Xcode's CoreDevice touch support, and Buoy couldn't set it up on this Mac. Use tap_element (it calls the element's handler) or run_flow; screenshots, recording and app control work on the iPhone.";var yi=new Map;async function of(e){const n=yi.get(e);if(n&&Date.now()-n.at<$s)return n.size;const t=await Xo(e);const i={width:t.readUInt32BE(16),height:t.readUInt32BE(20)};yi.set(e,{size:i,at:Date.now()});return i}var Zt=new Map;function ys(e,n){if(n){if(e==="up")return{from:[.5,.88],to:[.5,.06]};if(e==="down")return{from:[.5,.3],to:[.5,.94]};if(e==="left")return{from:[.9,.5],to:[.1,.5]};return{from:[.1,.5],to:[.9,.5]}}const t=e==="left"?-.35:e==="right"?.35:0;const i=e==="up"?-.3:e==="down"?.3:0;return{from:[.5-t/2,.5-i/2],to:[.5+t/2,.5+i/2]}}var af=60;var $s=3e3;function sf(e){et.delete(e);Zt.delete(e);yi.delete(e)}function Dn(e){if(e.platform==="android")return;Zt.delete(e.udid);yi.delete(e.udid)}var ks=/idb on this Mac|SimulatorKit|not booted|connection refused|Failed to connect to companion/i;async function _s(){const e=(await xn("xcode-select",["-p"],{timeout:5e3}).catch(()=>({stdout:""}))).stdout.trim();if(!e.endsWith("/Contents/Developer"))return null;const n=se.dirname(e);const t=se.join(e,"Library","PrivateFrameworks","SimulatorKit.framework");const i=se.join(n,"SharedFrameworks","SimulatorKit.framework");const r=l=>ae.access(l).then(()=>true,()=>false);if(await r(t)||!await r(i))return null;const o=(await xn("plutil",["-extract","DTXcodeBuild","raw","-o","-",se.join(n,"Info.plist")]).catch(()=>({stdout:"x"}))).stdout.trim();const a=se.join(Cr.homedir(),".buoy","xcode-shim",o.replace(/[^\w.-]/g,"_"),"Contents");const s=se.join(a,"Developer");if(await r(se.join(s,"Library","PrivateFrameworks","SimulatorKit.framework")))return s;await ae.mkdir(se.join(s,"Library","PrivateFrameworks"),{recursive:true});const c=async(l,d,u)=>{for(const h of await ae.readdir(l)){if(h===u)continue;await ae.symlink(se.join(l,h),se.join(d,h)).catch(()=>void 0)}};await c(n,a,"Developer");await c(e,s,"Library");await c(se.join(e,"Library"),se.join(s,"Library"),"PrivateFrameworks");await c(se.join(e,"Library","PrivateFrameworks"),se.join(s,"Library","PrivateFrameworks"),"").catch(()=>void 0);await ae.symlink(i,se.join(s,"Library","PrivateFrameworks","SimulatorKit.framework")).catch(()=>void 0);return s}async function Ss(e){const n=`/tmp/idb/${e}.buoy-restart`;await ae.mkdir("/tmp/idb",{recursive:true}).catch(()=>void 0);const t=async()=>{try{await(await ae.open(n,"wx")).close();return true}catch{const i=await ae.stat(n).then(r=>Date.now()-r.mtimeMs,()=>0);if(i>15e3){const r=`${n}.stale-${process.pid}-${Date.now()}`;await ae.rename(n,r).then(()=>ae.unlink(r).catch(()=>void 0),()=>void 0);return t()}return false}};if(!await t()){for(let i=0;i<60;i++){await qe(250);if(!await ae.stat(n).then(()=>true,()=>false))return}return}try{await xn("pkill",["-f",`idb_companion --udid ${e}`],{timeout:5e3}).catch(()=>void 0);await qe(300);const i=await _s().catch(()=>null);const r=bp("idb_companion",["--udid",e,"--grpc-domain-sock",`/tmp/idb/${e}_companion.sock`,"--only","simulator"],{detached:true,stdio:"ignore",env:i?{...process.env,DEVELOPER_DIR:i}:process.env});r.on("error",()=>void 0);r.unref();await qe(1800)}finally{await ae.unlink(n).catch(()=>void 0)}}async function Re(e,n=15e3,t=false){try{const{stdout:i}=await xn("idb",e,{timeout:n});return i}catch(i){const r=i;if(r.code==="ENOENT")throw new Error(rf);const o=(r.stderr||r.message||"").slice(0,300);const a=e.includes("--udid")?e[e.indexOf("--udid")+1]:void 0;if(!t&&a&&/not booted|connection refused|Failed to connect to companion/i.test(o)){await Ss(a);return Re(e,n,true)}if(/SimulatorKit is required/i.test(o)){if(!t&&a&&await _s().catch(()=>null)){await Ss(a);return Re(e,n,true)}throw new Error("idb can't send touches with this Xcode: it can't load SimulatorKit, and Buoy could not work around it. Use tap_element (it calls the element's handler) for now. Reading the screen (describe_screen, screenshot_component) still works.")}throw new Error(`idb ${e[0]} ${e[1]??""} failed: ${o}`)}}var qr=class extends Error{constructor(e){super(`idb can only see an empty app shell on this simulator, so native labels and screen size can't be read. This happens after Maestro or another XCTest tool ran on it, and lasts until the simulator reboots. To fix it, reboot just this simulator: xcrun simctl shutdown ${e} && xcrun simctl boot ${e}. Until then, tap_element and taps by testID or query still work.`);this.name="AxShellOnlyError"}};async function $i(e){const n=JSON.parse(await Re(["ui","describe-all","--udid",e,"--json","--nested"]));const t=[];const i=o=>{t.push(o);for(const a of o.children??[])i(a)};n.forEach(i);const r=t.find(o=>o.type==="Application"&&o.frame);if(!t.some(o=>(o.frame?.width??0)>0))throw new qr(e);return{all:t,screen:r?.frame?{width:r.frame.width,height:r.frame.height}:{width:0,height:0}}}var et=new Map;var ki=se.join(Cr.homedir(),".buoy","cache","native-bars.json");var xs=false;async function Ts(){if(xs||process.env.VITEST)return;xs=true;try{const e=JSON.parse(await ae.readFile(ki,"utf8"));for(const[n,t]of Object.entries(e))if(!et.has(n)&&t?.screen&&Array.isArray(t.els))et.set(n,t)}catch{}}function Ds(){if(process.env.VITEST)return;const e=Object.fromEntries([...et].map(([t,i])=>[t,{screen:i.screen,els:i.els.map(r=>({AXLabel:r.AXLabel,type:r.type,frame:r.frame}))}]));const n=`${ki}.${process.pid}`;void ae.mkdir(se.dirname(ki),{recursive:true}).then(()=>ae.writeFile(n,JSON.stringify(e))).then(()=>ae.rename(n,ki)).catch(()=>void 0)}async function cf(e,n){await Ts();const t=et.get(e);const i=n.trim().toLowerCase();const r=t?.els.find(o=>(o.AXLabel??"").trim().toLowerCase()===i&&o.frame);if(!t||!r?.frame)return null;try{const o=Math.round(r.frame.x+r.frame.width/2);const a=Math.round(r.frame.y+r.frame.height/2);const s=JSON.parse(await Re(["ui","describe-point","--udid",e,String(o),String(a),"--json"],3e3));if((s.AXLabel??"").trim().toLowerCase()===i&&s.frame)return{el:s,screen:t.screen}}catch{}et.delete(e);Ds();return null}async function Is(e,n,t,i=false,r=[.95,.97,.09,.06]){const o=n.trim().toLowerCase();const a=Date.now()+8e3;const s=f=>{const w=(f.AXLabel??"").trim().toLowerCase();if(!w)return 0;if(w===o)return 3;if(w.startsWith(o))return 2;if(w.includes(o))return 1;return 0};const{width:c,height:l}=t.screen;const d=f=>{if(!c||!f.frame)return true;const w=f.frame.x+f.frame.width/2;const g=f.frame.y+f.frame.height/2;return w>=0&&w<=c&&g>=0&&g<=l};let u=t.all.filter(f=>s(f)>0&&f.frame&&f.type!=="Application"&&d(f));if(!u.some(f=>s(f)===3)&&t.screen.width){const f=r.map(w=>{const g=[];for(let m=20;m<t.screen.width;m+=40)g.push([m,Math.round(t.screen.height*w)]);return g});for(const w of f){if(Date.now()>=a)break;const g=await Promise.allSettled(w.map(async([p,y])=>JSON.parse(await Re(["ui","describe-point","--udid",e,String(p),String(y),"--json"],3e3))));const m=[];for(const p of g){if(p.status==="rejected"){if(ks.test(String(p.reason)))throw p.reason;continue}const y=p.value;if(y?.frame&&y.AXLabel&&y.type&&y.type!=="Application")m.push(y);if(s(y)>0&&y.frame)u.push(y)}if(m.length){const p=et.get(e);const y=p&&p.screen.width===t.screen.width&&p.screen.height===t.screen.height;const v=[...y?p.els:[],...m].filter(($,T,k)=>k.findIndex(D=>D.AXLabel===$.AXLabel&&JSON.stringify(D.frame)===JSON.stringify($.frame))===T);et.set(e,{screen:t.screen,els:v});Ds()}if(u.some(p=>s(p)===3))break}}const h=Math.max(0,...u.map(s));if(i&&h<3)return[];u=u.filter(f=>s(f)===h);let b=u.filter((f,w)=>u.findIndex(g=>JSON.stringify(g.frame)===JSON.stringify(f.frame))===w);if(b.length>1){const f=b.filter(w=>(w.AXLabel??"").trim()===n.trim());if(f.length)b=f}if(b.length>1){const f=b.filter(w=>w.type&&!/StaticText|Text$|Other|Image/.test(w.type));if(f.length)b=f}return b}async function lf(e){const n=await $i(e);const t=n.all.filter(s=>s.frame&&s.frame.width>0&&s.type!=="Application");const i=s=>`${s.AXLabel??""}|${JSON.stringify(s.frame)}`;const r=new Set(t.map(i));const{width:o,height:a}=n.screen;if(o){const s=t.filter(u=>!!u.type&&!/StaticText|Text$|Other|Image|Group|Heading/.test(u.type));const c=(u,h)=>s.some(b=>u>=b.frame.x&&u<=b.frame.x+b.frame.width&&h>=b.frame.y&&h<=b.frame.y+b.frame.height);const l=[];for(const u of[.95,.97,.09,.06])for(let h=20;h<o;h+=40){const b=Math.round(a*u);if(!c(h,b))l.push([h,b])}const d=Date.now()+6e3;for(let u=0;u<l.length&&Date.now()<d;u+=8){const h=await Promise.all(l.slice(u,u+8).map(([b,f])=>Re(["ui","describe-point","--udid",e,String(b),String(f),"--json"],3e3).then(w=>JSON.parse(w),w=>{if(ks.test(String(w)))throw w;return null})));for(const b of h){if(!b?.type||!b.frame?.width||b.type==="Application"||r.has(i(b)))continue;r.add(i(b));t.push(b)}}}t.sort((s,c)=>s.frame.y-c.frame.y||s.frame.x-c.frame.x);return{elements:t,screen:n.screen}}function Ms(e){const n=e.frame;const t=[e.type??"Element",e.AXLabel?JSON.stringify(e.AXLabel.slice(0,80)):"",e.AXUniqueId?`id=${e.AXUniqueId}`:"",e.AXValue?`value=${JSON.stringify(String(e.AXValue).slice(0,40))}`:"",e.enabled===false?"(disabled)":""].filter(Boolean);return`${t.join(" ")} at (${Math.round(n.x)}, ${Math.round(n.y)}) ${Math.round(n.width)}x${Math.round(n.height)}`}function Ur(e){async function n(p,y,v){if(y.platform!=="ios"||!v)return;try{const $=await e.callAction(Xt,"describeScreen",{},p,{timeoutMs:2e3,silent:true});const T=$?.screen?.width??0;const k=$?.screen?.height??0;const D=Math.max(T,k)/Math.max(1,Math.min(T,k));if(T>0&&k>0&&D>1.7)Zt.set(y.udid,{size:{width:T,height:k},at:Date.now()})}catch{}}async function t(p,y){if(p.platform==="android")return Vn(p.serial);if(p.platform==="ios-device")return{...await of(p.udid),isPhone:true};const v=Zt.get(p.udid);if(!y&&v&&Date.now()-v.at<$s)return v.size;try{const $=(y??await $i(p.udid)).screen;if(!$.width)throw new Error("Could not read the simulator's screen size from idb.");Zt.set(p.udid,{size:$,at:Date.now()});return $}catch($){if(!y&&v&&$ instanceof qr)return v.size;if(y||!await Ie())throw $;const T=se.join(Cr.tmpdir(),`buoy-size-${process.pid}-${Date.now()}.png`);try{await xn("xcrun",["simctl","io",p.udid,"screenshot","--type=png",T],{timeout:2e4});const k=await ae.readFile(T);return{width:k.readUInt32BE(16),height:k.readUInt32BE(20)}}finally{await ae.rm(T,{force:true}).catch(()=>void 0)}}}async function i(p,y,v,$){if(p.platform==="android")await Eu(p.serial,y.x,y.y,v);else if(p.platform==="ios-device"){const T=await Ie();if(!T)throw new Error(Tn);return cs(T,await Gn(p.udid),await t(p),y.x,y.y,v,$)}else if(await Ie()){return cs(await Ie(),p.udid,{...await t(p),bundleId:p.bundleId},y.x,y.y,v,$)}else{await Er(p.udid);await Re(["ui","tap","--udid",p.udid,...v?["--duration",(v/1e3).toFixed(2)]:[],String(Math.round(y.x)),String(Math.round(y.y))])}}async function r(p,y){if(p.platform==="android"){const $=await nr(p.serial,p.packageName);const T=$??{x:0,y:0,...await Vn(p.serial)};return[(y.x-T.x)/T.width,(y.y-T.y)/T.height]}const v=await t(p);return[y.x/v.width,y.y/v.height]}async function o(p,y,v){try{const[$,T]=await r(y,v);if(!($>=0&&$<=1&&T>=0&&T<=1))return null;return await e.callAction(Xt,"hitTest",{x:$,y:T},p,{timeoutMs:2500,silent:true})}catch{return null}}async function a(p,y,v){const $=await o(p,y,v);if(!$?.found||!$.buoy)return null;const T=$.path?.length?` (${$.path.slice(0,3).join(" in ")})`:"";return`Buoy's own overlay${T} is on top at that spot, so a real touch would land on Buoy, not the app. Nothing was touched. Close or minimize the Buoy panel first, or pass allowBuoy: true to touch Buoy on purpose.`}async function s(p,y,v,$,T,k,D){if(!Xp(p))return;try{const C=await t(y);const I=S=>[S[0]/C.width,S[1]/C.height];const R=Date.now()-k-(y.platform==="android"?50:80);const _=D!==void 0?D:y.platform==="android"?null:await kn(y.udid);Zp(p,{at:R,kind:v,ms:k,from:$.map(I),to:T?.map(I),screen:C,orientation:_})}catch{}}async function c(p,y,v){const $=await e.callAction(Xt,"describeScreen",v.allowBuoy?{includeBuoy:true}:{},p);const T=(v.query??"").toLowerCase();const k=($.elements??[]).filter(_=>v.nativeTag!=null?_.nativeTag===v.nativeTag:v.testID?_.testID===v.testID:(_.text??_.label??"").toLowerCase()===T);const D=k.filter(_=>_.tap);if(D.length===0)return"No element on screen matches that. Call describe_screen, or use label for native controls.";if(D.length>1&&v.nativeTag==null)return`${D.length} elements match equally well, so nothing was tapped. Use nativeTag: ${D.map(_=>`#${_.nativeTag}`).join(", ")}.`;const C=D[0].tap;const I=`"${D[0].text??D[0].label??D[0].name}"`;if(y.platform==="android"){const _=await nr(y.serial,y.packageName);const S=_??{x:0,y:0,...await Vn(y.serial)};return{x:S.x+C.x*S.width,y:S.y+C.y*S.height,what:I}}const R=await t(y);return{x:C.x*R.width,y:C.y*R.height,what:I}}async function l(p,y,v=false,$){if(p.platform==="android"){const I=Bu(await ir(p.serial),y).filter(_=>!v||[_.desc,_.text].some(S=>(S??"").trim().toLowerCase()===y.trim().toLowerCase()));if(I.length===0)return`No native element labeled "${y}" was found on screen.`;if(I.length>1)return`${I.length} native elements match "${y}" equally well, so nothing was tapped: ${I.map(_=>`"${_.desc||_.text||_.id}" at (${_.bounds.x}, ${_.bounds.y})`).join(", ")}. Tap one by x/y.`;const R=I[0].bounds;return{x:R.x+R.width/2,y:R.y+R.height/2,what:`"${I[0].desc||I[0].text||I[0].id}" (${I[0].cls.split(".").pop()})`}}const T=await cf(p.udid,y);if(T){Zt.set(p.udid,{size:T.screen,at:Date.now()});const I=T.el.frame;return{x:I.x+I.width/2,y:I.y+I.height/2,what:`"${T.el.AXLabel}" (${T.el.type})`}}let k;try{k=await $i(p.udid)}catch(I){if(!(I instanceof qr))throw I;if($?.online){const R=await m($.id,p,y,true,"app");if(!("miss"in R))return{...R,what:`${R.what} (found in the app's own screen, because idb can only see an empty shell on this simulator)`};if(R.tie)return R.miss}return I.message}await t(p,k);const D=await Is(p.udid,y,k,v);if(D.length===0)return`No native element labeled "${y}" was found on screen.`;if(D.length>1)return`${D.length} native elements match "${y}" equally well, so nothing was tapped: ${D.map(I=>`"${I.AXLabel}" at (${Math.round(I.frame.x)}, ${Math.round(I.frame.y)})`).join(", ")}. Tap one by x/y.`;const C=D[0].frame;return{x:C.x+C.width/2,y:C.y+C.height/2,what:`"${D[0].AXLabel}" (${D[0].type})`}}async function d(p,y,v,$,T=false,k=0,D){const C=[...y,...v].map(R=>String(Math.round(R)));const I=p.platform==="android"?null:await Ie();if(p.platform!=="android"&&!I)await Er(p.udid);if(p.platform==="android"){if(T)await J(p.serial,`input draganddrop ${C.join(" ")} ${Math.round($)}`);else await Ou(p.serial,y,v,$);return void 0}if(I){const R=p.platform==="ios-device"?await Gn(p.udid):p.udid;const _=D?.screen??await t(p);return Op(I,R,{..._,bundleId:p.platform==="ios"?p.bundleId:null},y,v,$,T?700:0,T?0:k,D?.orientation)}if(T)await Re(["ui","swipe","--udid",p.udid,...C,"--duration",($/1e3).toFixed(2),"--delta","4"]);else await Re(["ui","swipe","--udid",p.udid,...C,"--duration",($/1e3).toFixed(2)]);return void 0}async function u(p){try{const y=await e.callAction(Xt,"describeScreen",{},p,{timeoutMs:3e3,silent:true});const v=new Map;for(const $ of y?.elements??[]){if(!$.tap)continue;const T=`${$.name??""}|${$.text??$.label??""}|${$.nativeTag??""}`;if(v.has(T))v.set(T,{x:NaN,y:NaN});else v.set(T,$.tap)}return v}catch{return null}}function h(p,y,v,$,T=true){let k=0;let D=0;for(const[C,I]of y){const R=p.get(C);if(!R)D++;else if(Number.isFinite(R[v])&&Number.isFinite(I[v])&&Math.abs(I[v]-R[v])>$)k++}return k+(T&&D>=2?D:0)}function b(p,y,v){const $=[];for(const[T,k]of y){const D=p.get(T);if(D&&Number.isFinite(D[v])&&Number.isFinite(k[v])&&Math.abs(k[v]-D[v])>.002)$.push(k[v]-D[v])}if($.length<2)return 0;$.sort((T,k)=>T-k);return $[Math.floor($.length/2)]}async function f(p,y){await qe(120);let v=await u(p);const $=Date.now()+2500;while(v&&Date.now()<$){await qe(100);const T=await u(p);if(!T)break;const k=h(v,T,y,.002,false)===0;v=T;if(k)break}return v}async function w(p,y,v,$,T,k=15){const D=Math.abs($[1]-v[1])>=Math.abs($[0]-v[0])?"y":"x";let C=await u(p);if(C===null)throw new Error("Scrolling to the end needs the app connected, to see when the list stops moving.");let I=0;let R=false;const _={screen:await t(y)};const S=D==="y"?1:0;const A=D==="y"?_.screen.height:_.screen.width;const O=Math.sign($[S]-v[S]);const F=Math.abs($[S]-v[S])/A;const P=new Set;const K=(Q,B)=>{for(const[z,re]of B){const le=Q.get(z);if(le&&Number.isFinite(le[D])&&Number.isFinite(re[D])&&Math.abs(re[D]-le[D])>.01)P.add(z)}};let N=C;while(I<k){const Q=await d(y,v,$,T,false,af,_);if(Q!==void 0)_.orientation=Q;I++;await s(p,y,"swipe",[v],[$],T,Q);await qe(90);const B=await u(p);await qe(70);const z=await u(p);if(!B||!z)throw new Error("The app stopped answering while scrolling.");const re=b(B,z,D)*O;K(C,z);N=z;if(re>.004||re>=-.004&&h(B,z,D,.002,false)===0&&h(C,z,D,.01)>0){C=z;continue}const le=await f(p,D);if(!le)throw new Error("The app stopped answering while scrolling.");K(C,le);N=le;if(h(C,le,D,.01)===0){R=true;break}const Me=b(C,le,D)*O;if(Me>.01&&Me<F*.85){R=true;break}C=le}const q=[...N].filter(([Q,B])=>P.has(Q)&&B[D]>=0&&B[D]<=1&&Q.split("|")[1]);const V=Q=>Q.sort((B,z)=>(z[1][D]-B[1][D])*-O)[0];const pe=V(q.filter(([Q])=>!Q.startsWith("Text|")))??V(q);return{swipes:I,atEnd:R,lastRow:pe?pe[0].split("|")[1]:void 0}}async function g(p,y){if(p.platform==="android"){const $=await nr(p.serial,p.packageName);const T=$??{x:0,y:0,...await Vn(p.serial)};return{x:T.x+y.x*T.width,y:T.y+y.y*T.height}}const v=await t(p);return{x:y.x*v.width,y:y.y*v.height}}async function m(p,y,v,$,T){if($&&(T==="app"||T==="loose")){const k=await e.callAction(Xt,"describeScreen",{},p,{timeoutMs:4e3,silent:true}).catch(()=>null);const D=(k?.elements??[]).filter(_=>_.tap&&_.tap.x>=0&&_.tap.x<=1&&_.tap.y>=0&&_.tap.y<=1);const C=v.trim().toLowerCase();const I=_=>(_.text??_.label??"").trim().toLowerCase();const R=T==="app"?[D.filter(_=>_.testID===v),D.filter(_=>I(_)===C)]:[D.filter(_=>!!C&&I(_).includes(C))];for(const _ of R){if(!_.length)continue;const S=_.filter(P=>P.interactive);const A=S.length?S:_;const O=A.filter((P,K)=>A.findIndex(N=>Math.abs(N.tap.x-P.tap.x)<.02&&Math.abs(N.tap.y-P.tap.y)<.02)===K);if(O.length>1){return{tie:true,miss:`${O.length} elements match "${v}", so nothing was touched: ${O.slice(0,5).map(P=>P.testID?`testID ${P.testID}`:JSON.stringify(P.text??P.label??P.name)).join(", ")}. Use an exact testID.`}}const F=O[0];return{...await g(y,F.tap),what:`"${F.text??F.label??F.name}"${F.testID?` [testID=${F.testID}]`:""}`}}}if((T==="native"||T==="nativeExact")&&y.platform!=="ios-device"){const k=await l(y,v,T==="nativeExact",{id:p,online:$});if(typeof k!=="string")return k;if(/equally well/.test(k))return{miss:k,tie:true};if(/empty app shell/.test(k))return{miss:k}}return{miss:`Nothing on screen matches "${v}".`}}return{primeScreen:n,screenOf:t,touch:i,hitAt:o,buoyCover:a,mark:s,reactSpot:c,labelSpot:l,swipe:d,scrollToEnd:w,findSpot:m}}async function df(e,n,t,i,r=false){const o=Ur(e);const{id:a,host:s,online:c}=await ge(e,n);if(s.platform==="ios-device")return null;Dn(s);await o.primeScreen(a,s,c);const l=await o.findSpot(a,s,t,c,r?"nativeExact":"native");if("miss"in l)return null;if(c&&await o.buoyCover(a,s,l))return null;const d=i?600:void 0;const u=await o.touch(s,l,d);await o.mark(a,s,d?"hold":"tap",[[l.x,l.y]],void 0,d??150,u);return`${d?"Long-pressed":"Tapped"} ${l.what} with a real touch at (${Math.round(l.x)}, ${Math.round(l.y)}).`}function uf(e){if(process.platform!=="darwin"||process.env.BUOY_WARM_TOUCH==="0")return;const n=Date.now();const t=async()=>{try{const i=e.peekProjectDevice();const r=i?e.listDevices().find(o=>o.deviceId===i)?.platform:void 0;if(i&&r==="ios"){const{host:o}=await ge(e,i);if(o.platform!=="ios")return;const a=await Ie();if(a)await Ba(a,o.udid,["--device",o.udid,"ping"]);await kn(o.udid);await Ts();if(!et.has(o.udid)){const s=await $i(o.udid).catch(()=>null);if(s?.screen.width)await Is(o.udid,"\0no such label",s,false,[.95]).catch(()=>void 0)}return}if(i&&r)return}catch{}if(Date.now()-n<3e4)setTimeout(()=>void t(),500).unref()};setTimeout(()=>void t(),300).unref()}function hf(e,n){uf(n);const{primeScreen:t,screenOf:i,touch:r,hitAt:o,buoyCover:a,mark:s,reactSpot:c,labelSpot:l,swipe:d,scrollToEnd:u}=Ur(n);const h=X.string().optional().describe("Android: the adb serial, when several devices are connected.");const b=f=>f.platform==="ios"?"pt":"px";e.registerTool("native_tap",{_meta:Ge(),description:"Tap the screen for real (an OS touch: CoreDevice on the iOS Simulator, the way Xcode sends it, or idb on older Xcode; adb on Android), for native-only controls like a native tab bar or a system dialog, or when tap_element finds no handler. Pass holdMs for a long press. label and x/y work even when the app's JS is stuck.",inputSchema:{label:X.string().optional().describe('Native accessibility label, e.g. "Rewards" for a native tab.'),query:X.string().optional().describe("Text of an element from describe_screen."),testID:X.string().optional(),nativeTag:X.number().optional(),x:X.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:X.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),holdMs:X.number().min(100).max(1e4).optional().describe("Hold the touch this long: a long press (context menus, Paste, reorder handles). 600 is a typical long press."),allowBuoy:X.boolean().optional().describe("Touch even if Buoy's own panel covers the spot. By default the tap stops there, because it would hit Buoy instead of the app."),serial:h,deviceId:L}},async f=>{try{const w=f.nativeTag!=null||!!f.testID||!!f.query;const{id:g,host:m,online:p}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:!w});Dn(m);await t(g,m,p);if(m.platform==="ios-device"&&!await Ie())return x(Tn);let y;if(f.x!=null&&f.y!=null){const T=await i(m);y={x:f.x*T.width,y:f.y*T.height,what:`(${f.x.toFixed(2)}, ${f.y.toFixed(2)})`}}else if(w){y=await c(g,m,f)}else if(f.label){if(m.platform==="ios-device")return x("label works on simulators and Android. On a physical iPhone, tap by query, testID or nativeTag (from describe_screen), or by x and y.");y=await l(m,f.label,false,{id:g,online:p})}else{return x("Give label, query, testID, nativeTag, or x and y.")}if(typeof y==="string")return x(y);if(p&&!f.allowBuoy){const T=await a(g,m,y);if(T)return x(T)}const v=await r(m,y,f.holdMs);await s(g,m,f.holdMs?"hold":"tap",[[y.x,y.y]],void 0,f.holdMs??150,v);const $=p?await Pe(n,g):" (The app is not connected, so its screen was not read back.)";return M(`${f.holdMs?`Long-pressed (${f.holdMs}ms)`:"Tapped"} ${y.what} with a real touch at (${Math.round(y.x)}, ${Math.round(y.y)}) ${b(m)}.${$}`)}catch(w){return x(j(w))}});e.registerTool("native_type",{description:"Type text on the real keyboard into the focused field (CoreDevice or idb on the iOS Simulator, adb on Android). Tap the field first (tap_element or native_tap). Unlike tap_element's text, this goes through the keyboard like a person typing. It types plain ASCII only (letters, digits, punctuation, newlines).",inputSchema:{text:X.string().describe("The text to type. Write {{secret:NAME}} for a password: Buoy types BUOY_SECRET_NAME from its environment and hides it in what it returns. Prefer real password fields (shown as dots)."),serial:h,deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});if(g.platform==="ios-device"&&!await Ie())return x(Tn);const p=Kn(f.text);let y=false;if(g.platform==="android")await Ru(g.serial,p);else if(g.platform==="ios-device")await ls(await Ie(),await Gn(g.udid),p);else{const $=await Ie();if($){await ls($,g.udid,p);y=true}else{await Er(g.udid);await Re(["ui","text","--udid",g.udid,p])}}if(!y)await qe(300);const v=m?await Pe(n,w):"";return M(`Typed ${JSON.stringify(f.text)} on the keyboard.${v}`)}catch(w){return x(j(w))}});e.registerTool("native_swipe",{_meta:Ge(),description:"Swipe the screen for real (CoreDevice or idb on the iOS Simulator, adb on Android): scroll a list, dismiss a sheet, change a page. Give a direction (the way your finger moves) or from/to points as fractions of the screen. To reach the bottom of a list, pass untilEnd: true. It swipes again until the screen stops moving, in one call.",inputSchema:{direction:X.enum(["up","down","left","right"]).optional().describe('"up" scrolls content down the list (finger moves up).'),fromX:X.number().min(0).max(1).optional(),fromY:X.number().min(0).max(1).optional(),toX:X.number().min(0).max(1).optional(),toY:X.number().min(0).max(1).optional(),durationMs:X.number().min(50).max(5e3).optional().describe("Default 300 (1200 with drag)."),drag:X.boolean().optional().describe("Drag instead of flick: press, hold, then move (reorder handles, drag and drop, sliders). On the iOS Simulator with older Xcode (idb) it is a slow swipe only."),untilEnd:X.boolean().optional().describe("Keep swiping until the screen stops moving (the end of the list), up to 15 swipes. Returns the last screen."),allowBuoy:X.boolean().optional().describe("Swipe even if Buoy's own panel covers the starting point."),serial:h,deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});Dn(g);await t(w,g,m);if(g.platform==="ios-device"&&!await Ie())return x(Tn);const p=await i(g);let y;let v;if(f.fromX!=null&&f.fromY!=null&&f.toX!=null&&f.toY!=null){y=[f.fromX,f.fromY];v=[f.toX,f.toY]}else{({from:y,to:v}=ys(f.direction??"up",!!f.untilEnd))}const $=[y[0]*p.width,y[1]*p.height];const T=[v[0]*p.width,v[1]*p.height];let k=0;if(f.drag&&g.platform==="android"){const _=Number((await J(g.serial,"settings get secure long_press_timeout").catch(()=>"")).trim())||500;k=_*1.5+600}const D=Math.max(f.durationMs??(f.drag?1200:300),k);if(m&&!f.allowBuoy){const _=await a(w,g,{x:$[0],y:$[1]});if(_)return x(_)}if(f.untilEnd){if(f.drag)return x("untilEnd scrolls with flicks. Leave out drag.");if(!m)return x("untilEnd needs the app connected, to see when the list stops moving.");const _=await u(w,g,$,T,f.durationMs??250);const{swipes:S,atEnd:A}=_;const O=await Pe(n,w,void 0,0);const{lastRow:F}=_;return M(`${A?`Reached the end of the list after ${S} swipe(s)${F?`. The last row is "${F}"`:""}`:`Swiped ${S} times and the screen was still moving`}.${O}`)}const C=g.platform==="android"?null:await Ie();const I=await d(g,$,T,D,f.drag);await s(w,g,f.drag?"drag":"swipe",[$],[T],D+(f.drag&&C?700:0),I);await qe(400);const R=m?await Pe(n,w):"";return M(`${f.drag?"Dragged":"Swiped"} from (${$.map(Math.round).join(", ")}) to (${T.map(Math.round).join(", ")}) ${b(g)}.${R}`)}catch(w){return x(j(w))}});e.registerTool("native_inspect",{description:"Read native controls on the screen. Pass pointX and pointY for points on iOS Sims, pixels elsewhere. Use x and y for screen fractions (0 to 1). Leave all points out to list controls, with types, labels, IDs, and frames. This includes tabs, alerts, and sheets. A point read also checks the app view and Buoy's cover. iOS Sims use idb; Android uses uiautomator. Real iPhones show just the app view.",inputSchema:{pointX:X.number().finite().min(0).optional().describe("From the left. iOS Sims use points. Other hosts use pixels."),pointY:X.number().finite().min(0).optional().describe("From the top. iOS Sims use points. Other hosts use pixels."),x:X.number().min(0).max(1).optional().describe("Horizontal position, 0 to 1."),y:X.number().min(0).max(1).optional().describe("Vertical position, 0 to 1."),serial:h,deviceId:L}},async f=>{try{const w=f.pointX!=null||f.pointY!=null;if(w&&(f.pointX==null||f.pointY==null))return x("Use pointX and pointY together.");if(w&&(f.x!=null||f.y!=null))return x("Use pointX/pointY or x/y, not both.");if(!w&&f.x==null!==(f.y==null))return x("Use x and y together.");const{id:g,host:m,online:p}=await ge(n,f.deviceId,{serial:f.serial},{allowOffline:true});Dn(m);await t(g,m,p);const y=b(m);if(!w&&(f.x==null||f.y==null)){if(m.platform==="ios-device")return x("A physical iPhone doesn't share its native element tree with this Mac. Use describe_screen for the app's elements, or native_inspect with x and y to see what is at a point.");let k;let D=0;if(m.platform==="android"){const I=await ir(m.serial);k=I.filter(R=>R.text||R.desc||R.id||R.clickable).map(R=>{const _=R.bounds;const S=[R.cls.split(".").pop(),R.text?JSON.stringify(R.text.slice(0,80)):"",R.desc?`desc=${JSON.stringify(R.desc.slice(0,60))}`:"",R.id?`id=${R.id.split("/").pop()}`:"",R.clickable?"(clickable)":""];return`${S.filter(Boolean).join(" ")} at (${_.x}, ${_.y}) ${_.width}x${_.height}`})}else{const{elements:I,screen:R}=await lf(m.udid);const _=I.filter(S=>!R.width||S.frame.y<R.height&&S.frame.y+S.frame.height>0&&S.frame.x<R.width&&S.frame.x+S.frame.width>0);D=I.length-_.length;k=_.map(Ms)}const C=k.slice(0,150);return M([`${k.length} native element(s) on screen, top to bottom (${y}; frames are x, y, width x height)${D?`. ${D} more are scrolled out of view`:""}:`,...C.map(I=>`- ${I}`),...k.length>C.length?[`(${k.length-C.length} more not shown.)`]:[],"Tap one with native_tap label or x/y. For the app's React elements (testID, handlers), use describe_screen."].join("\n"))}let v;if(w){const k=await i(m);v={x:f.pointX,y:f.pointY};if(v.x>=k.width||v.y>=k.height){return x(`That point is outside the screen (${k.width} x ${k.height} ${y}).`)}}else{const k=await i(m);v={x:f.x*k.width,y:f.y*k.height}}const $=[w?`At (${Math.round(v.x)}, ${Math.round(v.y)}) ${y}:`:`At (${f.x.toFixed(2)}, ${f.y.toFixed(2)}) = (${Math.round(v.x)}, ${Math.round(v.y)}) ${y}:`];const T=p?o(g,m,v):Promise.resolve(null);if(m.platform==="android"){const k=(await ir(m.serial)).filter(C=>{const I=C.bounds;return v.x>=I.x&&v.x<=I.x+I.width&&v.y>=I.y&&v.y<=I.y+I.height});const D=k.sort((C,I)=>C.bounds.width*C.bounds.height-I.bounds.width*I.bounds.height)[0];$.push(D?`- Native: ${D.cls.split(".").pop()}${D.text?` ${JSON.stringify(D.text.slice(0,80))}`:""}${D.desc?` desc=${JSON.stringify(D.desc.slice(0,60))}`:""}${D.id?` id=${D.id.split("/").pop()}`:""} at (${D.bounds.x}, ${D.bounds.y}) ${D.bounds.width}x${D.bounds.height}`:"- Native: nothing with a label or id here.")}else if(m.platform==="ios"){const k=JSON.parse(await Re(["ui","describe-point","--udid",m.udid,String(Math.round(v.x)),String(Math.round(v.y)),"--json"],5e3));$.push(k?.type&&k.frame?.width?`- Native: ${Ms(k)}`:"- Native: no accessible element here (plain views and canvases often have none).")}else{$.push("- Native: not available on a physical iPhone.")}if(!p)$.push("- App: not connected, so its views can't be read.");else{const k=await T;if(!k)$.push("- App: no answer (an older Buoy without hitTest, or the JS thread is busy).");else if(!k.found)$.push(`- App: ${k.reason??"no React view here (native UI, or outside the app's views)."}`);else{const D=k.view;const C=[D.name,D.testID?`testID=${D.testID}`:"",D.label?`label=${JSON.stringify(D.label)}`:"",D.text?JSON.stringify(D.text.slice(0,60)):""].filter(Boolean).join(" ");$.push(`- App: ${C}${k.path?.length?` in ${k.path.join(" < ")}`:""}.`);if(k.buoy)$.push("- This spot is Buoy's own overlay. A real touch here hits Buoy, not the app; close or minimize the panel first.");else if(k.target){const I=k.target;$.push(`- A tap here presses ${I.testID?`testID ${I.testID}`:JSON.stringify(I.text??I.label??I.name)} (${I.name}${I.nativeTag!=null?`, nativeTag ${I.nativeTag}`:""}).`)}else $.push("- Nothing tappable in the app here.")}}return M($.join("\n"))}catch(w){return x(j(w))}});e.registerTool("native_pinch",{description:"Pinch, spread or turn with two real fingers (CoreDevice on the iOS Simulator and a physical iPhone): zoom a map or photo, or rotate, in one call. scale above 1 zooms in (fingers spread), below 1 zooms out. The reply says how far a map turned and zoomed, read from the map itself. For other views it says how much of the area changed and shows it. No screenshot is needed. Not on Android or older Xcode, which can't send two fingers.",_meta:Ge(),inputSchema:{x:X.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),y:X.number().min(0).max(1).optional().describe("Center between the fingers, 0 to 1. Default 0.5."),scale:X.number().min(.1).max(10).optional().describe("How far the fingers spread: 2 doubles the gap (zoom in), 0.5 halves it (zoom out). Default 2."),rotate:X.number().min(-180).max(180).optional().describe("Turn the fingers this many degrees while pinching, clockwise. Default 0."),durationMs:X.number().min(100).max(5e3).optional().describe("Default 500."),allowBuoy:X.boolean().optional().describe("Pinch even if a finger starts on Buoy."),deviceId:L}},async f=>{try{const{id:w,host:g,online:m}=await ge(n,f.deviceId,{},{allowOffline:true});Dn(g);await t(w,g,m);if(g.platform==="android")return x("Android's input command sends one finger only, so Buoy can't pinch there yet. Use the app's zoom buttons, or native_swipe for one-finger moves.");const p=await Ie();if(!p)return x(g.platform==="ios-device"?Tn:"Pinching needs Xcode's CoreDevice touch support (Xcode 26 or later); idb sends one finger only.");const y=await i(g);const v=[(f.x??.5)*y.width,(f.y??.5)*y.height];const $=f.scale??2;const T=Math.min(y.width,y.height);const k=T*.4;const D=$>=1?Math.max(T*.04,k/$):k;const C=$>=1?k:Math.max(T*.04,k*$);const I=(f.rotate??0)*Math.PI/180;const R=ye=>[Math.min(y.width-1,Math.max(1,ye[0])),Math.min(y.height-1,Math.max(1,ye[1]))];const _=(ye,$e)=>[R([v[0]-ye*Math.cos($e),v[1]-ye*Math.sin($e)]),R([v[0]+ye*Math.cos($e),v[1]+ye*Math.sin($e)])];const[S,A]=_(D,0);const[O,F]=_(C,I);if(m&&!f.allowBuoy){for(const[ye,$e]of[S,A]){const ao=await a(w,g,{x:ye,y:$e});if(ao)return x(ao)}}const P=f.durationMs??500;const K={x:Math.max(0,(v[0]-k)/y.width),y:Math.max(0,(v[1]-k)/y.height),width:Math.min(1,2*k/y.width),height:Math.min(1,2*k/y.height)};const N=m?await As(n,w):[];const q=ye=>ye.find($e=>!$e.frame||(f.x??.5)>=$e.frame.x&&(f.x??.5)<=$e.frame.x+$e.frame.width&&(f.y??.5)>=$e.frame.y&&(f.y??.5)<=$e.frame.y+$e.frame.height);const V=q(N);const pe=V?null:await un(g).catch(()=>null);const Q=g.platform==="ios-device"?await Gn(g.udid):g.udid;const B=await Rp(p,Q,{...y,bundleId:g.platform==="ios"?g.bundleId:null},S,A,O,F,P);await s(w,g,"pinch",[S,A],[O,F],P,B);await qe(400);const[z,re,le]=await Promise.all([m?Pe(n,w,void 0,0):Promise.resolve(""),pe?un(g).catch(()=>null):Promise.resolve(null),V?As(n,w):Promise.resolve([])]);const Me=C/D;const Fi=Me>1.01?`Spread (zoom in, x${Me.toFixed(2)})`:Me<.99?`Pinched (zoom out, x${Me.toFixed(2)})`:"Turned";const At=`${Fi}${f.rotate?`, turning ${f.rotate} degrees,`:""} around (${Math.round(v[0])}, ${Math.round(v[1])}) ${b(g)} with two real fingers.`;const ro=V?le.find(ye=>ye.name===V.name&&ye.testID===V.testID):void 0;const oo=V&&ro?ff(V,ro):null;if(oo)return M(`${At}
603
+ ${oo}${z}`);const Pn=pe&&re?await pf(pe,re,K):null;if(!Pn)return M(`${At}${z}`);return{content:[{type:"text",text:`${At}
604
+ ${Pn.line}${z}`},{type:"image",data:Pn.image.data,mimeType:Pn.image.mimeType}]}}catch(w){return x(j(w))}})}async function pf(e,n,t){try{const i=za(e,t);const r=za(n,t);const{percent:o}=Tr(i,r);const a=o<2?`Checked: the area under the fingers looks the same as before (${o.toFixed(1)}% changed). The pinch did not move anything there.`:`Checked: ${o.toFixed(0)}% of the area under the fingers changed, so it moved. It looks like this now:`;return{line:a,image:await Nt(r,480)}}catch{return null}}async function As(e,n){try{const t=await e.callAction(Xt,"readMaps",{},n,{timeoutMs:2e3,silent:true});return t?.maps??[]}catch{return[]}}function ff(e,n){const t=`The map${e.testID?` [${e.testID}]`:""}`;const i=[];const r=e.camera.heading,o=n.camera.heading;if(typeof r==="number"&&typeof o==="number"){const u=f=>(f%360+360)%360;const h=f=>Math.round(Math.min(u(f),360-u(f)));const b=Math.round(Math.abs((u(o)-u(r)+540)%360-180));i.push(`heading ${Math.round(u(r))}\xB0 before, ${Math.round(u(o))}\xB0 now (turned ${b}\xB0, now ${h(o)}\xB0 from north)`)}const a=e.camera.altitude,s=n.camera.altitude;const c=e.camera.zoom,l=n.camera.zoom;const d=typeof a==="number"&&typeof s==="number"&&a>0&&s>0?a/s:typeof c==="number"&&typeof l==="number"?Math.pow(2,l-c):null;if(d!=null&&Number.isFinite(d)){const u=d*d;const h=d>1.01?`zoomed in x${d.toFixed(2)}, so it shows about 1/${u.toFixed(1)} of the area it did`:d<.99?`zoomed out x${(1/d).toFixed(2)}, so it shows about ${u>0?(1/u).toFixed(1):"?"} times the area`:"same zoom";const b=typeof a==="number"&&typeof s==="number"?` (altitude ${Math.round(a).toLocaleString("en-US")} m to ${Math.round(s).toLocaleString("en-US")} m)`:typeof c==="number"&&typeof l==="number"?` (zoom ${c.toFixed(1)} to ${l.toFixed(1)})`:"";i.push(`${h}${b}`)}return i.length?`Checked in the app: ${t} ${i.join("; ")}.`:null}var _i="highlight-updates";var Si=e=>({content:[{type:"text",text:e}]});var xi=e=>({isError:true,content:[{type:"text",text:e}]});var Cs=e=>e instanceof Error?e.message:String(e);var mf=e=>new Promise(n=>setTimeout(n,e));var wf=40;async function Pe(e,n,t,i=300){let r="";for(let a=0;a<(t?6:1);a++){await mf(a===0?i:250);try{const s=await e.callAction("route-events","getCurrentRoute",{},n,{timeoutMs:3e3,silent:true});r=String(s?.pathname??s?.path??"")}catch{break}if(!t||r.replace(/\(.*?\)\//g,"").replace(/\/$/,"")===t.replace(/\/$/,"")||r.endsWith(t))break}let o="";try{const a=await e.callAction(_i,"describeScreen",{},n,{timeoutMs:5e3,silent:true});const s=a?.elements??[];const c=new Set(s.filter(w=>w.name!=="Text").map(w=>w.text??w.label).filter(Boolean));const l=s.filter(w=>w.name!=="Text"&&w.tap&&(w.text??w.label));const d=(w,g)=>!!w.tap&&l.some(m=>Math.abs(m.tap.y-w.tap.y)<.06&&(m.text??m.label).includes(g));const u=s.filter(w=>{const g=w.text??w.label;if(w.name==="Text"&&(!g||/^[\s\uE000-\uF8FF]*$/.test(g)))return false;if(w.name==="Text"&&c.has(g))return false;if(w.name==="Text"&&g&&d(w,g))return false;return true});const h=w=>!w.tap||w.tap.x>=0&&w.tap.x<=1&&w.tap.y>=0&&w.tap.y<=1;const b=[...u.filter(h),...u.filter(w=>!h(w))];const f=b.slice(0,wf);o=f.map(Es).join("\n");if(b.length>f.length)o+=`
605
605
  \u2026 ${b.length-f.length} more (describe_screen lists everything)`}catch{o="(could not read the screen)"}return`
606
606
 
607
607
  Now on ${r?`\`${r}\``:"an unknown route"}. Screen (\u2022 = tappable):
608
- ${o}`}var Qp=be.enum(["screen","none"]).optional().describe('What to return after the action. Default "screen": the current route and what is on screen, so no follow-up describe_screen is needed. "none" skips it.');function _s(e){let n=e.interactive?"\u2022 ":" ";n+=e.name;const t=e.text??e.label;if(t)n+=` "${t}"`;if(e.role)n+=` <${e.role}>`;if(e.control)n+=` {${e.control}${e.value!==void 0?`=${JSON.stringify(e.value)}`:""}}`;if(e.longPressable)n+=" {long-press}";if(e.testID)n+=` [testID=${e.testID}]`;if(e.tap)n+=` (tap: ${e.tap.x},${e.tap.y})`;if(e.nativeTag!=null)n+=` #${e.nativeTag}`;return n}var Pr=0;var Ss=false;var ef=/isn't showing|No element matched|no JS press handler/;var xs=3;var tf="\n\nTip: driving several steps? `run_flow` does a whole tap/type/wait/assert sequence in ONE call and reports the final screen plus the network and console output it produced.";function nf(e,n){e.registerTool("describe_screen",{_meta:He(),description:"List elements exposed by the app React-fiber inspection integration, with available names, text, roles, testIDs, positions and native tags. This is not a complete native accessibility tree or a visual screenshot. Use the returned controls to select a target for tap_element; refresh after navigation or state changes. Empty results can reflect unavailable inspection. Needs the highlight-updates interaction integration. Works on the free tier.",inputSchema:{interactiveOnly:be.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),includeBuoy:be.boolean().optional().describe("Also list Buoy's own overlay \u2014 the dial, tool sheets, Ask Buoy's chat. Default false: the screen described is the app's. Exact testIDs inside Buoy (buoy-\u2026) can still be tapped without this."),includeHidden:be.boolean().optional().describe("Also list screens that are mounted but not showing (other tabs, screens under the current one). Default false: only what is on screen."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=Zh(n,i).catch(()=>null);const o=await n.callAction(pi,"describeScreen",{includeBuoy:t.includeBuoy,includeHidden:t.includeHidden},i);const a=o?.elements??[];const s=t.interactiveOnly?a.filter(w=>w.interactive):a;if(s.length===0){return fi("No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app.")}const c=o.hiddenBuoy?` Buoy's own overlay (${o.hiddenBuoy} element${o.hiddenBuoy===1?"":"s"}) is left out \u2014 pass includeBuoy:true to see it.`:"";const l=o.hiddenScreens?` ${o.hiddenScreens} screen${o.hiddenScreens===1?" that isn't":"s that aren't"} showing (other tabs, screens underneath) left out \u2014 pass includeHidden:true to see them.`:"";const d=`Screen ${o.screen.width}\xD7${o.screen.height} \u2014 ${s.length} element${s.length===1?"":"s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).${c}${l}`;let u="";if(!t.includeHidden&&a.some(w=>w.name==="BottomTabsScreen")){try{const w=await n.callAction(pi,"describeScreen",{includeHidden:true},i,{timeoutMs:3e3,silent:true});const g=[...new Set((w?.elements??[]).filter(m=>m.name==="BottomTabsScreen").map(m=>m.text??m.label).filter(m=>!!m))];if(g.length>1)u=`
609
- Tab bar (native, tap with tap_element query): ${g.join(", ")}.`}catch{}}const h=Ss?"":`
610
- Tip: run_flow does taps, typing and swipes in one call, e.g. steps [{tap:"Menu"},{tap:"Burgers"},{swipe:"up",untilEnd:true}] taps two things and scrolls to the end of the list.`;Ss=true;const b=await r;const f=s.map(w=>_s(w)+Qh(b,w.text));return fi(`${d}${u}${h}
608
+ ${o}`}var gf=be.enum(["screen","none"]).optional().describe('What to return after the action. Default "screen": the current route and what is on screen, so no follow-up describe_screen is needed. "none" skips it.');function Es(e){let n=e.interactive?"\u2022 ":" ";n+=e.name;const t=e.text??e.label;if(t)n+=` "${t}"`;if(e.role)n+=` <${e.role}>`;if(e.control)n+=` {${e.control}${e.value!==void 0?`=${JSON.stringify(e.value)}`:""}}`;if(e.longPressable)n+=" {long-press}";if(e.testID)n+=` [testID=${e.testID}]`;if(e.tap)n+=` (tap: ${e.tap.x},${e.tap.y})`;if(e.nativeTag!=null)n+=` #${e.nativeTag}`;return n}var zr=0;var Os=false;var bf=/isn't showing|No element matched|no JS press handler/;var Rs=3;var vf="\n\nTip: driving several steps? `run_flow` does a whole tap/type/wait/assert sequence in ONE call and reports the final screen plus the network and console output it produced.";function yf(e,n){e.registerTool("describe_screen",{_meta:Ge(),description:"List elements exposed by the app React-fiber inspection integration, with available names, text, roles, testIDs, positions and native tags. This is not a complete native accessibility tree or a visual screenshot. Use the returned controls to select a target for tap_element; refresh after navigation or state changes. Empty results can reflect unavailable inspection. Needs the highlight-updates interaction integration. Works on the free tier.",inputSchema:{interactiveOnly:be.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),includeBuoy:be.boolean().optional().describe("Also list Buoy's own overlay \u2014 the dial, tool sheets, Ask Buoy's chat. Default false: the screen described is the app's. Exact testIDs inside Buoy (buoy-\u2026) can still be tapped without this."),includeHidden:be.boolean().optional().describe("Also list screens that are mounted but not showing (other tabs, screens under the current one). Default false: only what is on screen."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=mp(n,i).catch(()=>null);const o=await n.callAction(_i,"describeScreen",{includeBuoy:t.includeBuoy,includeHidden:t.includeHidden},i);const a=o?.elements??[];const s=t.interactiveOnly?a.filter(w=>w.interactive):a;if(s.length===0){return Si("No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app.")}const c=o.hiddenBuoy?` Buoy's own overlay (${o.hiddenBuoy} element${o.hiddenBuoy===1?"":"s"}) is left out \u2014 pass includeBuoy:true to see it.`:"";const l=o.hiddenScreens?` ${o.hiddenScreens} screen${o.hiddenScreens===1?" that isn't":"s that aren't"} showing (other tabs, screens underneath) left out \u2014 pass includeHidden:true to see them.`:"";const d=`Screen ${o.screen.width}\xD7${o.screen.height} \u2014 ${s.length} element${s.length===1?"":"s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).${c}${l}`;let u="";if(!t.includeHidden&&a.some(w=>w.name==="BottomTabsScreen")){try{const w=await n.callAction(_i,"describeScreen",{includeHidden:true},i,{timeoutMs:3e3,silent:true});const g=[...new Set((w?.elements??[]).filter(m=>m.name==="BottomTabsScreen").map(m=>m.text??m.label).filter(m=>!!m))];if(g.length>1)u=`
609
+ Tab bar (native, tap with tap_element query): ${g.join(", ")}.`}catch{}}const h=Os?"":`
610
+ Tip: run_flow does taps, typing and swipes in one call, e.g. steps [{tap:"Menu"},{tap:"Burgers"},{swipe:"up",untilEnd:true}] taps two things and scrolls to the end of the list.`;Os=true;const b=await r;const f=s.map(w=>Es(w)+wp(b,w.text));return Si(`${d}${u}${h}
611
611
 
612
- ${f.join("\n")}`)}catch(i){return mi(ks(i))}});e.registerTool("tap_element",{_meta:He(),description:"Invoke a supported element handler in JavaScript: onPress/onLongPress, onValueChange or onChangeText. This does not reproduce native hit testing, keyboard entry or a physical gesture. Select an exposed element by nativeTag, testID or fuzzy query; inspect the matched target and resulting behavior. value sets a supported toggle or slider, text invokes a text-input handler, and longPress selects onLongPress. Can attempt to scroll an off-screen target into view. If a plain query finds no JS handler, Buoy tries a real touch on a native control with that label (a native tab bar). Use describe_screen for current controls and run_flow for sequences. Needs the app interaction integration. Works on the free tier.",inputSchema:{nativeTag:be.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),testID:be.string().optional().describe("Exact testID of the element."),query:be.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),value:be.union([be.number(),be.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),text:be.string().optional().describe("For a text input: the string to set via onChangeText."),longPress:be.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),scrollIntoView:be.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),includeBuoy:be.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),includeHidden:be.boolean().optional().describe("Also match elements on screens that are mounted but not showing. Default false."),observe:Qp,deviceId:L}},async t=>{try{if(t.nativeTag==null&&!t.testID&&!t.query){return mi("Provide one of: nativeTag, testID, or query (from describe_screen).")}const i=n.requireProDevice(t.deviceId);const r=await n.callAction(pi,"tapElement",{nativeTag:t.nativeTag,testID:t.testID,query:t.query,value:t.value,text:t.text===void 0?void 0:Bn(t.text),longPress:t.longPress,scrollIntoView:t.scrollIntoView,includeBuoy:t.includeBuoy,includeHidden:t.includeHidden},i);Pr+=1;const o=Pr>=xs&&Pr%xs===0?tf:"";if(r?.tapped&&r.matched){const l=r.matched;const d=l.text??l.label??l.name;const u=l.testID?` [testID=${l.testID}]`:l.nativeTag!=null?` #${l.nativeTag}`:"";const h=r.scrolled?" (scrolled into view)":"";const b=l.via==="onValueChange"?`Set "${d}"${u} \u2192 ${JSON.stringify(l.value)}`:l.via==="onChangeText"?`Typed ${JSON.stringify(l.value)} into "${d}"${u}`:l.via==="onLongPress"?`Long-pressed "${d}"${u}`:`Tapped "${d}"${u}`;const f=r.effect;const w=!f?"":f.commits>0?` ${f.commits} re-render${f.commits===1?"":"s"} followed.`:` No re-render followed within ${f.observedMs}ms \u2014 the handler ran but the app did not react. The element may sit on a screen beneath the current one, or be inert; do not treat this as done.`;const g=r.alsoMatched?.length?`
612
+ ${f.join("\n")}`)}catch(i){return xi(Cs(i))}});e.registerTool("tap_element",{_meta:Ge(),description:"Invoke a supported element handler in JavaScript: onPress/onLongPress, onValueChange or onChangeText. This does not reproduce native hit testing, keyboard entry or a physical gesture. Select an exposed element by nativeTag, testID or fuzzy query; inspect the matched target and resulting behavior. value sets a supported toggle or slider, text invokes a text-input handler, and longPress selects onLongPress. Can attempt to scroll an off-screen target into view. If a plain query finds no JS handler, Buoy tries a real touch on a native control with that label (a native tab bar). Use describe_screen for current controls and run_flow for sequences. Needs the app interaction integration. Works on the free tier.",inputSchema:{nativeTag:be.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),testID:be.string().optional().describe("Exact testID of the element."),query:be.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),value:be.union([be.number(),be.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),text:be.string().optional().describe("For a text input: the string to set via onChangeText."),longPress:be.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),scrollIntoView:be.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),includeBuoy:be.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),includeHidden:be.boolean().optional().describe("Also match elements on screens that are mounted but not showing. Default false."),observe:gf,deviceId:L}},async t=>{try{if(t.nativeTag==null&&!t.testID&&!t.query){return xi("Provide one of: nativeTag, testID, or query (from describe_screen).")}const i=n.requireProDevice(t.deviceId);const r=await n.callAction(_i,"tapElement",{nativeTag:t.nativeTag,testID:t.testID,query:t.query,value:t.value,text:t.text===void 0?void 0:Kn(t.text),longPress:t.longPress,scrollIntoView:t.scrollIntoView,includeBuoy:t.includeBuoy,includeHidden:t.includeHidden},i);zr+=1;const o=zr>=Rs&&zr%Rs===0?vf:"";if(r?.tapped&&r.matched){const l=r.matched;const d=l.text??l.label??l.name;const u=l.testID?` [testID=${l.testID}]`:l.nativeTag!=null?` #${l.nativeTag}`:"";const h=r.scrolled?" (scrolled into view)":"";const b=l.via==="onValueChange"?`Set "${d}"${u} \u2192 ${JSON.stringify(l.value)}`:l.via==="onChangeText"?`Typed ${JSON.stringify(l.value)} into "${d}"${u}`:l.via==="onLongPress"?`Long-pressed "${d}"${u}`:`Tapped "${d}"${u}`;const f=r.effect;const w=!f?"":f.commits>0?` ${f.commits} re-render${f.commits===1?"":"s"} followed.`:` No re-render followed within ${f.observedMs}ms \u2014 the handler ran but the app did not react. The element may sit on a screen beneath the current one, or be inert; do not treat this as done.`;const g=r.alsoMatched?.length?`
613
613
  Also matched equally well (the choice was a coin toss \u2014 re-tap by #nativeTag if this was wrong):
614
- `+r.alsoMatched.map(p=>`- ${p.name}${p.text?` "${p.text}"`:""}${p.testID?` [testID=${p.testID}]`:""}${p.nativeTag!=null?` #${p.nativeTag}`:""}`).join("\n"):"";const m=t.observe==="none"?"":await Re(n,i);return fi(`${b}${h}.${w}${g}${m}${o}`)}const a=!!t.query&&!t.testID&&t.nativeTag==null&&t.value===void 0&&t.text===void 0&&!t.includeHidden;const s=/equally well/.test(r?.reason??"");if(!r?.tapped&&a&&(s||ef.test(r?.reason??""))){const l=await Jp(n,i,t.query,t.longPress,s).catch(()=>null);if(l){const d=t.observe==="none"?"":await Re(n,i);return fi(`${l} It is a native control with no JS handler, so Buoy sent a real touch.${d}`)}}let c=r?.reason??"Tap failed.";if(r?.candidates?.length){c+="\n\nTappable candidates:\n"+r.candidates.map(l=>`- ${l.name}${l.text?` "${l.text}"`:""}${l.testID?` [testID=${l.testID}]`:""}${l.nativeTag!=null?` #${l.nativeTag}`:""}`).join("\n")}return mi(c)}catch(i){return mi(ks(i))}})}var Ts="route-events";function Ds(e,n=[]){for(const t of e??[]){if(t.path)n.push(t.path);if(t.children?.length)Ds(t.children,n)}return n}function Is(e,n){e.registerTool("get_routes",{description:'Read reported current navigation state and stack. includeSitemap:true includes available configured route paths; it does not establish complete application route coverage. Use get_events with sources:["route"] for retained navigation history.',inputSchema:{includeSitemap:Qn.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ts,i);const a=Array.isArray(r)?{events:r}:r??{};const s=Array.isArray(a.stack)?a.stack:[];const c=s.find(h=>h.isFocused)??s[s.length-1];const l=c?.pathname??"(unknown)";const d=[`**Current route:** \`${l}\``];if(s.length){const h=s.map((b,f)=>`${f+1}. \`${b.pathname??b.name??"?"}\`${b.isFocused?" \u2190 focused":""}`);d.push(`
614
+ `+r.alsoMatched.map(p=>`- ${p.name}${p.text?` "${p.text}"`:""}${p.testID?` [testID=${p.testID}]`:""}${p.nativeTag!=null?` #${p.nativeTag}`:""}`).join("\n"):"";const m=t.observe==="none"?"":await Pe(n,i);return Si(`${b}${h}.${w}${g}${m}${o}`)}const a=!!t.query&&!t.testID&&t.nativeTag==null&&t.value===void 0&&t.text===void 0&&!t.includeHidden;const s=/equally well/.test(r?.reason??"");if(!r?.tapped&&a&&(s||bf.test(r?.reason??""))){const l=await df(n,i,t.query,t.longPress,s).catch(()=>null);if(l){const d=t.observe==="none"?"":await Pe(n,i);return Si(`${l} It is a native control with no JS handler, so Buoy sent a real touch.${d}`)}}let c=r?.reason??"Tap failed.";if(r?.candidates?.length){c+="\n\nTappable candidates:\n"+r.candidates.map(l=>`- ${l.name}${l.text?` "${l.text}"`:""}${l.testID?` [testID=${l.testID}]`:""}${l.nativeTag!=null?` #${l.nativeTag}`:""}`).join("\n")}return xi(c)}catch(i){return xi(Cs(i))}})}var Ps="route-events";function Ns(e,n=[]){for(const t of e??[]){if(t.path)n.push(t.path);if(t.children?.length)Ns(t.children,n)}return n}function Fs(e,n){e.registerTool("get_routes",{description:'Read reported current navigation state and stack. includeSitemap:true includes available configured route paths; it does not establish complete application route coverage. Use get_events with sources:["route"] for retained navigation history.',inputSchema:{includeSitemap:li.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ps,i);const a=Array.isArray(r)?{events:r}:r??{};const s=Array.isArray(a.stack)?a.stack:[];const c=s.find(h=>h.isFocused)??s[s.length-1];const l=c?.pathname??"(unknown)";const d=[`**Current route:** \`${l}\``];if(s.length){const h=s.map((b,f)=>`${f+1}. \`${b.pathname??b.name??"?"}\`${b.isFocused?" \u2190 focused":""}`);d.push(`
615
615
  **Navigation stack** (top last):
616
- ${h.join("\n")}`)}const u=Ds(a.sitemap?.routes);if(t.includeSitemap){const h=u.slice(0,200);d.push(`
616
+ ${h.join("\n")}`)}const u=Ns(a.sitemap?.routes);if(t.includeSitemap){const h=u.slice(0,200);d.push(`
617
617
  **Sitemap (${u.length} routes${h.length<u.length?`, showing ${h.length}`:""}):**
618
618
  ${h.map(b=>`- \`${b}\``).join("\n")}`)}else{d.push(`
619
- **Sitemap:** ${u.length} routes _(pass includeSitemap:true to list them)_`)}d.push("\n_Navigate with navigate({ path })._");return M(at(o)+d.join("\n"))}catch(i){return x(j(i))}});e.registerTool("navigate",{description:"Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",inputSchema:{path:Qn.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),replace:Qn.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),observe:Qn.enum(["screen","none"]).optional().describe('What to return after navigating. Default "screen": the route it landed on and what is on screen. "none" skips it.'),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);await n.callAction(Ts,"navigate",{path:t.path,replace:t.replace===true},i);const r=t.observe==="none"?"":await Re(n,i,t.path);return M(`Navigated to \`${t.path}\`${t.replace?" (replaced current screen)":""}.${r}`)}catch(i){return x(j(i))}})}import{z as Nr}from"zod";var Ms="console";var As={verbose:0,info:1,warning:2,error:3};var rf={verbose:"\xB7",info:"info",warning:"warn",error:"ERR"};function of(e){const n=new Date(e);const t=(i,r=2)=>String(i).padStart(r,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}.${t(n.getMilliseconds(),3)}`}function Es(e,n){e.registerTool("get_console",{description:"Read retained captured console output as a compact tail of timestamps, levels and messages. Filter by minimum level or a case-insensitive message substring. Fatal JavaScript errors may be included when supported hooks captured them; post-crash availability is not guaranteed. Missing entries do not establish that no error occurred. Use get_snapshot for available full entry details. Needs configured console capture. Works on the free tier.",inputSchema:{level:Nr.enum(["verbose","info","warning","error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),limit:Nr.number().optional().describe("Most-recent N entries. Default 50."),pattern:Nr.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),deviceId:L}},async t=>{let i;let r;try{const o=n.requireProDevice(t.deviceId);({payload:i,stale:r}=await n.getSnapshot(Ms,o))}catch(o){const a=th(n,o,Ms);if(!a)return x(j(o));({payload:i,stale:r}=a)}{const o=at(r);const a=Array.isArray(i)?{entries:i}:i??{};const s=Array.isArray(a.entries)?a.entries:[];const c=t.level?As[t.level]:0;const l=t.pattern?.toLowerCase();const d=s.filter(m=>As[m.level]>=c&&(!l||(m.message??"").toLowerCase().includes(l)));const u=[...d].sort((m,p)=>m.id-p.id);const h=eh(t.limit,50);const b=u.slice(-h);if(b.length===0){return M(`${o}No console entries${t.level?` at level \u2265 ${t.level}`:""}${l?` matching "${t.pattern}"`:""} (${s.length} total captured).`)}const f=b.map(m=>`${of(m.timestamp)} [${rf[m.level]??m.level}] ${m.message}`);const w=t.level?`, level \u2265 ${t.level}`:"";const g=`**Console \u2014 ${s.length} entries (showing ${b.length}${w}):**`;return M(`${o}${g}
619
+ **Sitemap:** ${u.length} routes _(pass includeSitemap:true to list them)_`)}d.push("\n_Navigate with navigate({ path })._");return M(ct(o)+d.join("\n"))}catch(i){return x(j(i))}});e.registerTool("navigate",{description:"Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",inputSchema:{path:li.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),replace:li.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),observe:li.enum(["screen","none"]).optional().describe('What to return after navigating. Default "screen": the route it landed on and what is on screen. "none" skips it.'),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);await n.callAction(Ps,"navigate",{path:t.path,replace:t.replace===true},i);const r=t.observe==="none"?"":await Pe(n,i,t.path);return M(`Navigated to \`${t.path}\`${t.replace?" (replaced current screen)":""}.${r}`)}catch(i){return x(j(i))}})}import{z as Wr}from"zod";var Bs="console";var js={verbose:0,info:1,warning:2,error:3};var $f={verbose:"\xB7",info:"info",warning:"warn",error:"ERR"};function kf(e){const n=new Date(e);const t=(i,r=2)=>String(i).padStart(r,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}.${t(n.getMilliseconds(),3)}`}function Ls(e,n){e.registerTool("get_console",{description:"Read retained captured console output as a compact tail of timestamps, levels and messages. Filter by minimum level or a case-insensitive message substring. Fatal JavaScript errors may be included when supported hooks captured them; post-crash availability is not guaranteed. Missing entries do not establish that no error occurred. Use get_snapshot for available full entry details. Needs configured console capture. Works on the free tier.",inputSchema:{level:Wr.enum(["verbose","info","warning","error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),limit:Wr.number().optional().describe("Most-recent N entries. Default 50."),pattern:Wr.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),deviceId:L}},async t=>{let i;let r;try{const o=n.requireProDevice(t.deviceId);({payload:i,stale:r}=await n.getSnapshot(Bs,o))}catch(o){const a=bh(n,o,Bs);if(!a)return x(j(o));({payload:i,stale:r}=a)}{const o=ct(r);const a=Array.isArray(i)?{entries:i}:i??{};const s=Array.isArray(a.entries)?a.entries:[];const c=t.level?js[t.level]:0;const l=t.pattern?.toLowerCase();const d=s.filter(m=>js[m.level]>=c&&(!l||(m.message??"").toLowerCase().includes(l)));const u=[...d].sort((m,p)=>m.id-p.id);const h=gh(t.limit,50);const b=u.slice(-h);if(b.length===0){return M(`${o}No console entries${t.level?` at level \u2265 ${t.level}`:""}${l?` matching "${t.pattern}"`:""} (${s.length} total captured).`)}const f=b.map(m=>`${kf(m.timestamp)} [${$f[m.level]??m.level}] ${m.message}`);const w=t.level?`, level \u2265 ${t.level}`:"";const g=`**Console \u2014 ${s.length} entries (showing ${b.length}${w}):**`;return M(`${o}${g}
620
620
 
621
621
  \`\`\`
622
622
  ${f.join("\n")}
623
623
  \`\`\`
624
624
 
625
- _Tail of recent logs. Use get_snapshot for available console entry details._`)}})}import{McpServer as af}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as sf}from"zod";import{z as W}from"zod";var ce="network";var cf=/authorization|cookie|token|api[-_]?key|secret|session/i;function Cs(e){if(!e||typeof e!=="object")return null;return Object.fromEntries(Object.entries(e).map(([n,t])=>[n,cf.test(n)?"[redacted]":t]))}function Os(e){if(typeof e!=="string")return e;try{return JSON.parse(e)}catch{return e}}function lf(e){if(Array.isArray(e))return`array(${e.length})`;if(e&&typeof e==="object")return`{${Object.entries(e).slice(0,20).map(([n,t])=>`${n}: ${Array.isArray(t)?`array(${t.length})`:t===null?"null":typeof t}`).join(", ")}}`;return typeof e}function Rs(e,n){if(e==null)return"(none)";const t=xe(typeof e==="string"?e:JSON.stringify(e,null,1));return t.length<=n?t:`${t.slice(0,n)}
626
- \u2026 cut: showing ${n} of ${t.length} chars (pass maxChars to see more)`}var Ps=e=>{if(e?.enabled!==false)return"";return e.autoPaused?' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).':" \u2014 MASTER SWITCH IS OFF, nothing is firing"};function df(e){if(Array.isArray(e))return{events:e,saved:[]};if(e&&typeof e==="object"){const n=e;return{events:Array.isArray(n.events)?n.events:[],saved:Array.isArray(n.saved)?n.saved:[]}}return{events:[],saved:[]}}function uf(e){if(e.error)return"\u274C";if(e.status===void 0)return"\u23F3";if(e.status>=400)return"\u274C";if(e.status>=300)return"\u21AA\uFE0F";return"\u2705"}function hf(e){if(e.status!==void 0&&e.status>0)return String(e.status);if(e.error){const n=e.error.replace(/^HTTP\s+\d+:\s*/i,"").trim();if(!n||n==="undefined"||n==="no error"){return"ERR"}return`ERR ${e.error}`}if(e.status===void 0)return"pending";return String(e.status)}function pf(e){if(!e)return"";if(e<1024)return`${e}B`;if(e<1024*1024)return`${Math.round(e/1024)}KB`;return`${(e/(1024*1024)).toFixed(1)}MB`}function Ns(e,n){if(!n||n==="all")return true;const t=!!e.error||e.status!==void 0&&e.status>=400;if(n==="errors")return t;if(n==="pending")return e.status===void 0&&!e.error;if(n==="success")return!t&&e.status!==void 0;return true}function ff(e,n=600){if(e===null||e===void 0)return"(none)";const t=typeof e==="string"?e:JSON.stringify(e);return t.length>n?`${t.slice(0,n)}\u2026(+${t.length-n} chars)`:t}var Fs=8;async function Bs(e,n,t,i=Fs){const r=n.filter(c=>c.id&&(c.requestBodyOmitted===true&&c.requestData===void 0||c.responseBodyOmitted===true&&c.responseData===void 0));const o=new Map;if(r.length===0)return{bodies:o,skipped:0};const a=r.slice(0,i);const s=await Promise.allSettled(a.map(c=>e.callAction(ce,"getEventBody",{id:c.id},t)));s.forEach((c,l)=>{const d=a[l].id;const u=c.status==="fulfilled"?c.value:null;o.set(d,u?{...u}:{unresolved:true})});return{bodies:o,skipped:r.length-a.length}}async function mf(e,n){let t=null;try{t=await e.callAction(ce,"getCaptureStatus",{},n)}catch{return"Capture status is unavailable. An empty result does not establish whether requests occurred."}if(!t||t.capturing!==false){return"No requests are available in this result. Check capture setup, supported transports and hook timing before concluding that no request occurred."}return"Capture is disabled. Open the Network panel on the device or in Buoy Desktop, trigger a supported request and read again. An empty result does not establish whether requests occurred. Capture depends on installed hooks, supported transports and recording timing."}function js(e){return e>0?`
625
+ _Tail of recent logs. Use get_snapshot for available console entry details._`)}})}import{McpServer as _f}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as Sf}from"zod";import{z as W}from"zod";var ce="network";var xf=/authorization|cookie|token|api[-_]?key|secret|session/i;function qs(e){if(!e||typeof e!=="object")return null;return Object.fromEntries(Object.entries(e).map(([n,t])=>[n,xf.test(n)?"[redacted]":t]))}function Us(e){if(typeof e!=="string")return e;try{return JSON.parse(e)}catch{return e}}function Tf(e){if(Array.isArray(e))return`array(${e.length})`;if(e&&typeof e==="object")return`{${Object.entries(e).slice(0,20).map(([n,t])=>`${n}: ${Array.isArray(t)?`array(${t.length})`:t===null?"null":typeof t}`).join(", ")}}`;return typeof e}function zs(e,n){if(e==null)return"(none)";const t=Te(typeof e==="string"?e:JSON.stringify(e,null,1));return t.length<=n?t:`${t.slice(0,n)}
626
+ \u2026 cut: showing ${n} of ${t.length} chars (pass maxChars to see more)`}var Ws=e=>{if(e?.enabled!==false)return"";return e.autoPaused?' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).':" \u2014 MASTER SWITCH IS OFF, nothing is firing"};function Df(e){if(Array.isArray(e))return{events:e,saved:[]};if(e&&typeof e==="object"){const n=e;return{events:Array.isArray(n.events)?n.events:[],saved:Array.isArray(n.saved)?n.saved:[]}}return{events:[],saved:[]}}function If(e){if(e.error)return"\u274C";if(e.status===void 0)return"\u23F3";if(e.status>=400)return"\u274C";if(e.status>=300)return"\u21AA\uFE0F";return"\u2705"}function Mf(e){if(e.status!==void 0&&e.status>0)return String(e.status);if(e.error){const n=e.error.replace(/^HTTP\s+\d+:\s*/i,"").trim();if(!n||n==="undefined"||n==="no error"){return"ERR"}return`ERR ${e.error}`}if(e.status===void 0)return"pending";return String(e.status)}function Af(e){if(!e)return"";if(e<1024)return`${e}B`;if(e<1024*1024)return`${Math.round(e/1024)}KB`;return`${(e/(1024*1024)).toFixed(1)}MB`}function Hs(e,n){if(!n||n==="all")return true;const t=!!e.error||e.status!==void 0&&e.status>=400;if(n==="errors")return t;if(n==="pending")return e.status===void 0&&!e.error;if(n==="success")return!t&&e.status!==void 0;return true}function Cf(e,n=600){if(e===null||e===void 0)return"(none)";const t=typeof e==="string"?e:JSON.stringify(e);return t.length>n?`${t.slice(0,n)}\u2026(+${t.length-n} chars)`:t}var Js=8;async function Gs(e,n,t,i=Js){const r=n.filter(c=>c.id&&(c.requestBodyOmitted===true&&c.requestData===void 0||c.responseBodyOmitted===true&&c.responseData===void 0));const o=new Map;if(r.length===0)return{bodies:o,skipped:0};const a=r.slice(0,i);const s=await Promise.allSettled(a.map(c=>e.callAction(ce,"getEventBody",{id:c.id},t)));s.forEach((c,l)=>{const d=a[l].id;const u=c.status==="fulfilled"?c.value:null;o.set(d,u?{...u}:{unresolved:true})});return{bodies:o,skipped:r.length-a.length}}async function Ef(e,n){let t=null;try{t=await e.callAction(ce,"getCaptureStatus",{},n)}catch{return"Capture status is unavailable. An empty result does not establish whether requests occurred."}if(!t||t.capturing!==false){return"No requests are available in this result. Check capture setup, supported transports and hook timing before concluding that no request occurred."}return"Capture is disabled. Open the Network panel on the device or in Buoy Desktop, trigger a supported request and read again. An empty result does not establish whether requests occurred. Capture depends on installed hooks, supported transports and recording timing."}function Ks(e){return e>0?`
627
627
 
628
- _${e} more withheld ${e===1?"body was":"bodies were"} not fetched (cap ${Fs} per listing, they are large). Get one in full with its id._`:""}function Ls(e,n){const t=e.id?n.get(e.id):void 0;const i=(r,o,a)=>{const s=r??o;if(s!==void 0&&s!==null)return ff(s);if(!a)return"(none)";return t?.unresolved?"(withheld \u2014 the device did not answer getEventBody)":"(none)"};return`
628
+ _${e} more withheld ${e===1?"body was":"bodies were"} not fetched (cap ${Js} per listing, they are large). Get one in full with its id._`:""}function Vs(e,n){const t=e.id?n.get(e.id):void 0;const i=(r,o,a)=>{const s=r??o;if(s!==void 0&&s!==null)return Cf(s);if(!a)return"(none)";return t?.unresolved?"(withheld \u2014 the device did not answer getEventBody)":"(none)"};return`
629
629
  \u2514\u2500 request: ${i(e.requestData,t?.requestData,e.requestBodyOmitted)}
630
- \u2514\u2500 response: ${i(e.responseData,t?.responseData,e.responseBodyOmitted)}`}function qs(e,n,t){const i=n.path||n.url;const r=[`${n.duration!==void 0?`${Math.round(n.duration)}ms`:"\u2014"}`,pf(n.responseSize),n.host??"",n.requestClient??""].filter(Boolean);const o=n.operationName?` (${n.operationName})`:"";return`${e}. \`${n.id}\` ${uf(n)} ${hf(n)} ${n.method} ${i}${o}${t?` ${t}`:""}
631
- \u2514\u2500 ${r.join(" \xB7 ")}`}function wf(e,n){e.registerTool("network_conditions",{description:"Read or set the device's network conditions. Offline blocks new intercepted HTTP requests; slow adds 500 ms and verySlow adds 2000 ms before dispatch. Normal clears conditions. State resets on JS reload. Check app and server observations to verify the effect.",inputSchema:{action:W.enum(["get","set"]).describe("Read the effective profile or select one."),profile:W.enum(["normal","offline","slow","verySlow"]).optional().describe("Required for set."),deviceId:L}},async t=>{try{if(t.action==="set"&&!t.profile)return x("Set requires a profile.");const i=n.resolveDevice(t.deviceId);const r=await n.callAction(ce,t.action==="get"?"getNetworkConditions":"setNetworkConditions",t.action==="set"?{profile:t.profile}:void 0,i);if(!r||typeof r!=="object"||!("profile"in r))return x("The device did not report its effective network conditions.");return M(JSON.stringify(r))}catch(i){return x(j(i))}});e.registerTool("get_network_requests",{description:"List captured HTTP requests with IDs, available status/timing/size fields and pinned or saved markers. Capture depends on supported transports, installed hooks and recording timing; requests made before capture starts may be absent. Use IDs for network actions and flagged to inspect retained records. A flag does not establish who created it or the cause of a problem. Filter by status or URL pattern. includeBodies:true requests available truncated bodies, which can be missing or withheld. Needs the network integration. The list works on the free tier; includeBodies needs Buoy Pro.",inputSchema:{limit:W.number().optional().describe("Most-recent N requests. Default 25."),pattern:W.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),status:W.enum(["all","errors","success","pending"]).optional().describe("Filter by outcome. Default all."),flagged:W.enum(["pinned","saved","any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These are separate from the live list; restart retention depends on successful persistence."),includeBodies:W.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(ce,i);const{events:a,saved:s}=df(r);const c=typeof t.limit==="number"&&t.limit>0?t.limit:25;const l=t.pattern?.toLowerCase();const d=new Set;const u=new Set;for(const g of s){if(g.pinned)d.add(g.event?.id);if(g.saved)u.add(g.event?.id);if(g.pinned)d.add(g.liveId);if(g.saved)u.add(g.liveId)}if(t.flagged){const g=s.filter(v=>t.flagged==="pinned"?v.pinned:t.flagged==="saved"?v.saved:v.pinned||v.saved);const m=g.filter(v=>v.event&&Ns(v.event,t.status)).filter(v=>!l||(v.event.url??"").toLowerCase().includes(l)).slice(0,c);if(m.length===0){return M(at(o)+`No ${t.flagged==="any"?"pinned or saved":t.flagged} requests on ${i}.
630
+ \u2514\u2500 response: ${i(e.responseData,t?.responseData,e.responseBodyOmitted)}`}function Ys(e,n,t){const i=n.path||n.url;const r=[`${n.duration!==void 0?`${Math.round(n.duration)}ms`:"\u2014"}`,Af(n.responseSize),n.host??"",n.requestClient??""].filter(Boolean);const o=n.operationName?` (${n.operationName})`:"";return`${e}. \`${n.id}\` ${If(n)} ${Mf(n)} ${n.method} ${i}${o}${t?` ${t}`:""}
631
+ \u2514\u2500 ${r.join(" \xB7 ")}`}function Of(e,n){e.registerTool("network_conditions",{description:"Read or set the device's network conditions. Offline blocks new intercepted HTTP requests; slow adds 500 ms and verySlow adds 2000 ms before dispatch. Normal clears conditions. State resets on JS reload. Check app and server observations to verify the effect.",inputSchema:{action:W.enum(["get","set"]).describe("Read the effective profile or select one."),profile:W.enum(["normal","offline","slow","verySlow"]).optional().describe("Required for set."),deviceId:L}},async t=>{try{if(t.action==="set"&&!t.profile)return x("Set requires a profile.");const i=n.resolveDevice(t.deviceId);const r=await n.callAction(ce,t.action==="get"?"getNetworkConditions":"setNetworkConditions",t.action==="set"?{profile:t.profile}:void 0,i);if(!r||typeof r!=="object"||!("profile"in r))return x("The device did not report its effective network conditions.");return M(JSON.stringify(r))}catch(i){return x(j(i))}});e.registerTool("get_network_requests",{description:"List captured HTTP requests with IDs, available status/timing/size fields and pinned or saved markers. Capture depends on supported transports, installed hooks and recording timing; requests made before capture starts may be absent. Use IDs for network actions and flagged to inspect retained records. A flag does not establish who created it or the cause of a problem. Filter by status or URL pattern. includeBodies:true requests available truncated bodies, which can be missing or withheld. Needs the network integration. The list works on the free tier; includeBodies needs Buoy Pro.",inputSchema:{limit:W.number().optional().describe("Most-recent N requests. Default 25."),pattern:W.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),status:W.enum(["all","errors","success","pending"]).optional().describe("Filter by outcome. Default all."),flagged:W.enum(["pinned","saved","any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These are separate from the live list; restart retention depends on successful persistence."),includeBodies:W.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(ce,i);const{events:a,saved:s}=Df(r);const c=typeof t.limit==="number"&&t.limit>0?t.limit:25;const l=t.pattern?.toLowerCase();const d=new Set;const u=new Set;for(const g of s){if(g.pinned)d.add(g.event?.id);if(g.saved)u.add(g.event?.id);if(g.pinned)d.add(g.liveId);if(g.saved)u.add(g.liveId)}if(t.flagged){const g=s.filter(v=>t.flagged==="pinned"?v.pinned:t.flagged==="saved"?v.saved:v.pinned||v.saved);const m=g.filter(v=>v.event&&Hs(v.event,t.status)).filter(v=>!l||(v.event.url??"").toLowerCase().includes(l)).slice(0,c);if(m.length===0){return M(ct(o)+`No ${t.flagged==="any"?"pinned or saved":t.flagged} requests on ${i}.
632
632
 
633
- The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`)}const p=t.includeBodies?await Bs(n,m.map(v=>v.event),i):{bodies:new Map,skipped:0};const y=m.map((v,$)=>{const T=[v.pinned?"\u{1F4CC} pinned":"",v.saved?"\u{1F516} saved":""].filter(Boolean).join(" ");const k=v.event.id?.startsWith("saved:")?" \xB7 from an earlier app run":"";const D=v.bodyTruncated?" \xB7 body truncated":"";let E=qs($+1,v.event,T);E+=`
634
- \u2514\u2500 key \`${v.key}\`${k}${D}`;if(t.includeBodies)E+=Ls(v.event,p.bodies);return E}).join("\n");return M(at(o)+`**Kept requests on ${i}** \u2014 ${m.length} of ${g.length}
633
+ The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`)}const p=t.includeBodies?await Gs(n,m.map(v=>v.event),i):{bodies:new Map,skipped:0};const y=m.map((v,$)=>{const T=[v.pinned?"\u{1F4CC} pinned":"",v.saved?"\u{1F516} saved":""].filter(Boolean).join(" ");const k=v.event.id?.startsWith("saved:")?" \xB7 from an earlier app run":"";const D=v.bodyTruncated?" \xB7 body truncated":"";let C=Ys($+1,v.event,T);C+=`
634
+ \u2514\u2500 key \`${v.key}\`${k}${D}`;if(t.includeBodies)C+=Vs(v.event,p.bodies);return C}).join("\n");return M(ct(o)+`**Kept requests on ${i}** \u2014 ${m.length} of ${g.length}
635
635
 
636
636
  ${y}
637
637
 
638
- _These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._${js(p.skipped)}`)}const h=a.filter(g=>Ns(g,t.status)).filter(g=>!l||(g.url??"").toLowerCase().includes(l)).slice(0,c);if(h.length===0){const g=a.length===0?await mf(n,i):"No request matched the filter.";return M(at(o)+`No requests on ${i}. ${g}`)}const b=t.includeBodies?await Bs(n,h,i):{bodies:new Map,skipped:0};const f=h.map((g,m)=>{const p=[d.has(g.id)?"\u{1F4CC}":"",u.has(g.id)?"\u{1F516}":""].filter(Boolean).join("");let y=qs(m+1,g,p);if(t.includeBodies)y+=Ls(g,b.bodies);return y}).join("\n");const w=s.length>0?` \xB7 ${s.length} kept (flagged:'any' to list)`:"";return M(at(o)+`**Network requests on ${i}** \u2014 ${h.length} of ${a.length}${w}
638
+ _These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._${Ks(p.skipped)}`)}const h=a.filter(g=>Hs(g,t.status)).filter(g=>!l||(g.url??"").toLowerCase().includes(l)).slice(0,c);if(h.length===0){const g=a.length===0?await Ef(n,i):"No request matched the filter.";return M(ct(o)+`No requests on ${i}. ${g}`)}const b=t.includeBodies?await Gs(n,h,i):{bodies:new Map,skipped:0};const f=h.map((g,m)=>{const p=[d.has(g.id)?"\u{1F4CC}":"",u.has(g.id)?"\u{1F516}":""].filter(Boolean).join("");let y=Ys(m+1,g,p);if(t.includeBodies)y+=Vs(g,b.bodies);return y}).join("\n");const w=s.length>0?` \xB7 ${s.length} kept (flagged:'any' to list)`:"";return M(ct(o)+`**Network requests on ${i}** \u2014 ${h.length} of ${a.length}${w}
639
639
 
640
640
  ${f}
641
641
 
642
- _\u{1F4CC} pinned \xB7 \u{1F516} saved. Full status, headers and body for one: get_network_request({id:"${h[0].id}"}). Pin or save one: network_action({action:"pin", id:"${h[0].id}"})._${js(b.skipped)}`)}catch(i){return x(j(i))}});e.registerTool("get_network_request",{description:"Read ONE captured request by its id (from get_network_requests): method, URL, status, whether a mock produced it, request and response headers (secrets redacted) and the full bodies up to maxChars, plus a one-line shape of the response (array lengths included) so counts don't depend on reading a long body. Use this instead of re-listing requests with includeBodies when you need one response.",inputSchema:{id:W.string().describe("The request id from get_network_requests."),maxChars:W.number().optional().describe("Most body characters to show per body. Default 12000."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(ce,"getEventBody",{id:t.id},i);if(!r)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out; list again with get_network_requests.`);const o=typeof t.maxChars==="number"&&t.maxChars>0?t.maxChars:12e3;const a=Os(r.responseData);const s=[`**${r.method??"?"} ${r.url??"?"}** \u2192 ${r.status??"no status"}${r.override?" (answered by a network override, not the server)":""}`,`Response shape: ${lf(a)}`,`
642
+ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Full status, headers and body for one: get_network_request({id:"${h[0].id}"}). Pin or save one: network_action({action:"pin", id:"${h[0].id}"})._${Ks(b.skipped)}`)}catch(i){return x(j(i))}});e.registerTool("get_network_request",{description:"Read ONE captured request by its id (from get_network_requests): method, URL, status, whether a mock produced it, request and response headers (secrets redacted) and the full bodies up to maxChars, plus a one-line shape of the response (array lengths included) so counts don't depend on reading a long body. Use this instead of re-listing requests with includeBodies when you need one response.",inputSchema:{id:W.string().describe("The request id from get_network_requests."),maxChars:W.number().optional().describe("Most body characters to show per body. Default 12000."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(ce,"getEventBody",{id:t.id},i);if(!r)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out; list again with get_network_requests.`);const o=typeof t.maxChars==="number"&&t.maxChars>0?t.maxChars:12e3;const a=Us(r.responseData);const s=[`**${r.method??"?"} ${r.url??"?"}** \u2192 ${r.status??"no status"}${r.override?" (answered by a network override, not the server)":""}`,`Response shape: ${Tf(a)}`,`
643
643
  **Response body:**
644
- ${Rs(a,o)}`,`
644
+ ${zs(a,o)}`,`
645
645
  **Request body:**
646
- ${Rs(Os(r.requestData),Math.min(o,4e3))}`,`
647
- **Response headers:** ${JSON.stringify(Cs(r.responseHeaders))}`,`**Request headers:** ${JSON.stringify(Cs(r.requestHeaders))}`];return M(s.join("\n"))}catch(i){return x(j(i))}});e.registerTool("network_action",{description:"Pin or save captured requests as retained records, or remove records and clear lists. Bodies may be truncated or omitted by storage budgets. Records are kept separately from the live list; availability after restart depends on successful device persistence. Get request IDs from get_network_requests. remove uses a retained-record key from flagged results. An action acknowledgement does not confirm a durable storage write.",inputSchema:{action:W.enum(["pin","unpin","save","unsave","remove","clearPinned","clearSaved","clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),id:W.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),key:W.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=["pin","unpin","save","unsave"].includes(t.action);if(r&&!t.id){return x(`"${t.action}" needs an \`id\`. List requests with get_network_requests to get one.`)}if(t.action==="remove"&&!t.key){return x('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).')}switch(t.action){case"pin":case"unpin":{const o=await n.callAction(ce,"setPinned",{id:t.id,pinned:t.action==="pin"},i);if(o===null)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const a=typeof o==="object"&&o!==null&&o.reason==="pin-cap";if(a)return x(`Pin limit reached on ${i}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);return M(`${t.action==="pin"?"\u{1F4CC} Pinned":"Unpinned"} \`${t.id}\` on ${i}.`)}case"save":case"unsave":{const o=await n.callAction(ce,"setSaved",{id:t.id,saved:t.action==="save"},i);if(o===null)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const a=typeof o==="object"&&o!==null&&o.reason==="saved-cap";if(a)return x(`Saved-request limit reached on ${i}. Remove one first, or network_action({action:"clearSaved"}).`);return M(`${t.action==="save"?"\u{1F516} Saved":"Unsaved"} \`${t.id}\` on ${i}.`)}case"remove":await n.callAction(ce,"removeSavedRecord",{key:t.key},i);return M(`Removed kept record \`${t.key}\` on ${i}.`);case"clearPinned":await n.callAction(ce,"clearPinnedRequests",{},i);return M(`Unpinned all requests on ${i} (saved ones kept).`);case"clearSaved":await n.callAction(ce,"clearSavedRequests",{},i);return M(`Cleared saved requests on ${i} (pinned ones kept).`);case"clearRequests":await n.callAction(ce,"clearEvents",{},i);return M(`Clear request sent to ${i}. Re-list to confirm the live capture list; this action does not clear retained records.`)}}catch(i){return x(j(i))}});e.registerTool("network_override",{description:"Configure response, failure or delay rules for supported requests intercepted by the app integration. Coverage depends on transport, hooks and rule-loading timing; early startup requests may bypass a rule. Inspect existing rules before editing, trigger the intended request and check observed results. Simulated offline behavior does not disconnect the device. Remove rules created for this task afterward or restore prior settings; preserve unrelated rules. Requires Buoy Pro and network override support.",inputSchema:{action:W.enum(["list","add","remove","clear","enable","disable","debug"]).describe("add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."),urlPattern:W.string().optional().describe("For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."),fromRequestId:W.string().optional().describe("For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."),methods:W.array(W.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),kind:W.enum(["respond","fail","delay"]).optional().describe("respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."),status:W.number().optional().describe("For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."),body:W.union([W.string(),W.record(W.unknown()),W.array(W.unknown())]).optional().describe("For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."),bodyPatch:W.record(W.unknown()).optional().describe('For `add` with `fromRequestId` (or a rule `id`): fields deep-merged INTO the real captured response \u2014 the way to change one field of a big payload without pasting it back. Send only what changes, e.g. {"name":"test123"}. Ignored when `body` is given.'),statusText:W.string().optional().describe("For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."),headers:W.record(W.string()).optional().describe("For kind 'respond': response headers to send."),failKind:W.enum(["network","timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),delayMs:W.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),times:W.number().optional().describe("Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."),alternate:W.boolean().optional().describe("Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."),name:W.string().optional().describe("Label shown in the developer's UI."),enabled:W.boolean().optional().describe("For `add`: whether the rule is enabled. Default true; pass false to stage it switched off. Useful for staging a rule you'll arm later with enable({id})."),id:W.string().optional().describe("Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."),url:W.string().optional().describe("For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);switch(t.action){case"add":{if(!t.urlPattern&&!t.fromRequestId){return x("`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'.")}const r=await n.callAction(ce,"upsertOverrideRule",{fromRequestId:t.fromRequestId,rule:{id:t.id,name:t.name,urlPattern:t.urlPattern,methods:t.methods,kind:t.kind,status:t.status,statusText:t.statusText,body:t.body,bodyPatch:t.bodyPatch,headers:t.headers,failKind:t.failKind,delayMs:t.delayMs,times:t.times,alternate:t.alternate,enabled:t.enabled!==false}},i);const o=r;if(!o?.ok){return x(o?.error??`Could not add the rule on ${i}.`)}const a=o.rule?.urlPattern??t.urlPattern;const s=o.rule?.enabled===false?`
646
+ ${zs(Us(r.requestData),Math.min(o,4e3))}`,`
647
+ **Response headers:** ${JSON.stringify(qs(r.responseHeaders))}`,`**Request headers:** ${JSON.stringify(qs(r.requestHeaders))}`];return M(s.join("\n"))}catch(i){return x(j(i))}});e.registerTool("network_action",{description:"Pin or save captured requests as retained records, or remove records and clear lists. Bodies may be truncated or omitted by storage budgets. Records are kept separately from the live list; availability after restart depends on successful device persistence. Get request IDs from get_network_requests. remove uses a retained-record key from flagged results. An action acknowledgement does not confirm a durable storage write.",inputSchema:{action:W.enum(["pin","unpin","save","unsave","remove","clearPinned","clearSaved","clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),id:W.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),key:W.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=["pin","unpin","save","unsave"].includes(t.action);if(r&&!t.id){return x(`"${t.action}" needs an \`id\`. List requests with get_network_requests to get one.`)}if(t.action==="remove"&&!t.key){return x('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).')}switch(t.action){case"pin":case"unpin":{const o=await n.callAction(ce,"setPinned",{id:t.id,pinned:t.action==="pin"},i);if(o===null)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const a=typeof o==="object"&&o!==null&&o.reason==="pin-cap";if(a)return x(`Pin limit reached on ${i}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);return M(`${t.action==="pin"?"\u{1F4CC} Pinned":"Unpinned"} \`${t.id}\` on ${i}.`)}case"save":case"unsave":{const o=await n.callAction(ce,"setSaved",{id:t.id,saved:t.action==="save"},i);if(o===null)return x(`No request "${t.id}" on ${i}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const a=typeof o==="object"&&o!==null&&o.reason==="saved-cap";if(a)return x(`Saved-request limit reached on ${i}. Remove one first, or network_action({action:"clearSaved"}).`);return M(`${t.action==="save"?"\u{1F516} Saved":"Unsaved"} \`${t.id}\` on ${i}.`)}case"remove":await n.callAction(ce,"removeSavedRecord",{key:t.key},i);return M(`Removed kept record \`${t.key}\` on ${i}.`);case"clearPinned":await n.callAction(ce,"clearPinnedRequests",{},i);return M(`Unpinned all requests on ${i} (saved ones kept).`);case"clearSaved":await n.callAction(ce,"clearSavedRequests",{},i);return M(`Cleared saved requests on ${i} (pinned ones kept).`);case"clearRequests":await n.callAction(ce,"clearEvents",{},i);return M(`Clear request sent to ${i}. Re-list to confirm the live capture list; this action does not clear retained records.`)}}catch(i){return x(j(i))}});e.registerTool("network_override",{description:"Configure response, failure or delay rules for supported requests intercepted by the app integration. Coverage depends on transport, hooks and rule-loading timing; early startup requests may bypass a rule. Inspect existing rules before editing, trigger the intended request and check observed results. Simulated offline behavior does not disconnect the device. Remove rules created for this task afterward or restore prior settings; preserve unrelated rules. Requires Buoy Pro and network override support.",inputSchema:{action:W.enum(["list","add","remove","clear","enable","disable","debug"]).describe("add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."),urlPattern:W.string().optional().describe("For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."),fromRequestId:W.string().optional().describe("For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."),methods:W.array(W.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),kind:W.enum(["respond","fail","delay"]).optional().describe("respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."),status:W.number().optional().describe("For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."),body:W.union([W.string(),W.record(W.unknown()),W.array(W.unknown())]).optional().describe("For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."),bodyPatch:W.record(W.unknown()).optional().describe('For `add` with `fromRequestId` (or a rule `id`): fields deep-merged INTO the real captured response \u2014 the way to change one field of a big payload without pasting it back. Send only what changes, e.g. {"name":"test123"}. Ignored when `body` is given.'),statusText:W.string().optional().describe("For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."),headers:W.record(W.string()).optional().describe("For kind 'respond': response headers to send."),failKind:W.enum(["network","timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),delayMs:W.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),times:W.number().optional().describe("Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."),alternate:W.boolean().optional().describe("Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."),name:W.string().optional().describe("Label shown in the developer's UI."),enabled:W.boolean().optional().describe("For `add`: whether the rule is enabled. Default true; pass false to stage it switched off. Useful for staging a rule you'll arm later with enable({id})."),id:W.string().optional().describe("Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."),url:W.string().optional().describe("For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);switch(t.action){case"add":{if(!t.urlPattern&&!t.fromRequestId){return x("`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'.")}const r=await n.callAction(ce,"upsertOverrideRule",{fromRequestId:t.fromRequestId,rule:{id:t.id,name:t.name,urlPattern:t.urlPattern,methods:t.methods,kind:t.kind,status:t.status,statusText:t.statusText,body:t.body,bodyPatch:t.bodyPatch,headers:t.headers,failKind:t.failKind,delayMs:t.delayMs,times:t.times,alternate:t.alternate,enabled:t.enabled!==false}},i);const o=r;if(!o?.ok){return x(o?.error??`Could not add the rule on ${i}.`)}const a=o.rule?.urlPattern??t.urlPattern;const s=o.rule?.enabled===false?`
648
648
 
649
649
  It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${o.rule?.id}"}).`:"";return M(`Override added on ${i} \u2014 \`${o.rule?.id}\` matching \`${a}\`.${s}
650
650
 
651
- Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${o.rule?.id}"}) when you're done.`)}case"remove":{if(!t.id){return x('`remove` needs an `id`. List them with network_override({action:"list"}).')}await n.callAction(ce,"deleteOverrideRule",{id:t.id},i);return M(`Removed override \`${t.id}\` on ${i}.`)}case"clear":await n.callAction(ce,"clearOverrideRules",{},i);return M(`Removed all override rules on ${i}. Other interception or simulation settings may still apply.`);case"enable":case"disable":{const r=t.action==="enable";if(t.id){await n.callAction(ce,"setOverrideRuleEnabled",{id:t.id,enabled:r},i);return M(`Rule \`${t.id}\` was requested ${r?"ON":"OFF"} on ${i}. The other rules and the master switch are unchanged.`)}await n.callAction(ce,"setOverridesEnabled",{enabled:r},i);return M(r?`Overrides enabled in settings for ${i}.`:`Overrides switched OFF for ${i}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`)}case"list":{const r=await n.callAction(ce,"listOverrideRules",{},i);const o=r?.rules??[];if(t.id){const s=o.find(c=>c.id===t.id);if(!s){return x(`No override rule \`${t.id}\` on ${i}. List them with network_override({action:"list"}).`)}return M(`**Override rule \`${s.id}\`** on ${i}${Ps(r)}
651
+ Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${o.rule?.id}"}) when you're done.`)}case"remove":{if(!t.id){return x('`remove` needs an `id`. List them with network_override({action:"list"}).')}await n.callAction(ce,"deleteOverrideRule",{id:t.id},i);return M(`Removed override \`${t.id}\` on ${i}.`)}case"clear":await n.callAction(ce,"clearOverrideRules",{},i);return M(`Removed all override rules on ${i}. Other interception or simulation settings may still apply.`);case"enable":case"disable":{const r=t.action==="enable";if(t.id){await n.callAction(ce,"setOverrideRuleEnabled",{id:t.id,enabled:r},i);return M(`Rule \`${t.id}\` was requested ${r?"ON":"OFF"} on ${i}. The other rules and the master switch are unchanged.`)}await n.callAction(ce,"setOverridesEnabled",{enabled:r},i);return M(r?`Overrides enabled in settings for ${i}.`:`Overrides switched OFF for ${i}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`)}case"list":{const r=await n.callAction(ce,"listOverrideRules",{},i);const o=r?.rules??[];if(t.id){const s=o.find(c=>c.id===t.id);if(!s){return x(`No override rule \`${t.id}\` on ${i}. List them with network_override({action:"list"}).`)}return M(`**Override rule \`${s.id}\`** on ${i}${Ws(r)}
652
652
 
653
653
  \`\`\`json
654
654
  ${JSON.stringify(s,null,2)}
655
655
  \`\`\``)}if(o.length===0){return M(`No override rules on ${i}.
656
656
 
657
657
  Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`)}const a=o.map((s,c)=>{const l=s.kind==="respond"?`\u2192 ${s.status??200}${s.statusText?` ${s.statusText}`:""}`:s.kind==="fail"?`\u2192 ${s.failKind==="timeout"?"timeout":"network error"}`:"\u2192 real response";const d=[s.methods?.length?s.methods.join("/"):"any method",l,s.kind==="respond"&&s.body?`body ${s.body.length}B`:"",s.headers&&Object.keys(s.headers).length?`${Object.keys(s.headers).length} header(s)`:"",s.delayMs?`${s.delayMs}ms delay`:"",s.alternate?"every other request":"",typeof s.times==="number"?`${s.times}\xD7 then off`:"",`${s.hits} hit${s.hits===1?"":"s"}`,s.seen!=null&&s.seen!==s.hits?`${s.seen} matched`:"",s.enabled?"":"DISABLED"].filter(Boolean);return`${c+1}. \`${s.id}\`${s.name?` ${s.name}`:""}
658
- \u2514\u2500 \`${s.urlPattern}\` \xB7 ${d.join(" \xB7 ")}`});return M(`**Override rules on ${i}**${Ps(r)}
658
+ \u2514\u2500 \`${s.urlPattern}\` \xB7 ${d.join(" \xB7 ")}`});return M(`**Override rules on ${i}**${Ws(r)}
659
659
 
660
660
  ${a.join("\n")}
661
661
 
@@ -665,25 +665,25 @@ Full detail incl. body: network_override({action:"list", id:"${o[0]?.id}"}).`)}c
665
665
  ${JSON.stringify(r,null,2)}
666
666
  \`\`\`
667
667
 
668
- A rule that never fires is usually one of: the master switch is off, the interceptor isn't installed (nothing is subscribed to the network tool), or the pattern doesn't match the FULL url including its query string.`)}}}catch(i){return x(j(i))}})}import{z as lt}from"zod";var wi="sentry";var $t=e=>({content:[{type:"text",text:e}]});var gi=e=>({isError:true,content:[{type:"text",text:e}]});var bi=e=>e instanceof Error?e.message:String(e);function Us(e){const n=new Date(e);const t=i=>String(i).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}function zs(e){if(e<1024)return`${e}B`;if(e<1024*1024)return`${(e/1024).toFixed(1)}KB`;return`${(e/(1024*1024)).toFixed(2)}MB`}function Fr(e,n){if(n==="bytes")return zs(e);if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;if(e>=1e3)return`${(e/1e3).toFixed(1)}k`;return String(e)}var gf={errors:5e3,spans:5e6,replays:50,logBytes:5*1024**3,attachmentBytes:1*1024**3};var bf={errors:"count",spans:"count",replays:"count",logBytes:"bytes",attachmentBytes:"bytes"};function vi(e){switch(e){case"attached":return"";case"sdk-not-found":return"\n\n\u26A0\uFE0F The tool never found @sentry/react-native \u2014 nothing is being captured. The app must pass `getClient` to SentryRoot, or the SDK isn't installed.";case"no-client":return"\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";default:return"\n\n\u23F3 Still looking for the app's Sentry client."}}function vf(e,n){e.registerTool("get_sentry_envelopes",{description:"Inspect retained captured Sentry envelopes and available item types, sizes, spans and payload details. Coverage depends on supported hooks. Local capture does not prove successful delivery, ingestion or billing impact. Filter by item type to investigate the available evidence.",inputSchema:{type:lt.string().optional().describe("Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."),limit:lt.number().optional().describe("Most-recent N envelopes. Default 25."),deviceId:lt.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(wi,i);const a=o?`${o}
668
+ A rule that never fires is usually one of: the master switch is off, the interceptor isn't installed (nothing is subscribed to the network tool), or the pattern doesn't match the FULL url including its query string.`)}}}catch(i){return x(j(i))}})}import{z as ut}from"zod";var Ti="sentry";var St=e=>({content:[{type:"text",text:e}]});var Di=e=>({isError:true,content:[{type:"text",text:e}]});var Ii=e=>e instanceof Error?e.message:String(e);function Xs(e){const n=new Date(e);const t=i=>String(i).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}function Zs(e){if(e<1024)return`${e}B`;if(e<1024*1024)return`${(e/1024).toFixed(1)}KB`;return`${(e/(1024*1024)).toFixed(2)}MB`}function Hr(e,n){if(n==="bytes")return Zs(e);if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;if(e>=1e3)return`${(e/1e3).toFixed(1)}k`;return String(e)}var Rf={errors:5e3,spans:5e6,replays:50,logBytes:5*1024**3,attachmentBytes:1*1024**3};var Pf={errors:"count",spans:"count",replays:"count",logBytes:"bytes",attachmentBytes:"bytes"};function Mi(e){switch(e){case"attached":return"";case"sdk-not-found":return"\n\n\u26A0\uFE0F The tool never found @sentry/react-native \u2014 nothing is being captured. The app must pass `getClient` to SentryRoot, or the SDK isn't installed.";case"no-client":return"\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";default:return"\n\n\u23F3 Still looking for the app's Sentry client."}}function Nf(e,n){e.registerTool("get_sentry_envelopes",{description:"Inspect retained captured Sentry envelopes and available item types, sizes, spans and payload details. Coverage depends on supported hooks. Local capture does not prove successful delivery, ingestion or billing impact. Filter by item type to investigate the available evidence.",inputSchema:{type:ut.string().optional().describe("Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."),limit:ut.number().optional().describe("Most-recent N envelopes. Default 25."),deviceId:ut.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ti,i);const a=o?`${o}
669
669
 
670
- `:"";const s=r??{};const c=Array.isArray(s.envelopes)?s.envelopes:[];const l=t.type?c.filter(f=>f.items.some(w=>w.type===t.type)):c;const d=typeof t.limit==="number"&&t.limit>0?t.limit:25;const u=l.slice(-d);if(u.length===0){return $t(`${a}No Sentry envelopes${t.type?` containing a '${t.type}' item`:""} (${c.length} captured this session).${vi(s.status)}`)}const h=u.map(f=>{const w=f.items.map(y=>y.type).join("+");const g=f.items.reduce((y,v)=>y+(v.spanCount??0),0);const m=g>0?` \xB7 ${g} span${g===1?"":"s"}`:"";const p=`${f.id}. ${Us(f.timestamp)} [${w}] ${zs(f.totalBytes)}${m}`;return`${p}
671
- \u2514\u2500 ${f.items[0]?.summary??"envelope"}`});const b=`**Sentry \u2014 ${c.length} envelope${c.length===1?"":"s"} sent (showing ${u.length}${t.type?`, type '${t.type}'`:""}):**`;return $t(`${a}${b}${vi(s.status)}
670
+ `:"";const s=r??{};const c=Array.isArray(s.envelopes)?s.envelopes:[];const l=t.type?c.filter(f=>f.items.some(w=>w.type===t.type)):c;const d=typeof t.limit==="number"&&t.limit>0?t.limit:25;const u=l.slice(-d);if(u.length===0){return St(`${a}No Sentry envelopes${t.type?` containing a '${t.type}' item`:""} (${c.length} captured this session).${Mi(s.status)}`)}const h=u.map(f=>{const w=f.items.map(y=>y.type).join("+");const g=f.items.reduce((y,v)=>y+(v.spanCount??0),0);const m=g>0?` \xB7 ${g} span${g===1?"":"s"}`:"";const p=`${f.id}. ${Xs(f.timestamp)} [${w}] ${Zs(f.totalBytes)}${m}`;return`${p}
671
+ \u2514\u2500 ${f.items[0]?.summary??"envelope"}`});const b=`**Sentry \u2014 ${c.length} envelope${c.length===1?"":"s"} sent (showing ${u.length}${t.type?`, type '${t.type}'`:""}):**`;return St(`${a}${b}${Mi(s.status)}
672
672
 
673
673
  ${h.join("\n")}
674
674
 
675
- _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`)}catch(i){return gi(bi(i))}});e.registerTool("get_sentry_cost",{description:"Inspect captured session usage and the tool's configured monthly extrapolation and quota assumptions. These estimates do not establish Sentry ingestion, current plan limits or an actual bill. Check the account's current terms and usage before making cost decisions.",inputSchema:{deviceId:lt.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(wi,i);const a=o?`${o}
675
+ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`)}catch(i){return Di(Ii(i))}});e.registerTool("get_sentry_cost",{description:"Inspect captured session usage and the tool's configured monthly extrapolation and quota assumptions. These estimates do not establish Sentry ingestion, current plan limits or an actual bill. Check the account's current terms and usage before making cost decisions.",inputSchema:{deviceId:ut.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ti,i);const a=o?`${o}
676
676
 
677
- `:"";const s=r??{};const c=s.usage;if(!c?.session){return $t(`${a}No usage is available in this capture.${vi(s.status)}`)}const l=c.session;const d=c.projectedMonthly;const u=Object.keys(l);const h=u.map(g=>{const m=bf[g];const p=Fr(l[g],m);if(!d)return`- ${g}: ${p} this session`;const y=d[g];const v=gf[g];const $=v>0?Math.round(y/v*100):null;const T=$===null?" (no configured reference quota)":` \u2014 ${$}% of the configured reference quota of ${Fr(v,m)}`;return`- ${g}: ${p} this session \u2192 ~${Fr(y,m)}/mo${T}`});const b=c.windowMs?` over ${Math.round(c.windowMs/1e3)}s`:"";const f=c.topContributors?.length?`
677
+ `:"";const s=r??{};const c=s.usage;if(!c?.session){return St(`${a}No usage is available in this capture.${Mi(s.status)}`)}const l=c.session;const d=c.projectedMonthly;const u=Object.keys(l);const h=u.map(g=>{const m=Pf[g];const p=Hr(l[g],m);if(!d)return`- ${g}: ${p} this session`;const y=d[g];const v=Rf[g];const $=v>0?Math.round(y/v*100):null;const T=$===null?" (no configured reference quota)":` \u2014 ${$}% of the configured reference quota of ${Hr(v,m)}`;return`- ${g}: ${p} this session \u2192 ~${Hr(y,m)}/mo${T}`});const b=c.windowMs?` over ${Math.round(c.windowMs/1e3)}s`:"";const f=c.topContributors?.length?`
678
678
 
679
679
  **What produced it:**
680
- ${c.topContributors.map(g=>`- ${g.label} \u2014 ${g.units} ${g.category}`).join("\n")}`:"";const w=d?"\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._":"\n\n_Session too short to project a month from._";return $t(`${a}**Sentry usage estimate \u2014 ${s.envelopes?.length??0} envelopes${b}:**
680
+ ${c.topContributors.map(g=>`- ${g.label} \u2014 ${g.units} ${g.category}`).join("\n")}`:"";const w=d?"\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._":"\n\n_Session too short to project a month from._";return St(`${a}**Sentry usage estimate \u2014 ${s.envelopes?.length??0} envelopes${b}:**
681
681
 
682
- ${h.join("\n")}${f}${w}`)}catch(i){return gi(bi(i))}});e.registerTool("get_sentry_drops",{description:"Why events never reached Sentry. Answers 'I called captureException but the event isn't in Sentry' \u2014 the single most common Sentry problem \u2014 by naming the layer that discarded it: a beforeSend callback returning null, a sample-rate roll, dedupe or another event processor, React Native tracing discarding an empty transaction, or a native rate limit.",inputSchema:{limit:lt.number().optional().describe("Most-recent N drop records. Default 30."),deviceId:lt.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(wi,i);const a=o?`${o}
682
+ ${h.join("\n")}${f}${w}`)}catch(i){return Di(Ii(i))}});e.registerTool("get_sentry_drops",{description:"Why events never reached Sentry. Answers 'I called captureException but the event isn't in Sentry' \u2014 the single most common Sentry problem \u2014 by naming the layer that discarded it: a beforeSend callback returning null, a sample-rate roll, dedupe or another event processor, React Native tracing discarding an empty transaction, or a native rate limit.",inputSchema:{limit:ut.number().optional().describe("Most-recent N drop records. Default 30."),deviceId:ut.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ti,i);const a=o?`${o}
683
683
 
684
- `:"";const s=r??{};const c=s.drops?.summary??[];const l=s.drops?.recent??[];if(c.length===0){return $t(`${a}No drops are reported in the available capture.${vi(s.status)}
684
+ `:"";const s=r??{};const c=s.drops?.summary??[];const l=s.drops?.recent??[];if(c.length===0){return St(`${a}No drops are reported in the available capture.${Mi(s.status)}
685
685
 
686
- _If an event is missing from sentry.io, check capture coverage, transport errors, the DSN/project, inbound filters and quota. Local capture does not confirm delivery._`)}const d=typeof t.limit==="number"&&t.limit>0?t.limit:30;const u=l.slice(-d);const h=c.map(f=>`- ${f.reason}: ${f.units} unit${f.units===1?"":"s"} across ${f.records} record${f.records===1?"":"s"}`);const b=u.map(f=>`${Us(f.timestamp)} [${f.reason}] ${f.category}${f.quantity>1?` \xD7${f.quantity}`:""} \u2014 ${f.detail} (${f.source})`);return $t(`${a}**Sentry drops \u2014 by reason:**
686
+ _If an event is missing from sentry.io, check capture coverage, transport errors, the DSN/project, inbound filters and quota. Local capture does not confirm delivery._`)}const d=typeof t.limit==="number"&&t.limit>0?t.limit:30;const u=l.slice(-d);const h=c.map(f=>`- ${f.reason}: ${f.units} unit${f.units===1?"":"s"} across ${f.records} record${f.records===1?"":"s"}`);const b=u.map(f=>`${Xs(f.timestamp)} [${f.reason}] ${f.category}${f.quantity>1?` \xD7${f.quantity}`:""} \u2014 ${f.detail} (${f.source})`);return St(`${a}**Sentry drops \u2014 by reason:**
687
687
 
688
688
  ${h.join("\n")}
689
689
 
@@ -693,53 +693,53 @@ ${h.join("\n")}
693
693
  ${b.join("\n")}
694
694
  \`\`\`
695
695
 
696
- _"observed" means the tool watched it happen; "reported" means the SDK counted it afterwards. Native drops only appear when the SDK runs with debug enabled._`)}catch(i){return gi(bi(i))}});e.registerTool("sentry_action",{description:"Act on the Sentry inspector. 'clear' drops every captured envelope and drop record \u2014 use it to get a clean window before driving a flow you want to measure.",inputSchema:{action:lt.enum(["clear"]).describe("The action to perform."),deviceId:lt.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);await n.callAction(wi,"clearEnvelopes",{},i);return $t("Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost.")}catch(i){return gi(bi(i))}})}import{z as Qe}from"zod";var Ws="time-machine";function Hs(e){return e<1024?`${e} B`:`${(e/1024).toFixed(1)} KB`}function Js(e){const n=Object.entries(e.results).map(([i,r])=>{const o=r.skipped.slice(0,3).map(s=>`
696
+ _"observed" means the tool watched it happen; "reported" means the SDK counted it afterwards. Native drops only appear when the SDK runs with debug enabled._`)}catch(i){return Di(Ii(i))}});e.registerTool("sentry_action",{description:"Act on the Sentry inspector. 'clear' drops every captured envelope and drop record \u2014 use it to get a clean window before driving a flow you want to measure.",inputSchema:{action:ut.enum(["clear"]).describe("The action to perform."),deviceId:ut.string().optional().describe("The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out.")}},async t=>{try{const i=n.requireProDevice(t.deviceId);await n.callAction(Ti,"clearEnvelopes",{},i);return St("Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost.")}catch(i){return Di(Ii(i))}})}import{z as tt}from"zod";var Qs="time-machine";function ec(e){return e<1024?`${e} B`:`${(e/1024).toFixed(1)} KB`}function tc(e){const n=Object.entries(e.results).map(([i,r])=>{const o=r.skipped.slice(0,3).map(s=>`
697
697
  \xB7 ${s.id}: ${s.reason}`).join("");const a=r.warnings.slice(0,3).map(s=>`
698
698
  \u26A0 ${s}`).join("");return` ${r.ok?"\u2713":"\u2715"} ${i} \u2014 applied ${r.applied}${r.skipped.length?`, skipped ${r.skipped.length}`:""}${o}${a}`});const t=e.route;if(t){n.push(t.status==="navigated"?` \u2713 route \u2014 navigated to ${t.href}`:t.status==="deferred"?` \u21BB route \u2014 ${t.href} will be opened after the reload`:` \u2715 route \u2014 ${t.error??t.status}`)}return`Restored "${e.snapshotName}" (${e.mode}${e.willReload?", app reloading":""}):
699
- ${n.join("\n")}`}function yf(e,n){e.registerTool("get_time_machine",{description:"List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore supported sources to repeat the flow. Uncaptured and backend state can still differ.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Ws,i);const a=r??{};const s=a.snapshots??[];const c=a.providers??[];const l=c.map(h=>{const b=h.canCapture?h.canRestore?"capture+restore":"capture only":"unavailable";return`- ${h.label} (${h.id}): ${b}${h.detail?` \u2014 ${h.detail}`:""}`});if(a.route){l.push(a.route.available?`- Route (route): capture+restore \u2014 recorded with every snapshot; pass restoreRoute:true to return to it${a.route.current?` (now ${a.route.current})`:""}`:"- Route (route): unavailable \u2014 the app reports no routes (Expo Router, or BuoyRoutes on native), so snapshots record no route")}const d=s.map(h=>{const b=Object.values(h.sources).reduce((g,m)=>g+m.warnings.length,0);const f=h.baseline?"baseline: clear supported providers and request reload":`${Hs(h.sizeBytes)} \xB7 ${Object.keys(h.sources).join(", ")}`;const w=h.route?` \xB7 route ${h.route.href}`:"";return`- \`${h.id}\` **${h.name}** \u2014 ${new Date(h.createdAt).toISOString()} \xB7 ${f}${w}${b?` \xB7 \u26A0 ${b}`:""}`});const u=[`**Time Machine \u2014 ${s.length} snapshot(s):**`,d.length?d.join("\n"):"_none yet \u2014 capture one with time_machine_action_",`**Sources:**
699
+ ${n.join("\n")}`}function Ff(e,n){e.registerTool("get_time_machine",{description:"List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore supported sources to repeat the flow. Uncaptured and backend state can still differ.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(Qs,i);const a=r??{};const s=a.snapshots??[];const c=a.providers??[];const l=c.map(h=>{const b=h.canCapture?h.canRestore?"capture+restore":"capture only":"unavailable";return`- ${h.label} (${h.id}): ${b}${h.detail?` \u2014 ${h.detail}`:""}`});if(a.route){l.push(a.route.available?`- Route (route): capture+restore \u2014 recorded with every snapshot; pass restoreRoute:true to return to it${a.route.current?` (now ${a.route.current})`:""}`:"- Route (route): unavailable \u2014 the app reports no routes (Expo Router, or BuoyRoutes on native), so snapshots record no route")}const d=s.map(h=>{const b=Object.values(h.sources).reduce((g,m)=>g+m.warnings.length,0);const f=h.baseline?"baseline: clear supported providers and request reload":`${ec(h.sizeBytes)} \xB7 ${Object.keys(h.sources).join(", ")}`;const w=h.route?` \xB7 route ${h.route.href}`:"";return`- \`${h.id}\` **${h.name}** \u2014 ${new Date(h.createdAt).toISOString()} \xB7 ${f}${w}${b?` \xB7 \u26A0 ${b}`:""}`});const u=[`**Time Machine \u2014 ${s.length} snapshot(s):**`,d.length?d.join("\n"):"_none yet \u2014 capture one with time_machine_action_",`**Sources:**
700
700
  ${l.join("\n")||"_no providers registered_"}`];if(a.lastOutcome)u.push(`**Last restore:**
701
- ${Js(a.lastOutcome)}`);return M(at(o)+u.join("\n\n"))}catch(i){return x(j(i))}});e.registerTool("time_machine_action",{description:"Manage snapshots of supported registered state sources: capture, preview, restore, capture_baseline, delete or rename. Preview affected items before restoring. Live mode replaces supported state in place; reload mode restores supported persisted storage and requests a JS reload. Baselines clear providers that support clearing and request a reload; they do not reset backend state or reproduce a complete reinstall. restoreRoute:true requests the captured route when available. Inspect per-source applied/skipped results and reload outcomes.",inputSchema:{action:Qe.enum(["capture","capture_baseline","preview","restore","delete","rename"]).describe("What to do."),id:Qe.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),name:Qe.string().optional().describe("Snapshot name (for capture and rename)."),mode:Qe.enum(["live","reload"]).optional().describe("Restore mode. Default 'live'."),excludeKeys:Qe.array(Qe.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),restoreRoute:Qe.boolean().optional().describe("Restore only: also navigate back to the route the snapshot was captured on (shown as `route` by get_time_machine). With mode 'reload' the navigation happens automatically on the next boot. Default false \u2014 state is restored but the app stays on the current screen."),sourceIds:Qe.array(Qe.string()).optional().describe("Capture/restore only these source ids (default all)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(o,a)=>n.callAction(Ws,o,a,i);switch(t.action){case"capture":{const o=await r("capture",{name:t.name??"",sourceIds:t.sourceIds});const a=o.snapshot;const s=Object.entries(a.sources).flatMap(([c,l])=>l.warnings.map(d=>`
702
- \u26A0 ${c}: ${d}`)).join("");return M(`Captured \`${a.id}\` "${a.name}" \u2014 ${Hs(a.sizeBytes)} \xB7 ${Object.keys(a.sources).join(", ")}${s}`)}case"capture_baseline":{const o=await r("captureBaseline",{});return M(`Captured baseline \`${o.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`)}case"preview":{if(!t.id)return x("preview requires `id` (from get_time_machine).");const o=await r("preview",{id:t.id});const a=[];let s=0;for(const l of o.sources){const d={};for(const b of l.items){d[b.verdict]=(d[b.verdict]??0)+1}const u=(d.added??0)+(d.removed??0)+(d.changed??0)+(d["too-large"]??0);s+=u;const h=Object.entries(d).filter(([b])=>b!=="unchanged").map(([b,f])=>`${f} ${b}`);a.push(`- **${l.label}**: ${h.length?h.join(", "):"no changes"}${l.error?` \u2014 \u26A0 ${l.error}`:""}`);for(const b of l.items.filter(f=>f.verdict!=="unchanged").slice(0,8)){a.push(` ${b.verdict==="added"?"+":b.verdict==="removed"?"\u2212":b.verdict==="wont-apply"?"\u2298":"\u2248"} ${b.label}${b.reason?` (${b.reason})`:""}`)}for(const b of l.notes)a.push(` \xB7 ${b}`)}const c=s===0?"**Restore would change NOTHING** (states identical or nothing applicable).":`**Restore would apply ${s} change(s):**`;return M(`${c}
703
- ${a.join("\n")}`)}case"restore":{if(!t.id)return x("restore requires `id` (from get_time_machine).");const o=await r("restore",{id:t.id,mode:t.mode??"live",excludeKeys:t.excludeKeys,restoreRoute:t.restoreRoute});return M(Js(o))}case"delete":{if(!t.id)return x("delete requires `id`.");await r("delete",{id:t.id});return M(`Deleted snapshot \`${t.id}\`.`)}case"rename":{if(!t.id||!t.name)return x("rename requires `id` and `name`.");await r("rename",{id:t.id,name:t.name});return M(`Renamed \`${t.id}\` to "${t.name}".`)}}}catch(i){return x(j(i))}})}import{z as he}from"zod";var Gs="clock";function kt(e){const n=e<0?"-":"+";let t=Math.abs(Math.round(e/1e3));if(t===0)return"0s";const i=[];for(const[r,o]of[["d",86400],["h",3600],["m",60],["s",1]]){if(t>=o){i.push(`${Math.floor(t/o)}${r}`);t%=o}if(i.length===2)break}return n+i.join(" ")}function Ks(e){const n=[e.active?`**App clock: ${e.virtualIso}** (${e.mode}${e.rate!==1?`, ${e.rate}\xD7`:""}), ${kt(e.offsetMs)} from real time ${new Date(e.realNow).toISOString()}`:`**App clock: real time** (${e.virtualIso})`,`Time zone: ${e.timeZone} (the override never changes it)`,`Pending app timers: ${e.timers.timeouts} timeouts, ${e.timers.intervals} intervals`];if(e.lastJump){n.push(`Last change: ${kt(e.lastJump.byMs)}${e.lastJump.firedTimers?`, ran ${e.lastJump.firedTimers} due timer(s)`:""}`)}n.push(...$f(e));n.push(`Settings: persist=${e.settings.persist} showChip=${e.settings.showChip} fireTimersOnJump=${e.settings.fireTimersOnJump}`);return n.join("\n")}function $f(e){const n=e.tokens;if(!n)return[];if(!n.available)return["Sign-in tokens: unavailable (the app has no @buoy-gg/network)."];if(n.tokens.length===0)return["Sign-in tokens: none seen yet (they appear once the app sends a signed-in request)."];const t=["Sign-in tokens (read from captured requests; raw tokens are never shown):"];for(const r of n.tokens){const o=r.expiresAt!==void 0?r.expiresAt-e.virtualNow:void 0;const a=r.serverExpiresAt!==void 0?r.serverExpiresAt-e.realNow:void 0;const s=r.neverExpires?"never expires":o===void 0?"expiry unknown":o>0?`expires in ${kt(o).slice(1)} on the app's clock`:`expired ${kt(-o).slice(1)} ago on the app's clock`;const c=a!==void 0&&o!==void 0&&Math.abs(a-o)>2e3?a>0?`; the server accepts it for ${kt(a).slice(1)} more`:"; the server no longer accepts it":"";const l=[r.hint??r.formatLabel,r.expirySource?`expiry from ${r.expirySource}`:null,r.issuedBy?`issued by ${r.issuedBy}`:null,r.claims.sub?`sub ${r.claims.sub}`:null,`${r.uses} request(s)`,r.refreshes?`${r.refreshes} refresh(es)`:null,r.expiredSends?`SENT ${r.expiredSends}\xD7 AFTER EXPIRY`:null,r.rejected?`rejected ${r.rejected}\xD7`:null,r.looping?`REFRESH LOOP (${r.recentRefreshes} new tokens in 20s)`:null].filter(Boolean);t.push(`- [${r.id}] ${r.label} \u2192 ${r.host}: ${s}${c} (${l.join(", ")})`)}const i=n.check;if(i){const r=i.status==="refreshed"?`refreshed${i.kind==="401"&&i.forcedAt?" after the forced 401":""}${i.newIssuedBy?` via ${i.newIssuedBy}`:""}${i.newLifetimeMs?`; new token lasts ${kt(i.newLifetimeMs).slice(1)}`:""}`:i.status==="loop"?`REFRESH LOOP: ${i.newTokens} new tokens since the check started`:i.kind==="401"&&i.forcedAt===void 0?"waiting: the next request to that API will get a 401 (the app must send one)":`waiting for a new token (${kt(e.realNow-i.startedAt).slice(1)} so far)`;const o=i.staleAfterFailure?`; the app re-sent the old token ${i.staleAfterFailure}\xD7 after the 401`:"";t.push(`Refresh check (${i.kind==="401"?"forced 401":"jump to expiry"}): ${r}${o}`)}return t}function kf(e,n){e.registerTool("get_clock",{description:"Read the app's clock as the Clock tool (@buoy-gg/clock) sees it: the time the app reads now, whether it is overridden (running, frozen or sped up), how far it is from real time, and the pending app timers a jump ahead could run. Also lists the app's sign-in tokens seen in its traffic (JWT, PASETO, opaque tokens with an expiry from the login response, session cookies): time left on the app's clock and the server's, who issued them, refreshes, sends after expiry, refresh loops, and the result of a running refresh check. Check this before calling a date, countdown, expiry or sign-out a bug.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Gs,"getState",{},i);return M(Ks(r))}catch(i){return x(j(i))}});e.registerTool("clock_action",{description:"Override the app's clock. set_time moves it to `time` (ISO 8601, 'YYYY-MM-DD HH:MM' in the device's zone, epoch ms, or relative like '+30d'), optionally frozen. shift jumps by `by` ('+1d', '-2h30m', '90s'); a forward shift also runs app setTimeout/setInterval callbacks that come due inside the jump, once each. freeze/resume stop and restart it, set_rate runs it faster (60 = a minute per second), reset returns to real time. To test token refresh: jump_to_token_expiry moves the app's clock to `leadMs` (default 10000) before a token expires, for apps that refresh before expiry; fail_next_request answers the next request to that token's API with a 401 once, for apps that refresh after a 401. Both start a refresh check; read get_clock after the app sends a request to see whether a new token arrived. stop_token_check ends it and withdraws an unfired 401. Moves Date.now(), new Date() and no-argument Intl formatting; performance.now(), animations, native timers, the time zone and the backend's clock stay real. Screens that already computed a time keep it until they re-read the clock or the app reloads; the override survives reload_app.",inputSchema:{action:he.enum(["set_time","shift","freeze","resume","set_rate","reset","settings","jump_to_token_expiry","fail_next_request","stop_token_check"]).describe("What to do."),time:he.union([he.string(),he.number()]).optional().describe("set_time (required) / freeze (optional): target time."),by:he.union([he.string(),he.number()]).optional().describe("shift (required): signed duration like '+1d', '-2h30m', '90s', or ms."),freeze:he.boolean().optional().describe("set_time: stop the clock at `time`. Default false."),fireTimers:he.boolean().optional().describe("shift: run app timers that come due inside a forward jump. Default on."),rate:he.number().optional().describe("set_rate (required): speed multiplier, 0.1-86400; 1 is normal."),persist:he.boolean().optional().describe("settings: keep the override across reloads."),showChip:he.boolean().optional().describe("settings: show the floating clock chip while overridden."),fireTimersOnJump:he.boolean().optional().describe("settings: default for shift's fireTimers."),token:he.string().optional().describe("jump_to_token_expiry / fail_next_request: token id from get_clock. Default: the most recently used token."),leadMs:he.number().optional().describe("jump_to_token_expiry: land this many ms before expiry. Default 10000."),status:he.number().optional().describe("fail_next_request: 401 (default) or 403."),times:he.number().optional().describe("fail_next_request: how many requests to fail, 1-10. Default 1."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(a,s={})=>n.callAction(Gs,a,s,i);let o;switch(t.action){case"set_time":if(t.time===void 0)return x("set_time requires `time`.");o=await r("setTime",{time:t.time,freeze:t.freeze??false});break;case"shift":if(t.by===void 0)return x("shift requires `by`, e.g. '+1d'.");o=await r("shift",{by:t.by,...t.fireTimers!==void 0?{fireTimers:t.fireTimers}:{}});break;case"freeze":o=await r("freeze",t.time!==void 0?{time:t.time}:{});break;case"resume":o=await r("resume");break;case"set_rate":if(t.rate===void 0)return x("set_rate requires `rate`.");o=await r("setRate",{rate:t.rate});break;case"reset":o=await r("reset");break;case"settings":o=await r("updateSettings",{...t.persist!==void 0?{persist:t.persist}:{},...t.showChip!==void 0?{showChip:t.showChip}:{},...t.fireTimersOnJump!==void 0?{fireTimersOnJump:t.fireTimersOnJump}:{}});break;case"jump_to_token_expiry":o=await r("jumpToTokenExpiry",{...t.token?{token:t.token}:{},...t.leadMs!==void 0?{leadMs:t.leadMs}:{}});break;case"fail_next_request":o=await r("failNextRequest",{...t.token?{token:t.token}:{},...t.status!==void 0?{status:t.status}:{},...t.times!==void 0?{times:t.times}:{}});break;case"stop_token_check":o=await r("stopTokenCheck");break}return M(Ks(o))}catch(i){return x(j(i))}})}import{execFile as _f}from"child_process";import{promisify as Sf}from"util";import{z as _e}from"zod";var Br="permissions";var xf=Sf(_f);var Vs=["location","locationBackground","camera","microphone","notifications","photos","contacts","calendar","reminders","tracking","motion","bluetooth"];function Ys(e,n=12){const t=[];t.push(e.overriddenCount>0?`**${e.overriddenCount} permission(s) overridden** on ${e.platform}. The app sees the override; the OS state is unchanged.`:`**No overrides** on ${e.platform}: the app sees the real OS state.`);t.push(`Libraries found: ${e.libraries.length?e.libraries.join(", "):"none yet"}`);if(e.permissions.length===0){t.push("No permission APIs found yet. Rows appear for Expo modules in the app and for anything the app asks for.")}for(const r of e.permissions){const o=[r.override?`override **${r.override}**`:"not overridden",`system ${r.real??"unknown"}`,r.calls?`asked ${r.calls}\xD7`:"not asked yet",r.sources.length?`via ${r.sources.join(", ")}`:null,`can be: ${r.states.join(", ")}`].filter(Boolean);t.push(`- ${r.id} (${r.label}): ${o.join("; ")}`);if(r.warning)t.push(` \u26A0 ${r.warning}`)}const i=e.settings;t.push(`Settings: requestAnswer=${i.requestAnswer} notifyApp=${i.notifyApp} interceptSettings=${i.interceptSettings} enforce=${i.enforce} persist=${i.persist} showChip=${i.showChip}`);if(e.log.length){t.push("Recent calls (newest first):");for(const r of e.log.slice(0,n)){const o=new Date(r.at).toISOString().slice(11,19);const a=r.error??r.returned??"no answer";t.push(`- ${o} ${r.source}.${r.method} \u2192 ${a}${r.overridden?" (override)":""}${r.real&&r.real!==r.returned?`, system ${r.real}`:""}${r.answer?`, answered ${r.answer}`:""}`)}}if(e.host.bundleId){t.push(`Host: ${e.host.isSimulator?"simulator/emulator":e.host.isSimulator===false?"physical device":"unknown device"} "${e.host.deviceName??"?"}", app ${e.host.bundleId}`)}return t.join("\n")}var Tf={location:"location",locationBackground:"location-always",camera:"camera",microphone:"microphone",photos:"photos",contacts:"contacts",calendar:"calendar",reminders:"reminders",motion:"motion"};var Xs={location:["ACCESS_FINE_LOCATION","ACCESS_COARSE_LOCATION"],locationBackground:["ACCESS_BACKGROUND_LOCATION"],camera:["CAMERA"],microphone:["RECORD_AUDIO"],notifications:["POST_NOTIFICATIONS"],photos:["READ_MEDIA_IMAGES","READ_MEDIA_VIDEO"],contacts:["READ_CONTACTS","WRITE_CONTACTS"],calendar:["READ_CALENDAR","WRITE_CALENDAR"],motion:["ACTIVITY_RECOGNITION"],bluetooth:["BLUETOOTH_SCAN","BLUETOOTH_CONNECT"]};async function jr(e,n){const{stdout:t}=await xf(e,n,{timeout:2e4});return t}async function Df(e,n){if(n)return n;const t=JSON.parse(await jr("xcrun",["simctl","list","devices","booted","-j"]));const i=Object.values(t.devices).flat();const r=i.filter(o=>o.name===e.host.deviceName);if(r.length===1)return r[0].udid;if(!e.host.deviceName&&i.length===1)return i[0].udid;throw new Error(r.length===0?`No booted simulator is named "${e.host.deviceName??"?"}". Pass \`udid\`.`:`${r.length} booted simulators are named "${e.host.deviceName}". Pass \`udid\`.`)}async function If(e){if(e)return e;const n=await Yo();if(n.length===1)return n[0];throw new Error(n.length===0?"No Android device is connected to adb.":"Several Android devices are connected. Pass `serial`.")}async function Mf(e,n,t,i){const r=e.host.bundleId;if(!r)throw new Error("The app did not report its bundle id (needs expo-application).");if(e.platform==="ios"){if(e.host.isSimulator===false)throw new Error("A physical iPhone can't be changed from the host. Use the app's Settings page.");const c=t==="all"?"all":Tf[t];if(!c){throw new Error(`simctl has no privacy service for ${t}. Override it in the app instead (permission_action set).`)}const l=await Df(e,i.udid);await jr("xcrun",["simctl","privacy",l,n,c,r]);return`simctl privacy ${n} ${c} for ${r} on ${l}.${n==="grant"?"":" iOS ends the app's process when a permission is revoked or reset; relaunch it."}`}const o=await If(i.serial);const a=t==="all"?Object.values(Xs).flat():Xs[t];if(!a)throw new Error(`No Android permission is known for ${t}.`);const s=[];for(const c of a){const l=`android.permission.${c}`;try{const d=(...u)=>{const h=tn(o,["shell","pm",...u]);return jr(h.bin,h.args)};if(n==="grant")await d("grant",r,l);else{await d("revoke",r,l);if(n==="reset"){await d("clear-permission-flags",r,l,"user-set","user-fixed")}}s.push(c)}catch{}}if(s.length===0)throw new Error(`None of ${a.join(", ")} could be changed. Is it declared in the app's manifest?`);return`adb ${n} ${s.join(", ")} for ${r} on ${o}.${n==="grant"?"":" Android restarts the app's process when a permission is revoked."}`}function Af(e,n){e.registerTool("get_permissions",{description:"Read the app's permissions as the Permissions tool (@buoy-gg/permissions) sees them: each permission the app can reach (location, camera, notifications, photos\u2026), what the OS says, any override, which libraries asked (Expo modules, PermissionsAndroid, react-native-permissions), the states this platform allows, and the recent permission calls with what each one returned. Check this before calling a permission screen, prompt or Settings flow a bug.",inputSchema:{logLimit:_e.number().optional().describe("How many recent calls to list. Default 12."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Br,"getState",{},i);return M(Ys(r,t.logLimit??12))}catch(i){return x(j(i))}});e.registerTool("permission_action",{description:"Override what the app sees when it asks for a permission, without changing the OS. set makes `permission` read as `state` (undetermined = not asked, granted, limited = approximate location / selected photos or contacts / provisional notifications, denied, blocked = Android 'don't ask again', restricted = iOS parental controls); iOS has no blocked (a refusal is final, so denied reads as blocked) and Android has no restricted. clear returns one permission to the OS answer, reset clears all. While overridden, the real system prompt never shows: a request from undetermined (or Android denied) is answered by `answer` (ask = Buoy shows a prompt on the device and waits for a tap; allow, limited or deny answer at once) and moves the state the way the OS would. Linking.openSettings and app-settings: open Buoy's stand-in for Settings. By default a change sends the app background \u2192 active, like a return from Settings, so screens that re-check on foreground update; permission hooks that read only on mount update on their next read. With enforce on (default), calls that need access (current position, launch camera, media library reads) fail with the native module's error while refused. request_real shows the real system prompt once so the OS grants what the override pretends. The override survives reload_app.",inputSchema:{action:_e.enum(["set","clear","reset","answer","settings","request_real","return_from_settings","clear_log"]).describe("What to do."),permission:_e.string().optional().describe(`set / clear / request_real (required): ${Vs.join(", ")}, or android:<android.permission.NAME>.`),state:_e.enum(["undetermined","granted","limited","denied","blocked","restricted"]).optional().describe("set (required): the state the app sees."),answer:_e.enum(["ask","allow","limited","deny"]).optional().describe("answer (required): how a request from 'not asked' is answered while overridden."),notifyApp:_e.boolean().optional().describe("settings: send background \u2192 active after each change. Default on."),interceptSettings:_e.boolean().optional().describe("settings: stand in for Settings while overridden. Default on."),enforce:_e.boolean().optional().describe("settings: fail calls that need a refused permission. Default on."),persist:_e.boolean().optional().describe("settings: keep overrides across reloads. Default on."),showChip:_e.boolean().optional().describe("settings: show the floating chip while overridden. Default on."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(a,s={})=>n.callAction(Br,a,s,i);let o;switch(t.action){case"set":if(!t.permission||!t.state)return x("set requires `permission` and `state`.");o=await r("setOverride",{permission:t.permission,state:t.state});break;case"clear":if(!t.permission)return x("clear requires `permission`.");o=await r("clearOverride",{permission:t.permission});break;case"reset":o=await r("resetAll");break;case"answer":if(!t.answer)return x("answer requires `answer`: ask, allow, limited or deny.");o=await r("updateSettings",{requestAnswer:t.answer});break;case"settings":o=await r("updateSettings",{...t.notifyApp!==void 0?{notifyApp:t.notifyApp}:{},...t.interceptSettings!==void 0?{interceptSettings:t.interceptSettings}:{},...t.enforce!==void 0?{enforce:t.enforce}:{},...t.persist!==void 0?{persist:t.persist}:{},...t.showChip!==void 0?{showChip:t.showChip}:{}});break;case"request_real":if(!t.permission)return x("request_real requires `permission`.");o=await r("requestReal",{permission:t.permission});break;case"return_from_settings":o=await r("simulateReturnFromSettings");break;case"clear_log":o=await r("clearLog");break}return M(Ys(o))}catch(i){return x(j(i))}});e.registerTool("system_permission",{description:"Change the REAL OS permission for the connected app from this machine, for what an in-app override can't fake (native code that checks the OS itself, like reading a position or opening the camera). iOS Simulator uses `xcrun simctl privacy` (grant, revoke, reset; reset returns to 'not asked' so the real prompt shows again). There is no simctl service for notifications or tracking. Android uses `adb shell pm grant/revoke`; reset also clears 'don't ask again'. Revoking or resetting ends the app's process on both platforms, so relaunch it afterwards. Physical iPhones can't be changed this way.",inputSchema:{action:_e.enum(["grant","revoke","reset"]).describe("grant, revoke, or reset to not asked."),permission:_e.string().describe(`${Vs.join(", ")}, or all.`),udid:_e.string().optional().describe("iOS: the simulator udid, when its name is ambiguous."),serial:_e.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Br,"getState",{},i);const o=await Mf(r,t.action,t.permission,{udid:t.udid,serial:t.serial});return M(o)}catch(i){return x(j(i))}})}import{execFile as Ef}from"child_process";import{promisify as Cf}from"util";import{z as ne}from"zod";var yi="lifecycle";var Of=Cf(Ef);function kn(e){if(e==null)return"can't count";return e===0?"none":String(e)}function $i(e){const n=[];if(e.away){const i=Math.round((Date.now()-e.away.startedAt)/1e3);n.push(`**Simulated: ${e.away.kind==="background"?"in the background":"interrupted (inactive)"}** for ${i}s`+(e.away.returnAt?`, returns in ${Math.max(0,Math.round((e.away.returnAt-Date.now())/1e3))}s`:", until return")+`. Real OS state: ${e.realAppState}.`)}else{n.push(`App state: active (real OS state: ${e.realAppState}).`)}if(e.power){n.push(`Simulated battery: ${Math.round(e.power.level*100)}%, ${e.power.state}${e.power.lowPowerMode?", low power mode":""} (reached ${e.power.reached.join(", ")})`+(e.power.realGetters.length?`; still real: ${e.power.realGetters.join(", ")}`:""))}if(e.scheme)n.push(`Simulated color scheme: ${e.scheme.scheme} (${e.scheme.mode}).`);n.push(`App listeners: app state ${kn(e.listeners.appState)}, memory warning ${kn(e.listeners.memoryWarning)}, deep link ${kn(e.listeners.deepLink)}`+(e.platform==="android"?`, window focus ${kn(e.listeners.focus)}`:"")+(e.batteryLibraries.length?`, battery ${kn(e.listeners.battery)} (${e.batteryLibraries.join(", ")})`:", battery: no battery library installed")+".");if(e.report){const i=e.report;n.push(`
701
+ ${tc(a.lastOutcome)}`);return M(ct(o)+u.join("\n\n"))}catch(i){return x(j(i))}});e.registerTool("time_machine_action",{description:"Manage snapshots of supported registered state sources: capture, preview, restore, capture_baseline, delete or rename. Preview affected items before restoring. Live mode replaces supported state in place; reload mode restores supported persisted storage and requests a JS reload. Baselines clear providers that support clearing and request a reload; they do not reset backend state or reproduce a complete reinstall. restoreRoute:true requests the captured route when available. Inspect per-source applied/skipped results and reload outcomes.",inputSchema:{action:tt.enum(["capture","capture_baseline","preview","restore","delete","rename"]).describe("What to do."),id:tt.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),name:tt.string().optional().describe("Snapshot name (for capture and rename)."),mode:tt.enum(["live","reload"]).optional().describe("Restore mode. Default 'live'."),excludeKeys:tt.array(tt.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),restoreRoute:tt.boolean().optional().describe("Restore only: also navigate back to the route the snapshot was captured on (shown as `route` by get_time_machine). With mode 'reload' the navigation happens automatically on the next boot. Default false \u2014 state is restored but the app stays on the current screen."),sourceIds:tt.array(tt.string()).optional().describe("Capture/restore only these source ids (default all)."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(o,a)=>n.callAction(Qs,o,a,i);switch(t.action){case"capture":{const o=await r("capture",{name:t.name??"",sourceIds:t.sourceIds});const a=o.snapshot;const s=Object.entries(a.sources).flatMap(([c,l])=>l.warnings.map(d=>`
702
+ \u26A0 ${c}: ${d}`)).join("");return M(`Captured \`${a.id}\` "${a.name}" \u2014 ${ec(a.sizeBytes)} \xB7 ${Object.keys(a.sources).join(", ")}${s}`)}case"capture_baseline":{const o=await r("captureBaseline",{});return M(`Captured baseline \`${o.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`)}case"preview":{if(!t.id)return x("preview requires `id` (from get_time_machine).");const o=await r("preview",{id:t.id});const a=[];let s=0;for(const l of o.sources){const d={};for(const b of l.items){d[b.verdict]=(d[b.verdict]??0)+1}const u=(d.added??0)+(d.removed??0)+(d.changed??0)+(d["too-large"]??0);s+=u;const h=Object.entries(d).filter(([b])=>b!=="unchanged").map(([b,f])=>`${f} ${b}`);a.push(`- **${l.label}**: ${h.length?h.join(", "):"no changes"}${l.error?` \u2014 \u26A0 ${l.error}`:""}`);for(const b of l.items.filter(f=>f.verdict!=="unchanged").slice(0,8)){a.push(` ${b.verdict==="added"?"+":b.verdict==="removed"?"\u2212":b.verdict==="wont-apply"?"\u2298":"\u2248"} ${b.label}${b.reason?` (${b.reason})`:""}`)}for(const b of l.notes)a.push(` \xB7 ${b}`)}const c=s===0?"**Restore would change NOTHING** (states identical or nothing applicable).":`**Restore would apply ${s} change(s):**`;return M(`${c}
703
+ ${a.join("\n")}`)}case"restore":{if(!t.id)return x("restore requires `id` (from get_time_machine).");const o=await r("restore",{id:t.id,mode:t.mode??"live",excludeKeys:t.excludeKeys,restoreRoute:t.restoreRoute});return M(tc(o))}case"delete":{if(!t.id)return x("delete requires `id`.");await r("delete",{id:t.id});return M(`Deleted snapshot \`${t.id}\`.`)}case"rename":{if(!t.id||!t.name)return x("rename requires `id` and `name`.");await r("rename",{id:t.id,name:t.name});return M(`Renamed \`${t.id}\` to "${t.name}".`)}}}catch(i){return x(j(i))}})}import{z as he}from"zod";var nc="clock";function xt(e){const n=e<0?"-":"+";let t=Math.abs(Math.round(e/1e3));if(t===0)return"0s";const i=[];for(const[r,o]of[["d",86400],["h",3600],["m",60],["s",1]]){if(t>=o){i.push(`${Math.floor(t/o)}${r}`);t%=o}if(i.length===2)break}return n+i.join(" ")}function ic(e){const n=[e.active?`**App clock: ${e.virtualIso}** (${e.mode}${e.rate!==1?`, ${e.rate}\xD7`:""}), ${xt(e.offsetMs)} from real time ${new Date(e.realNow).toISOString()}`:`**App clock: real time** (${e.virtualIso})`,`Time zone: ${e.timeZone} (the override never changes it)`,`Pending app timers: ${e.timers.timeouts} timeouts, ${e.timers.intervals} intervals`];if(e.lastJump){n.push(`Last change: ${xt(e.lastJump.byMs)}${e.lastJump.firedTimers?`, ran ${e.lastJump.firedTimers} due timer(s)`:""}`)}n.push(...Bf(e));n.push(`Settings: persist=${e.settings.persist} showChip=${e.settings.showChip} fireTimersOnJump=${e.settings.fireTimersOnJump}`);return n.join("\n")}function Bf(e){const n=e.tokens;if(!n)return[];if(!n.available)return["Sign-in tokens: unavailable (the app has no @buoy-gg/network)."];if(n.tokens.length===0)return["Sign-in tokens: none seen yet (they appear once the app sends a signed-in request)."];const t=["Sign-in tokens (read from captured requests; raw tokens are never shown):"];for(const r of n.tokens){const o=r.expiresAt!==void 0?r.expiresAt-e.virtualNow:void 0;const a=r.serverExpiresAt!==void 0?r.serverExpiresAt-e.realNow:void 0;const s=r.neverExpires?"never expires":o===void 0?"expiry unknown":o>0?`expires in ${xt(o).slice(1)} on the app's clock`:`expired ${xt(-o).slice(1)} ago on the app's clock`;const c=a!==void 0&&o!==void 0&&Math.abs(a-o)>2e3?a>0?`; the server accepts it for ${xt(a).slice(1)} more`:"; the server no longer accepts it":"";const l=[r.hint??r.formatLabel,r.expirySource?`expiry from ${r.expirySource}`:null,r.issuedBy?`issued by ${r.issuedBy}`:null,r.claims.sub?`sub ${r.claims.sub}`:null,`${r.uses} request(s)`,r.refreshes?`${r.refreshes} refresh(es)`:null,r.expiredSends?`SENT ${r.expiredSends}\xD7 AFTER EXPIRY`:null,r.rejected?`rejected ${r.rejected}\xD7`:null,r.looping?`REFRESH LOOP (${r.recentRefreshes} new tokens in 20s)`:null].filter(Boolean);t.push(`- [${r.id}] ${r.label} \u2192 ${r.host}: ${s}${c} (${l.join(", ")})`)}const i=n.check;if(i){const r=i.status==="refreshed"?`refreshed${i.kind==="401"&&i.forcedAt?" after the forced 401":""}${i.newIssuedBy?` via ${i.newIssuedBy}`:""}${i.newLifetimeMs?`; new token lasts ${xt(i.newLifetimeMs).slice(1)}`:""}`:i.status==="loop"?`REFRESH LOOP: ${i.newTokens} new tokens since the check started`:i.kind==="401"&&i.forcedAt===void 0?"waiting: the next request to that API will get a 401 (the app must send one)":`waiting for a new token (${xt(e.realNow-i.startedAt).slice(1)} so far)`;const o=i.staleAfterFailure?`; the app re-sent the old token ${i.staleAfterFailure}\xD7 after the 401`:"";t.push(`Refresh check (${i.kind==="401"?"forced 401":"jump to expiry"}): ${r}${o}`)}return t}function jf(e,n){e.registerTool("get_clock",{description:"Read the app's clock as the Clock tool (@buoy-gg/clock) sees it: the time the app reads now, whether it is overridden (running, frozen or sped up), how far it is from real time, and the pending app timers a jump ahead could run. Also lists the app's sign-in tokens seen in its traffic (JWT, PASETO, opaque tokens with an expiry from the login response, session cookies): time left on the app's clock and the server's, who issued them, refreshes, sends after expiry, refresh loops, and the result of a running refresh check. Check this before calling a date, countdown, expiry or sign-out a bug.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(nc,"getState",{},i);return M(ic(r))}catch(i){return x(j(i))}});e.registerTool("clock_action",{description:"Override the app's clock. set_time moves it to `time` (ISO 8601, 'YYYY-MM-DD HH:MM' in the device's zone, epoch ms, or relative like '+30d'), optionally frozen. shift jumps by `by` ('+1d', '-2h30m', '90s'); a forward shift also runs app setTimeout/setInterval callbacks that come due inside the jump, once each. freeze/resume stop and restart it, set_rate runs it faster (60 = a minute per second), reset returns to real time. To test token refresh: jump_to_token_expiry moves the app's clock to `leadMs` (default 10000) before a token expires, for apps that refresh before expiry; fail_next_request answers the next request to that token's API with a 401 once, for apps that refresh after a 401. Both start a refresh check; read get_clock after the app sends a request to see whether a new token arrived. stop_token_check ends it and withdraws an unfired 401. Moves Date.now(), new Date() and no-argument Intl formatting; performance.now(), animations, native timers, the time zone and the backend's clock stay real. Screens that already computed a time keep it until they re-read the clock or the app reloads; the override survives reload_app.",inputSchema:{action:he.enum(["set_time","shift","freeze","resume","set_rate","reset","settings","jump_to_token_expiry","fail_next_request","stop_token_check"]).describe("What to do."),time:he.union([he.string(),he.number()]).optional().describe("set_time (required) / freeze (optional): target time."),by:he.union([he.string(),he.number()]).optional().describe("shift (required): signed duration like '+1d', '-2h30m', '90s', or ms."),freeze:he.boolean().optional().describe("set_time: stop the clock at `time`. Default false."),fireTimers:he.boolean().optional().describe("shift: run app timers that come due inside a forward jump. Default on."),rate:he.number().optional().describe("set_rate (required): speed multiplier, 0.1-86400; 1 is normal."),persist:he.boolean().optional().describe("settings: keep the override across reloads."),showChip:he.boolean().optional().describe("settings: show the floating clock chip while overridden."),fireTimersOnJump:he.boolean().optional().describe("settings: default for shift's fireTimers."),token:he.string().optional().describe("jump_to_token_expiry / fail_next_request: token id from get_clock. Default: the most recently used token."),leadMs:he.number().optional().describe("jump_to_token_expiry: land this many ms before expiry. Default 10000."),status:he.number().optional().describe("fail_next_request: 401 (default) or 403."),times:he.number().optional().describe("fail_next_request: how many requests to fail, 1-10. Default 1."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(a,s={})=>n.callAction(nc,a,s,i);let o;switch(t.action){case"set_time":if(t.time===void 0)return x("set_time requires `time`.");o=await r("setTime",{time:t.time,freeze:t.freeze??false});break;case"shift":if(t.by===void 0)return x("shift requires `by`, e.g. '+1d'.");o=await r("shift",{by:t.by,...t.fireTimers!==void 0?{fireTimers:t.fireTimers}:{}});break;case"freeze":o=await r("freeze",t.time!==void 0?{time:t.time}:{});break;case"resume":o=await r("resume");break;case"set_rate":if(t.rate===void 0)return x("set_rate requires `rate`.");o=await r("setRate",{rate:t.rate});break;case"reset":o=await r("reset");break;case"settings":o=await r("updateSettings",{...t.persist!==void 0?{persist:t.persist}:{},...t.showChip!==void 0?{showChip:t.showChip}:{},...t.fireTimersOnJump!==void 0?{fireTimersOnJump:t.fireTimersOnJump}:{}});break;case"jump_to_token_expiry":o=await r("jumpToTokenExpiry",{...t.token?{token:t.token}:{},...t.leadMs!==void 0?{leadMs:t.leadMs}:{}});break;case"fail_next_request":o=await r("failNextRequest",{...t.token?{token:t.token}:{},...t.status!==void 0?{status:t.status}:{},...t.times!==void 0?{times:t.times}:{}});break;case"stop_token_check":o=await r("stopTokenCheck");break}return M(ic(o))}catch(i){return x(j(i))}})}import{execFile as Lf}from"child_process";import{promisify as qf}from"util";import{z as Se}from"zod";var Jr="permissions";var Uf=qf(Lf);var rc=["location","locationBackground","camera","microphone","notifications","photos","contacts","calendar","reminders","tracking","motion","bluetooth"];function oc(e,n=12){const t=[];t.push(e.overriddenCount>0?`**${e.overriddenCount} permission(s) overridden** on ${e.platform}. The app sees the override; the OS state is unchanged.`:`**No overrides** on ${e.platform}: the app sees the real OS state.`);t.push(`Libraries found: ${e.libraries.length?e.libraries.join(", "):"none yet"}`);if(e.permissions.length===0){t.push("No permission APIs found yet. Rows appear for Expo modules in the app and for anything the app asks for.")}for(const r of e.permissions){const o=[r.override?`override **${r.override}**`:"not overridden",`system ${r.real??"unknown"}`,r.calls?`asked ${r.calls}\xD7`:"not asked yet",r.sources.length?`via ${r.sources.join(", ")}`:null,`can be: ${r.states.join(", ")}`].filter(Boolean);t.push(`- ${r.id} (${r.label}): ${o.join("; ")}`);if(r.warning)t.push(` \u26A0 ${r.warning}`)}const i=e.settings;t.push(`Settings: requestAnswer=${i.requestAnswer} notifyApp=${i.notifyApp} interceptSettings=${i.interceptSettings} enforce=${i.enforce} persist=${i.persist} showChip=${i.showChip}`);if(e.log.length){t.push("Recent calls (newest first):");for(const r of e.log.slice(0,n)){const o=new Date(r.at).toISOString().slice(11,19);const a=r.error??r.returned??"no answer";t.push(`- ${o} ${r.source}.${r.method} \u2192 ${a}${r.overridden?" (override)":""}${r.real&&r.real!==r.returned?`, system ${r.real}`:""}${r.answer?`, answered ${r.answer}`:""}`)}}if(e.host.bundleId){t.push(`Host: ${e.host.isSimulator?"simulator/emulator":e.host.isSimulator===false?"physical device":"unknown device"} "${e.host.deviceName??"?"}", app ${e.host.bundleId}`)}return t.join("\n")}var zf={location:"location",locationBackground:"location-always",camera:"camera",microphone:"microphone",photos:"photos",contacts:"contacts",calendar:"calendar",reminders:"reminders",motion:"motion"};var ac={location:["ACCESS_FINE_LOCATION","ACCESS_COARSE_LOCATION"],locationBackground:["ACCESS_BACKGROUND_LOCATION"],camera:["CAMERA"],microphone:["RECORD_AUDIO"],notifications:["POST_NOTIFICATIONS"],photos:["READ_MEDIA_IMAGES","READ_MEDIA_VIDEO"],contacts:["READ_CONTACTS","WRITE_CONTACTS"],calendar:["READ_CALENDAR","WRITE_CALENDAR"],motion:["ACTIVITY_RECOGNITION"],bluetooth:["BLUETOOTH_SCAN","BLUETOOTH_CONNECT"]};async function Gr(e,n){const{stdout:t}=await Uf(e,n,{timeout:2e4});return t}async function Wf(e,n){if(n)return n;const t=JSON.parse(await Gr("xcrun",["simctl","list","devices","booted","-j"]));const i=Object.values(t.devices).flat();const r=i.filter(o=>o.name===e.host.deviceName);if(r.length===1)return r[0].udid;if(!e.host.deviceName&&i.length===1)return i[0].udid;throw new Error(r.length===0?`No booted simulator is named "${e.host.deviceName??"?"}". Pass \`udid\`.`:`${r.length} booted simulators are named "${e.host.deviceName}". Pass \`udid\`.`)}async function Hf(e){if(e)return e;const n=await oa();if(n.length===1)return n[0];throw new Error(n.length===0?"No Android device is connected to adb.":"Several Android devices are connected. Pass `serial`.")}async function Jf(e,n,t,i){const r=e.host.bundleId;if(!r)throw new Error("The app did not report its bundle id (needs expo-application).");if(e.platform==="ios"){if(e.host.isSimulator===false)throw new Error("A physical iPhone can't be changed from the host. Use the app's Settings page.");const c=t==="all"?"all":zf[t];if(!c){throw new Error(`simctl has no privacy service for ${t}. Override it in the app instead (permission_action set).`)}const l=await Wf(e,i.udid);await Gr("xcrun",["simctl","privacy",l,n,c,r]);return`simctl privacy ${n} ${c} for ${r} on ${l}.${n==="grant"?"":" iOS ends the app's process when a permission is revoked or reset; relaunch it."}`}const o=await Hf(i.serial);const a=t==="all"?Object.values(ac).flat():ac[t];if(!a)throw new Error(`No Android permission is known for ${t}.`);const s=[];for(const c of a){const l=`android.permission.${c}`;try{const d=(...u)=>{const h=cn(o,["shell","pm",...u]);return Gr(h.bin,h.args)};if(n==="grant")await d("grant",r,l);else{await d("revoke",r,l);if(n==="reset"){await d("clear-permission-flags",r,l,"user-set","user-fixed")}}s.push(c)}catch{}}if(s.length===0)throw new Error(`None of ${a.join(", ")} could be changed. Is it declared in the app's manifest?`);return`adb ${n} ${s.join(", ")} for ${r} on ${o}.${n==="grant"?"":" Android restarts the app's process when a permission is revoked."}`}function Gf(e,n){e.registerTool("get_permissions",{description:"Read the app's permissions as the Permissions tool (@buoy-gg/permissions) sees them: each permission the app can reach (location, camera, notifications, photos\u2026), what the OS says, any override, which libraries asked (Expo modules, PermissionsAndroid, react-native-permissions), the states this platform allows, and the recent permission calls with what each one returned. Check this before calling a permission screen, prompt or Settings flow a bug.",inputSchema:{logLimit:Se.number().optional().describe("How many recent calls to list. Default 12."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Jr,"getState",{},i);return M(oc(r,t.logLimit??12))}catch(i){return x(j(i))}});e.registerTool("permission_action",{description:"Override what the app sees when it asks for a permission, without changing the OS. set makes `permission` read as `state` (undetermined = not asked, granted, limited = approximate location / selected photos or contacts / provisional notifications, denied, blocked = Android 'don't ask again', restricted = iOS parental controls); iOS has no blocked (a refusal is final, so denied reads as blocked) and Android has no restricted. clear returns one permission to the OS answer, reset clears all. While overridden, the real system prompt never shows: a request from undetermined (or Android denied) is answered by `answer` (ask = Buoy shows a prompt on the device and waits for a tap; allow, limited or deny answer at once) and moves the state the way the OS would. Linking.openSettings and app-settings: open Buoy's stand-in for Settings. By default a change sends the app background \u2192 active, like a return from Settings, so screens that re-check on foreground update; permission hooks that read only on mount update on their next read. With enforce on (default), calls that need access (current position, launch camera, media library reads) fail with the native module's error while refused. request_real shows the real system prompt once so the OS grants what the override pretends. The override survives reload_app.",inputSchema:{action:Se.enum(["set","clear","reset","answer","settings","request_real","return_from_settings","clear_log"]).describe("What to do."),permission:Se.string().optional().describe(`set / clear / request_real (required): ${rc.join(", ")}, or android:<android.permission.NAME>.`),state:Se.enum(["undetermined","granted","limited","denied","blocked","restricted"]).optional().describe("set (required): the state the app sees."),answer:Se.enum(["ask","allow","limited","deny"]).optional().describe("answer (required): how a request from 'not asked' is answered while overridden."),notifyApp:Se.boolean().optional().describe("settings: send background \u2192 active after each change. Default on."),interceptSettings:Se.boolean().optional().describe("settings: stand in for Settings while overridden. Default on."),enforce:Se.boolean().optional().describe("settings: fail calls that need a refused permission. Default on."),persist:Se.boolean().optional().describe("settings: keep overrides across reloads. Default on."),showChip:Se.boolean().optional().describe("settings: show the floating chip while overridden. Default on."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(a,s={})=>n.callAction(Jr,a,s,i);let o;switch(t.action){case"set":if(!t.permission||!t.state)return x("set requires `permission` and `state`.");o=await r("setOverride",{permission:t.permission,state:t.state});break;case"clear":if(!t.permission)return x("clear requires `permission`.");o=await r("clearOverride",{permission:t.permission});break;case"reset":o=await r("resetAll");break;case"answer":if(!t.answer)return x("answer requires `answer`: ask, allow, limited or deny.");o=await r("updateSettings",{requestAnswer:t.answer});break;case"settings":o=await r("updateSettings",{...t.notifyApp!==void 0?{notifyApp:t.notifyApp}:{},...t.interceptSettings!==void 0?{interceptSettings:t.interceptSettings}:{},...t.enforce!==void 0?{enforce:t.enforce}:{},...t.persist!==void 0?{persist:t.persist}:{},...t.showChip!==void 0?{showChip:t.showChip}:{}});break;case"request_real":if(!t.permission)return x("request_real requires `permission`.");o=await r("requestReal",{permission:t.permission});break;case"return_from_settings":o=await r("simulateReturnFromSettings");break;case"clear_log":o=await r("clearLog");break}return M(oc(o))}catch(i){return x(j(i))}});e.registerTool("system_permission",{description:"Change the REAL OS permission for the connected app from this machine, for what an in-app override can't fake (native code that checks the OS itself, like reading a position or opening the camera). iOS Simulator uses `xcrun simctl privacy` (grant, revoke, reset; reset returns to 'not asked' so the real prompt shows again). There is no simctl service for notifications or tracking. Android uses `adb shell pm grant/revoke`; reset also clears 'don't ask again'. Revoking or resetting ends the app's process on both platforms, so relaunch it afterwards. Physical iPhones can't be changed this way.",inputSchema:{action:Se.enum(["grant","revoke","reset"]).describe("grant, revoke, or reset to not asked."),permission:Se.string().describe(`${rc.join(", ")}, or all.`),udid:Se.string().optional().describe("iOS: the simulator udid, when its name is ambiguous."),serial:Se.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Jr,"getState",{},i);const o=await Jf(r,t.action,t.permission,{udid:t.udid,serial:t.serial});return M(o)}catch(i){return x(j(i))}})}import{execFile as Kf}from"child_process";import{promisify as Vf}from"util";import{z as ne}from"zod";var Ai="lifecycle";var Yf=Vf(Kf);function In(e){if(e==null)return"can't count";return e===0?"none":String(e)}function Ci(e){const n=[];if(e.away){const i=Math.round((Date.now()-e.away.startedAt)/1e3);n.push(`**Simulated: ${e.away.kind==="background"?"in the background":"interrupted (inactive)"}** for ${i}s`+(e.away.returnAt?`, returns in ${Math.max(0,Math.round((e.away.returnAt-Date.now())/1e3))}s`:", until return")+`. Real OS state: ${e.realAppState}.`)}else{n.push(`App state: active (real OS state: ${e.realAppState}).`)}if(e.power){n.push(`Simulated battery: ${Math.round(e.power.level*100)}%, ${e.power.state}${e.power.lowPowerMode?", low power mode":""} (reached ${e.power.reached.join(", ")})`+(e.power.realGetters.length?`; still real: ${e.power.realGetters.join(", ")}`:""))}if(e.scheme)n.push(`Simulated color scheme: ${e.scheme.scheme} (${e.scheme.mode}).`);n.push(`App listeners: app state ${In(e.listeners.appState)}, memory warning ${In(e.listeners.memoryWarning)}, deep link ${In(e.listeners.deepLink)}`+(e.platform==="android"?`, window focus ${In(e.listeners.focus)}`:"")+(e.batteryLibraries.length?`, battery ${In(e.listeners.battery)} (${e.batteryLibraries.join(", ")})`:", battery: no battery library installed")+".");if(e.report){const i=e.report;n.push(`
704
704
  **Report: ${i.label}**${i.collectingUntil!=null?" (still collecting)":""}`);if(i.unavailable)n.push(i.unavailable);if(i.finding)n.push(`Finding: ${i.finding}`);if(i.note)n.push(`Note: ${i.note}`);const r=Object.entries(i.counts).map(([o,a])=>`${a} ${o}`).join(", ");if(!i.unavailable)n.push(`Counts: ${r||"nothing happened"}`);for(const o of i.events.slice(0,25)){n.push(`- +${(o.at/1e3).toFixed(1)}s ${o.whileAway?"[away] ":""}${o.source}: ${o.title}${o.subtitle?` (${o.subtitle})`:""}${o.status==="error"?" ERROR":""}`)}}if(e.relaunch){const i=e.relaunch;n.push(i.restored==null&&i.after==null?`
705
705
  Relaunch: checking where the app landed (was on ${i.before??"unknown"}).`:`
706
706
  Relaunch: ${i.restored?`restored ${i.after}`:`landed on ${i.after??"unknown"}, was on ${i.before??"unknown"}`}${i.tookMs!=null?` (${(i.tookMs/1e3).toFixed(1)}s)`:""}.`)}if(typeof e.handled==="boolean")n.push(`Back press handled by the app: ${e.handled}.`);if(e.log.length)n.push(`
707
- Recent: ${e.log.slice(0,5).map(i=>i.text).join(" \xB7 ")}`);const t=e.recipes.filter(i=>!i.unavailable).map(i=>i.id);n.push(`Recipes: ${t.join(", ")}`);return n.join("\n")}async function Rf(e){const{stdout:n}=await Of("xcrun",["simctl",...e],{timeout:2e4});return n}async function Pf(e,n){if(e.platform==="ios"){await Rf(["ui",e.udid,"appearance",n]);return`Simulator appearance set to ${n} (affects every app on this simulator).`}if(e.platform==="ios-device")throw new Error("A physical iPhone's appearance can't be changed from the computer. Leave `real` off to simulate it.");await Be(e.serial,["shell","cmd","uimode","night",n==="dark"?"yes":"no"]);return`Android night mode set to ${n==="dark"?"on":"off"} (affects every app on this device).`}var Nf=e=>new Promise(n=>setTimeout(n,e));function Ff(e,n){e.registerTool("get_lifecycle",{description:"Read the Lifecycle tool (@buoy-gg/lifecycle): the app's simulated and real app state, how many of the APP's own listeners each lifecycle signal has (app state, memory warning, deep link, battery), the report of what the app did during and after the last simulation (requests, query refetches, route changes, store writes, with a finding such as requests made while in the background), and the relaunch check (did the app reopen where it was). Read it after lifecycle_action; reports keep collecting for a few seconds after the app returns.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(yi,"getState",{},i);return M($i(r))}catch(i){return x(j(i))}});e.registerTool("lifecycle_action",{description:"Put the app through lifecycle events and interruptions. background sends it to the background (iOS: inactive then background; Android: blur then background), interrupt makes it inactive like the app switcher or a call, return brings it back; `duration` ('5s', '31m') returns by itself, and skipWait with a duration moves the app clock instead of waiting (needs @buoy-gg/clock), which tests session timeouts in one call. memory_warning, open_url (`url`), press_back (Android; never exits the app, reports whether a screen handled it), color_scheme (`scheme`), battery (`level` 0-1, `lowPowerMode`, `state`; needs expo-battery, react-native-device-info or @capacitor/device), reset_battery, relaunch (restarts the app's JS, then checks where it landed), recipe (`id`, see get_lifecycle), clear_report, reset (end every simulation). Simulations reach the app's JS listeners only: nothing is suspended and the next real OS event wins. With `real: true` (iOS Simulator or Android), background/return/relaunch/color_scheme/open_url cause the real OS event instead (relaunch then kills the process, a real cold start). App control, iOS Simulator or Android, no lifecycle package needed: quit closes the app, launch opens it (works while it is closed), reinstall (`appPath`: a simulator .app or an .apk) wipes its data like a new user and keeps Buoy's sign-in on iOS, boot_simulator (`simulator`) starts an iOS Simulator. Read get_lifecycle afterwards for the report.",inputSchema:{action:ne.enum(["background","interrupt","return","memory_warning","open_url","press_back","color_scheme","battery","reset_battery","relaunch","recipe","clear_report","reset","settings","quit","launch","reinstall","boot_simulator"]).describe("What to do."),duration:ne.union([ne.string(),ne.number()]).optional().describe("background / interrupt: return by itself after this long ('5s', '5m', ms). Omit to stay until return."),skipWait:ne.boolean().optional().describe("background / interrupt: move the app clock forward by `duration` and return at once. Needs @buoy-gg/clock."),url:ne.string().optional().describe("open_url (required): the link, with its scheme."),scheme:ne.enum(["light","dark","system"]).optional().describe("color_scheme (required). 'system' ends the simulated scheme."),mode:ne.enum(["js","native"]).optional().describe("color_scheme: 'js' emits the change to JS listeners (default); 'native' overrides the app's native appearance too."),level:ne.number().optional().describe("battery: 0 to 1 (or a percentage above 1)."),lowPowerMode:ne.boolean().optional().describe("battery: low power mode on or off."),state:ne.enum(["unplugged","charging","full","unknown"]).optional().describe("battery: charging state."),id:ne.string().optional().describe("recipe (required): a recipe id from get_lifecycle, e.g. 'away-31m'."),showChip:ne.boolean().optional().describe("settings: show the floating status chip while simulating."),reportWindow:ne.union([ne.string(),ne.number()]).optional().describe("settings: how long reports keep collecting after the app returns ('5s')."),real:ne.boolean().optional().describe("Cause the real OS event on the iOS Simulator or Android device (background, return, relaunch, color_scheme, open_url)."),appPath:ne.string().optional().describe("reinstall (required): the built app, a simulator .app folder or an Android .apk."),keepBuoySignIn:ne.boolean().optional().describe("reinstall, iOS: keep Buoy's sign-in and device id. Default true."),simulator:ne.string().optional().describe('boot_simulator (required): simulator name or udid, e.g. "iPhone 17 Pro".'),udid:ne.string().optional().describe("The simulator udid, when it can't be found by itself."),serial:ne.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{if(t.action==="boot_simulator"){if(!t.simulator&&!t.udid)return x('boot_simulator requires `simulator` (a name like "iPhone 17 Pro" or a udid).');if(!n.listDevices().some(s=>fe(s)))return x("boot_simulator needs a Buoy Pro app seen in this session. Open the app once first.");return M(await qu(t.simulator??t.udid))}const i=["quit","launch","reinstall"];if(t.real||i.includes(t.action)){const s=t.action==="launch"||t.action==="quit";const c=["quit","relaunch","reinstall"].includes(t.action);const{id:l,host:d,online:u}=await ge(n,t.deviceId,{udid:t.udid,serial:t.serial},{allowOffline:s,changesApp:c});const h=(y,v={})=>n.callAction(yi,y,v,l);const b=u?await h("getState").catch(()=>null):null;const f=b?.host?.bundleId;if(f&&d.platform==="ios"&&!d.bundleId)d.bundleId=f;let w;switch(t.action){case"quit":w=`${await ar(d)} Buoy disconnects until it is launched again (lifecycle_action launch).`;break;case"launch":w=`${await Ot(d)} A dev build loads its bundle first; give it a few seconds to connect.`;break;case"reinstall":if(!t.appPath)return x("reinstall requires `appPath`: the built simulator .app folder or Android .apk.");w=await Bu(d,t.appPath,t.keepBuoySignIn??true);break;case"background":w=`${await Ru(d)} Use return with real: true.`;break;case"return":await Ot(d);w="Brought back to the foreground for real.";break;case"relaunch":if(u)await h("prepareRelaunch").catch(()=>void 0);await ar(d);await Ot(d);w="Killed the process and launched it again (a real cold start). Read get_lifecycle in a few seconds for where it landed; a dev build reloads its bundle first.";break;case"color_scheme":if(!t.scheme)return x("color_scheme requires `scheme`.");if(t.scheme==="system")return x("With real: true, pass light or dark; the device has no system value to go back to.");w=await Pf(d,t.scheme);break;case"open_url":if(!t.url)return x("open_url requires `url`.");w=await Pu(d,t.url);break;default:return x(`${t.action} has no real (host-side) version. iOS has no command-line memory warning; leave \`real\` off to simulate it.`)}await Nf(1200);const g=` (${Rt(d)})`;const m=["return","color_scheme","open_url"].includes(t.action);const p=m&&b?await n.callAction(yi,"getState",{},l,{timeoutMs:5e3}).catch(()=>null):null;return M(`${w}${g}${p?`
707
+ Recent: ${e.log.slice(0,5).map(i=>i.text).join(" \xB7 ")}`);const t=e.recipes.filter(i=>!i.unavailable).map(i=>i.id);n.push(`Recipes: ${t.join(", ")}`);return n.join("\n")}async function Xf(e){const{stdout:n}=await Yf("xcrun",["simctl",...e],{timeout:2e4});return n}async function Zf(e,n){if(e.platform==="ios"){await Xf(["ui",e.udid,"appearance",n]);return`Simulator appearance set to ${n} (affects every app on this simulator).`}if(e.platform==="ios-device")throw new Error("A physical iPhone's appearance can't be changed from the computer. Leave `real` off to simulate it.");await Le(e.serial,["shell","cmd","uimode","night",n==="dark"?"yes":"no"]);return`Android night mode set to ${n==="dark"?"on":"off"} (affects every app on this device).`}var Qf=e=>new Promise(n=>setTimeout(n,e));function em(e,n){e.registerTool("get_lifecycle",{description:"Read the Lifecycle tool (@buoy-gg/lifecycle): the app's simulated and real app state, how many of the APP's own listeners each lifecycle signal has (app state, memory warning, deep link, battery), the report of what the app did during and after the last simulation (requests, query refetches, route changes, store writes, with a finding such as requests made while in the background), and the relaunch check (did the app reopen where it was). Read it after lifecycle_action; reports keep collecting for a few seconds after the app returns.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Ai,"getState",{},i);return M(Ci(r))}catch(i){return x(j(i))}});e.registerTool("lifecycle_action",{description:"Put the app through lifecycle events and interruptions. background sends it to the background (iOS: inactive then background; Android: blur then background), interrupt makes it inactive like the app switcher or a call, return brings it back; `duration` ('5s', '31m') returns by itself, and skipWait with a duration moves the app clock instead of waiting (needs @buoy-gg/clock), which tests session timeouts in one call. memory_warning, open_url (`url`), press_back (Android; never exits the app, reports whether a screen handled it), color_scheme (`scheme`), battery (`level` 0-1, `lowPowerMode`, `state`; needs expo-battery, react-native-device-info or @capacitor/device), reset_battery, relaunch (restarts the app's JS, then checks where it landed), recipe (`id`, see get_lifecycle), clear_report, reset (end every simulation). Simulations reach the app's JS listeners only: nothing is suspended and the next real OS event wins. With `real: true` (iOS Simulator or Android), background/return/relaunch/color_scheme/open_url cause the real OS event instead (relaunch then kills the process, a real cold start). App control, iOS Simulator or Android, no lifecycle package needed: quit closes the app, launch opens it (works while it is closed), reinstall (`appPath`: a simulator .app or an .apk) wipes its data like a new user and keeps Buoy's sign-in on iOS, boot_simulator (`simulator`) starts an iOS Simulator. Read get_lifecycle afterwards for the report.",inputSchema:{action:ne.enum(["background","interrupt","return","memory_warning","open_url","press_back","color_scheme","battery","reset_battery","relaunch","recipe","clear_report","reset","settings","quit","launch","reinstall","boot_simulator"]).describe("What to do."),duration:ne.union([ne.string(),ne.number()]).optional().describe("background / interrupt: return by itself after this long ('5s', '5m', ms). Omit to stay until return."),skipWait:ne.boolean().optional().describe("background / interrupt: move the app clock forward by `duration` and return at once. Needs @buoy-gg/clock."),url:ne.string().optional().describe("open_url (required): the link, with its scheme."),scheme:ne.enum(["light","dark","system"]).optional().describe("color_scheme (required). 'system' ends the simulated scheme."),mode:ne.enum(["js","native"]).optional().describe("color_scheme: 'js' emits the change to JS listeners (default); 'native' overrides the app's native appearance too."),level:ne.number().optional().describe("battery: 0 to 1 (or a percentage above 1)."),lowPowerMode:ne.boolean().optional().describe("battery: low power mode on or off."),state:ne.enum(["unplugged","charging","full","unknown"]).optional().describe("battery: charging state."),id:ne.string().optional().describe("recipe (required): a recipe id from get_lifecycle, e.g. 'away-31m'."),showChip:ne.boolean().optional().describe("settings: show the floating status chip while simulating."),reportWindow:ne.union([ne.string(),ne.number()]).optional().describe("settings: how long reports keep collecting after the app returns ('5s')."),real:ne.boolean().optional().describe("Cause the real OS event on the iOS Simulator or Android device (background, return, relaunch, color_scheme, open_url)."),appPath:ne.string().optional().describe("reinstall (required): the built app, a simulator .app folder or an Android .apk."),keepBuoySignIn:ne.boolean().optional().describe("reinstall, iOS: keep Buoy's sign-in and device id. Default true."),simulator:ne.string().optional().describe('boot_simulator (required): simulator name or udid, e.g. "iPhone 17 Pro".'),udid:ne.string().optional().describe("The simulator udid, when it can't be found by itself."),serial:ne.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{if(t.action==="boot_simulator"){if(!t.simulator&&!t.udid)return x('boot_simulator requires `simulator` (a name like "iPhone 17 Pro" or a udid).');if(!n.listDevices().some(s=>fe(s)))return x("boot_simulator needs a Buoy Pro app seen in this session. Open the app once first.");return M(await rh(t.simulator??t.udid))}const i=["quit","launch","reinstall"];if(t.real||i.includes(t.action)){const s=t.action==="launch"||t.action==="quit";const c=["quit","relaunch","reinstall"].includes(t.action);const{id:l,host:d,online:u}=await ge(n,t.deviceId,{udid:t.udid,serial:t.serial},{allowOffline:s,changesApp:c});const h=(y,v={})=>n.callAction(Ai,y,v,l);const b=u?await h("getState").catch(()=>null):null;const f=b?.host?.bundleId;if(f&&d.platform==="ios"&&!d.bundleId)d.bundleId=f;let w;switch(t.action){case"quit":w=`${await fr(d)} Buoy disconnects until it is launched again (lifecycle_action launch).`;break;case"launch":w=`${await Ft(d)} A dev build loads its bundle first; give it a few seconds to connect.`;break;case"reinstall":if(!t.appPath)return x("reinstall requires `appPath`: the built simulator .app folder or Android .apk.");w=await th(d,t.appPath,t.keepBuoySignIn??true);break;case"background":w=`${await Xu(d)} Use return with real: true.`;break;case"return":await Ft(d);w="Brought back to the foreground for real.";break;case"relaunch":if(u)await h("prepareRelaunch").catch(()=>void 0);await fr(d);await Ft(d);w="Killed the process and launched it again (a real cold start). Read get_lifecycle in a few seconds for where it landed; a dev build reloads its bundle first.";break;case"color_scheme":if(!t.scheme)return x("color_scheme requires `scheme`.");if(t.scheme==="system")return x("With real: true, pass light or dark; the device has no system value to go back to.");w=await Zf(d,t.scheme);break;case"open_url":if(!t.url)return x("open_url requires `url`.");w=await Zu(d,t.url);break;default:return x(`${t.action} has no real (host-side) version. iOS has no command-line memory warning; leave \`real\` off to simulate it.`)}await Qf(1200);const g=` (${Bt(d)})`;const m=["return","color_scheme","open_url"].includes(t.action);const p=m&&b?await n.callAction(Ai,"getState",{},l,{timeoutMs:5e3}).catch(()=>null):null;return M(`${w}${g}${p?`
708
708
 
709
- ${$i(p)}`:""}`)}const r=n.requireProDevice(t.deviceId);const o=(s,c={})=>n.callAction(yi,s,c,r);let a;switch(t.action){case"background":a=await o("background",{duration:t.duration,skipWait:t.skipWait});break;case"interrupt":a=await o("interrupt",{duration:t.duration,skipWait:t.skipWait});break;case"return":a=await o("returnToApp");break;case"memory_warning":a=await o("memoryWarning");break;case"open_url":if(!t.url)return x("open_url requires `url`.");a=await o("openUrl",{url:t.url});break;case"press_back":a=await o("pressBack");break;case"color_scheme":if(!t.scheme)return x("color_scheme requires `scheme`.");a=await o("setColorScheme",{scheme:t.scheme,mode:t.mode??"js"});break;case"battery":a=await o("setPower",{...t.level!==void 0?{level:t.level}:{},...t.lowPowerMode!==void 0?{lowPowerMode:t.lowPowerMode}:{},...t.state?{state:t.state}:{}});break;case"reset_battery":a=await o("resetPower");break;case"relaunch":a=await o("relaunch");return M(`Relaunching: the app's JS restarts now. Read get_lifecycle in a few seconds for where it landed.
709
+ ${Ci(p)}`:""}`)}const r=n.requireProDevice(t.deviceId);const o=(s,c={})=>n.callAction(Ai,s,c,r);let a;switch(t.action){case"background":a=await o("background",{duration:t.duration,skipWait:t.skipWait});break;case"interrupt":a=await o("interrupt",{duration:t.duration,skipWait:t.skipWait});break;case"return":a=await o("returnToApp");break;case"memory_warning":a=await o("memoryWarning");break;case"open_url":if(!t.url)return x("open_url requires `url`.");a=await o("openUrl",{url:t.url});break;case"press_back":a=await o("pressBack");break;case"color_scheme":if(!t.scheme)return x("color_scheme requires `scheme`.");a=await o("setColorScheme",{scheme:t.scheme,mode:t.mode??"js"});break;case"battery":a=await o("setPower",{...t.level!==void 0?{level:t.level}:{},...t.lowPowerMode!==void 0?{lowPowerMode:t.lowPowerMode}:{},...t.state?{state:t.state}:{}});break;case"reset_battery":a=await o("resetPower");break;case"relaunch":a=await o("relaunch");return M(`Relaunching: the app's JS restarts now. Read get_lifecycle in a few seconds for where it landed.
710
710
 
711
- ${$i(a)}`);case"recipe":if(!t.id)return x("recipe requires `id`; get_lifecycle lists them.");a=await o("runRecipe",{id:t.id});break;case"clear_report":a=await o("clearReport");break;case"reset":a=await o("reset");break;case"settings":a=await o("updateSettings",{...t.showChip!==void 0?{showChip:t.showChip}:{},...t.reportWindow!==void 0?{reportWindow:t.reportWindow}:{}});break;default:return x(`Unknown action ${t.action}.`)}return M($i(a))}catch(i){return x(j(i))}})}import{execFile as Bf}from"child_process";import{promisify as jf}from"util";import{z as H}from"zod";var Zs=jf(Bf);var Qs="location";function Vt(e){if(e==null||!Number.isFinite(e))return"?";return e<1e3?`${Math.round(e)} m`:`${(e/1e3).toFixed(e<1e4?1:0)} km`}function Lr(e){return`${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`}function ec(e){const n=e.override;const t=[];if(!e.active){t.push("**Location: real** (no override; the app gets the device's own location)")}else{const o=e.current.fix;const a=o?Lr(o):`no fix (${e.current.reason})`;if(n.mode==="route"&&n.route&&e.progress){t.push(`**Location: route "${n.route.name??n.route.id??"custom"}"** ${n.playing?"moving":e.progress.finished?"arrived":"paused"} at ${(n.route.speed*3.6).toFixed(1)} km/h, ${Vt(e.progress.along)} of ${Vt(e.progress.length)}${n.route.loop?" (loops)":""} \u2192 ${a}`)}else if(n.mode==="fixed"){t.push(`**Location: pinned** ${n.place.label?`"${n.place.label}" `:""}\u2192 ${a}`)}else{t.push(`**Location: real position**, with overrides below`)}if(o){t.push(`Reported: \xB1${Vt(o.accuracy)}${o.heading!=null?`, heading ${Math.round(o.heading)}\xB0`:""}${o.speed!=null?`, ${o.speed.toFixed(1)} m/s`:""}${n.jitter?`, drifting up to ${Vt(n.jitter)}`:""}`)}if(!n.signal)t.push("GPS signal: OFF (updates stop, requests fail)");if(!n.services)t.push("Location services: OFF (the app is told they are disabled)")}if(e.permission){const o=e.permission;t.push(o.status==="granted"?`Permission: granted (${o.background?"always":"while in use"}, ${o.precision==="reduced"?"approximate":"precise"})`:`Permission: ${o.status.toUpperCase()} (the app's location requests fail)`)}if(e.current.precision==="reduced")t.push("Precise location is OFF in the permission: positions snap to ~3 km.");t.push(`Libraries overridden: ${e.libraries.length?e.libraries.join(", "):"none found in this app"}`);const i=e.watches.filter(o=>o.kind==="position");t.push(`Live watches: ${i.length?i.map(o=>`${o.library}#${o.id} (${o.updates} updates)`).join(", "):"none"}`);for(const o of e.tasks){if(o.kind==="geofencing"){t.push(`Geofencing task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs):`);for(const a of o.regions){t.push(`- ${a.identifier}${a.label?` (${a.label})`:""}: ${a.state}, radius ${Vt(a.radius)}${a.distance!=null?`, ${Vt(a.distance)} from center`:""} (${Lr(a)})`)}}else{t.push(`Background location task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs)`)}}const r=e.log.slice(-10);if(r.length){t.push("Recent:");for(const o of r)t.push(`- [${o.kind}] ${o.text}${o.detail?` (${o.detail})`:""}`)}if(e.places.length)t.push(`App places: ${e.places.map(o=>o.label??Lr(o)).join(", ")}`);if(e.routes.length)t.push(`App routes: ${e.routes.map(o=>o.id??o.name).join(", ")}`);t.push(`Presets: ${e.presets.map(o=>o.id).join(", ")}`);return t.join("\n")}async function Lf(){const{stdout:e}=await Zs("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const n=JSON.parse(e);return Object.values(n.devices).flat().filter(t=>t.state==="Booted").map(({udid:t,name:i})=>({udid:t,name:i}))}async function qf(e){if(process.platform!=="darwin")throw new Error("sim_location needs macOS with Xcode's iOS Simulator.");const n=await Lf();if(e){if(!n.some(t=>t.udid===e))throw new Error(`Simulator ${e} is not booted.`);return e}if(n.length===1)return n[0].udid;if(n.length===0)throw new Error("No iOS Simulator is booted.");throw new Error(`${n.length} simulators are booted; pass \`udid\`: ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}`)}function Uf(e,n){e.registerTool("get_location",{description:"Read where the app thinks it is, as the Location tool (@buoy-gg/location) sees it: real or overridden (pinned place, route with progress and speed), the position the app gets right now, GPS signal and location-services switches, which location libraries are overridden, the app's live location watches, its geofencing regions with inside/outside state and distance, background location tasks, and recent location events (requests, geofence enters and exits, errors). Check this before calling a distance, nearby list, geofence or location permission flow a bug.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(Qs,"getState",{},i);return M(ec(r))}catch(i){return x(j(i))}});e.registerTool("location_action",{description:"Override where the app thinks it is. set_location pins it to `latitude`+`longitude`, a `query` ('lat, lon' or a Google/Apple Maps link) or a `place` name from get_location. start_route moves it along `points` ([{latitude, longitude}, ...]), a `route` (a preset: walk, run, drive, highway, tunnel, loop; or an app route id), or in a straight line `to` a place, at `speed` m/s; heading and speed follow the route. pause_route / resume_route / seek_route (`meters` or `fraction`) / set_speed control it, and stop_route ends it and stays where it got to. enter_region / exit_region move just inside or outside a monitored geofence `identifier`, which runs the app's geofencing task. conditions sets the signal quality in one step: `conditions` good (\xB15 m), weak (\xB165 m, drifting up to 50 m), no-signal or off. signal (on=false) stops updates and fails requests; services (on=false) reports location services disabled. tune sets `accuracy` and `jitter` (drift) in meters. use_real returns to the real position but keeps signal/services switches; reset ends every override. Works for expo-location (including background tasks and geofencing), @react-native-community/geolocation and react-native-geolocation-service. Permission comes from the device or the Permissions tool: a denied permission still fails. Native maps' own blue dot follows the device's real location; use sim_location for that. The override survives reload_app.",inputSchema:{action:H.enum(["set_location","start_route","pause_route","resume_route","stop_route","seek_route","set_speed","enter_region","exit_region","conditions","signal","services","tune","use_real","reset","settings","clear_log"]).describe("What to do."),latitude:H.number().optional().describe("set_location: latitude, -90 to 90."),longitude:H.number().optional().describe("set_location: longitude, -180 to 180."),label:H.string().optional().describe("set_location: a name to show for the place."),query:H.string().optional().describe("set_location: 'lat, lon' text or a Google/Apple Maps link."),place:H.string().optional().describe("set_location: a place name from get_location (app places and built-ins)."),points:H.array(H.object({latitude:H.number(),longitude:H.number()})).optional().describe("start_route: two or more points to travel through."),route:H.string().optional().describe("start_route: a preset id (walk, run, drive, highway, tunnel, loop) or an app route id."),to:H.string().optional().describe("start_route: travel in a straight line to this place name or 'lat, lon'."),speed:H.number().optional().describe("start_route / set_speed: meters per second (1.4 walk, 13.4 city, 30 highway)."),heading:H.number().optional().describe("start_route with a preset: direction to head, degrees from north. tune: heading reported while pinned."),loop:H.boolean().optional().describe("start_route with points: start over after the last point."),meters:H.number().optional().describe("seek_route: meters from the route's start."),fraction:H.number().optional().describe("seek_route: 0 to 1 along the route."),identifier:H.string().optional().describe("enter_region / exit_region (required): the region's identifier from get_location."),task:H.string().optional().describe("enter_region / exit_region: the geofencing task name, when two tasks share an identifier."),conditions:H.enum(["good","weak","no-signal","off"]).optional().describe("conditions (required): good, weak, no-signal or off."),on:H.boolean().optional().describe("signal / services (required): true = working, false = off."),accuracy:H.number().optional().describe("tune: reported accuracy radius in meters."),jitter:H.number().optional().describe("tune: GPS drift in meters (0 = none)."),interval:H.number().optional().describe("tune: ms between updates to live watches (default 1000)."),persist:H.boolean().optional().describe("settings: keep the override across reloads."),showChip:H.boolean().optional().describe("settings: show the floating location chip while overridden."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(s,c={})=>n.callAction(Qs,s,c,i);const o=(...s)=>{const c={};for(const l of s)if(t[l]!==void 0)c[l]=t[l];return c};let a;switch(t.action){case"set_location":a=await r("setLocation",o("latitude","longitude","label","query","place"));break;case"start_route":a=await r("startRoute",o("points","route","to","speed","heading","loop"));break;case"pause_route":a=await r("pauseRoute");break;case"resume_route":a=await r("resumeRoute");break;case"stop_route":a=await r("stopRoute");break;case"seek_route":a=await r("seekRoute",o("meters","fraction"));break;case"set_speed":if(t.speed===void 0)return x("set_speed requires `speed` in m/s.");a=await r("setSpeed",{speed:t.speed});break;case"enter_region":case"exit_region":if(!t.identifier)return x(`${t.action} requires \`identifier\`; get_location lists the regions.`);a=await r(t.action==="enter_region"?"enterRegion":"exitRegion",o("identifier","task"));break;case"conditions":if(!t.conditions)return x("conditions requires `conditions`: good, weak, no-signal or off.");a=await r("setConditions",{conditions:t.conditions});break;case"signal":if(t.on===void 0)return x("signal requires `on`.");a=await r("setSignal",{on:t.on});break;case"services":if(t.on===void 0)return x("services requires `on`.");a=await r("setServices",{on:t.on});break;case"tune":a=await r("tune",o("accuracy","jitter","heading","interval"));break;case"use_real":a=await r("useRealLocation");break;case"reset":a=await r("reset");break;case"settings":a=await r("updateSettings",o("persist","showChip"));break;case"clear_log":a=await r("clearLog");break}return M(ec(a))}catch(i){return x(j(i))}});e.registerTool("sim_location",{description:"Set the iOS Simulator's own location with `xcrun simctl location` (runs on this Mac, not in the app). Unlike location_action, this reaches native code: map views' blue dot (react-native-maps, expo-maps, Mapbox), native geofence monitoring, other SDKs. set pins `latitude`+`longitude`; route moves through `points` at `speed` m/s (updates every `interval` s); clear stops it. Needs no Buoy in the app. The in-app Location tool, when on, still decides what the app's JS location calls return. Pass `udid` when more than one simulator is booted.",inputSchema:{action:H.enum(["set","route","clear"]).describe("What to do."),latitude:H.number().optional().describe("set: latitude."),longitude:H.number().optional().describe("set: longitude."),points:H.array(H.object({latitude:H.number(),longitude:H.number()})).optional().describe("route: two or more waypoints."),speed:H.number().optional().describe("route: meters per second. Default 20."),interval:H.number().optional().describe("route: seconds between updates. Default 1."),udid:H.string().optional().describe("The simulator's UDID. Required when several are booted.")}},async t=>{try{const i=await qf(t.udid);const r=(...o)=>Zs("xcrun",["simctl","location",i,...o],{timeout:15e3});switch(t.action){case"set":if(t.latitude===void 0||t.longitude===void 0)return x("set requires `latitude` and `longitude`.");await r("set",`${t.latitude},${t.longitude}`);return M(`Simulator ${i} location set to ${t.latitude}, ${t.longitude}.`);case"route":{const o=t.points??[];if(o.length<2)return x("route requires two or more `points`.");const a=[`--speed=${t.speed??20}`,`--interval=${t.interval??1}`];await r("start",...a,...o.map(s=>`${s.latitude},${s.longitude}`));return M(`Simulator ${i} is moving through ${o.length} points at ${t.speed??20} m/s.`)}case"clear":await r("clear");return M(`Simulator ${i} location cleared.`)}}catch(i){return x(j(i))}})}import{z as U}from"zod";var tc="scenarios";var _t=e=>({content:[{type:"text",text:e}]});var Le=e=>({isError:true,content:[{type:"text",text:e}]});var nc=e=>e instanceof Error?e.message:String(e);function zf(e){const n=e.options?.length?` one of ${e.options.join("|")}`:"";const t=e.default!==void 0?` (default ${String(e.default)})`:"";return`${e.key}: ${e.type}${n}${t}`}function ki(e){const n=e.vars?.length?`
712
- variables: ${e.vars.map(zf).join("; ")}`:"";const t=e.stepLines?.length?`
711
+ ${Ci(a)}`);case"recipe":if(!t.id)return x("recipe requires `id`; get_lifecycle lists them.");a=await o("runRecipe",{id:t.id});break;case"clear_report":a=await o("clearReport");break;case"reset":a=await o("reset");break;case"settings":a=await o("updateSettings",{...t.showChip!==void 0?{showChip:t.showChip}:{},...t.reportWindow!==void 0?{reportWindow:t.reportWindow}:{}});break;default:return x(`Unknown action ${t.action}.`)}return M(Ci(a))}catch(i){return x(j(i))}})}import{execFile as tm}from"child_process";import{promisify as nm}from"util";import{z as H}from"zod";var sc=nm(tm);var cc="location";function Qt(e){if(e==null||!Number.isFinite(e))return"?";return e<1e3?`${Math.round(e)} m`:`${(e/1e3).toFixed(e<1e4?1:0)} km`}function Kr(e){return`${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`}function lc(e){const n=e.override;const t=[];if(!e.active){t.push("**Location: real** (no override; the app gets the device's own location)")}else{const o=e.current.fix;const a=o?Kr(o):`no fix (${e.current.reason})`;if(n.mode==="route"&&n.route&&e.progress){t.push(`**Location: route "${n.route.name??n.route.id??"custom"}"** ${n.playing?"moving":e.progress.finished?"arrived":"paused"} at ${(n.route.speed*3.6).toFixed(1)} km/h, ${Qt(e.progress.along)} of ${Qt(e.progress.length)}${n.route.loop?" (loops)":""} \u2192 ${a}`)}else if(n.mode==="fixed"){t.push(`**Location: pinned** ${n.place.label?`"${n.place.label}" `:""}\u2192 ${a}`)}else{t.push(`**Location: real position**, with overrides below`)}if(o){t.push(`Reported: \xB1${Qt(o.accuracy)}${o.heading!=null?`, heading ${Math.round(o.heading)}\xB0`:""}${o.speed!=null?`, ${o.speed.toFixed(1)} m/s`:""}${n.jitter?`, drifting up to ${Qt(n.jitter)}`:""}`)}if(!n.signal)t.push("GPS signal: OFF (updates stop, requests fail)");if(!n.services)t.push("Location services: OFF (the app is told they are disabled)")}if(e.permission){const o=e.permission;t.push(o.status==="granted"?`Permission: granted (${o.background?"always":"while in use"}, ${o.precision==="reduced"?"approximate":"precise"})`:`Permission: ${o.status.toUpperCase()} (the app's location requests fail)`)}if(e.current.precision==="reduced")t.push("Precise location is OFF in the permission: positions snap to ~3 km.");t.push(`Libraries overridden: ${e.libraries.length?e.libraries.join(", "):"none found in this app"}`);const i=e.watches.filter(o=>o.kind==="position");t.push(`Live watches: ${i.length?i.map(o=>`${o.library}#${o.id} (${o.updates} updates)`).join(", "):"none"}`);for(const o of e.tasks){if(o.kind==="geofencing"){t.push(`Geofencing task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs):`);for(const a of o.regions){t.push(`- ${a.identifier}${a.label?` (${a.label})`:""}: ${a.state}, radius ${Qt(a.radius)}${a.distance!=null?`, ${Qt(a.distance)} from center`:""} (${Kr(a)})`)}}else{t.push(`Background location task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs)`)}}const r=e.log.slice(-10);if(r.length){t.push("Recent:");for(const o of r)t.push(`- [${o.kind}] ${o.text}${o.detail?` (${o.detail})`:""}`)}if(e.places.length)t.push(`App places: ${e.places.map(o=>o.label??Kr(o)).join(", ")}`);if(e.routes.length)t.push(`App routes: ${e.routes.map(o=>o.id??o.name).join(", ")}`);t.push(`Presets: ${e.presets.map(o=>o.id).join(", ")}`);return t.join("\n")}async function im(){const{stdout:e}=await sc("xcrun",["simctl","list","devices","booted","-j"],{timeout:1e4});const n=JSON.parse(e);return Object.values(n.devices).flat().filter(t=>t.state==="Booted").map(({udid:t,name:i})=>({udid:t,name:i}))}async function rm(e){if(process.platform!=="darwin")throw new Error("sim_location needs macOS with Xcode's iOS Simulator.");const n=await im();if(e){if(!n.some(t=>t.udid===e))throw new Error(`Simulator ${e} is not booted.`);return e}if(n.length===1)return n[0].udid;if(n.length===0)throw new Error("No iOS Simulator is booted.");throw new Error(`${n.length} simulators are booted; pass \`udid\`: ${n.map(t=>`${t.name} (${t.udid})`).join(", ")}`)}function om(e,n){e.registerTool("get_location",{description:"Read where the app thinks it is, as the Location tool (@buoy-gg/location) sees it: real or overridden (pinned place, route with progress and speed), the position the app gets right now, GPS signal and location-services switches, which location libraries are overridden, the app's live location watches, its geofencing regions with inside/outside state and distance, background location tasks, and recent location events (requests, geofence enters and exits, errors). Check this before calling a distance, nearby list, geofence or location permission flow a bug.",inputSchema:{deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=await n.callAction(cc,"getState",{},i);return M(lc(r))}catch(i){return x(j(i))}});e.registerTool("location_action",{description:"Override where the app thinks it is. set_location pins it to `latitude`+`longitude`, a `query` ('lat, lon' or a Google/Apple Maps link) or a `place` name from get_location. start_route moves it along `points` ([{latitude, longitude}, ...]), a `route` (a preset: walk, run, drive, highway, tunnel, loop; or an app route id), or in a straight line `to` a place, at `speed` m/s; heading and speed follow the route. pause_route / resume_route / seek_route (`meters` or `fraction`) / set_speed control it, and stop_route ends it and stays where it got to. enter_region / exit_region move just inside or outside a monitored geofence `identifier`, which runs the app's geofencing task. conditions sets the signal quality in one step: `conditions` good (\xB15 m), weak (\xB165 m, drifting up to 50 m), no-signal or off. signal (on=false) stops updates and fails requests; services (on=false) reports location services disabled. tune sets `accuracy` and `jitter` (drift) in meters. use_real returns to the real position but keeps signal/services switches; reset ends every override. Works for expo-location (including background tasks and geofencing), @react-native-community/geolocation and react-native-geolocation-service. Permission comes from the device or the Permissions tool: a denied permission still fails. Native maps' own blue dot follows the device's real location; use sim_location for that. The override survives reload_app.",inputSchema:{action:H.enum(["set_location","start_route","pause_route","resume_route","stop_route","seek_route","set_speed","enter_region","exit_region","conditions","signal","services","tune","use_real","reset","settings","clear_log"]).describe("What to do."),latitude:H.number().optional().describe("set_location: latitude, -90 to 90."),longitude:H.number().optional().describe("set_location: longitude, -180 to 180."),label:H.string().optional().describe("set_location: a name to show for the place."),query:H.string().optional().describe("set_location: 'lat, lon' text or a Google/Apple Maps link."),place:H.string().optional().describe("set_location: a place name from get_location (app places and built-ins)."),points:H.array(H.object({latitude:H.number(),longitude:H.number()})).optional().describe("start_route: two or more points to travel through."),route:H.string().optional().describe("start_route: a preset id (walk, run, drive, highway, tunnel, loop) or an app route id."),to:H.string().optional().describe("start_route: travel in a straight line to this place name or 'lat, lon'."),speed:H.number().optional().describe("start_route / set_speed: meters per second (1.4 walk, 13.4 city, 30 highway)."),heading:H.number().optional().describe("start_route with a preset: direction to head, degrees from north. tune: heading reported while pinned."),loop:H.boolean().optional().describe("start_route with points: start over after the last point."),meters:H.number().optional().describe("seek_route: meters from the route's start."),fraction:H.number().optional().describe("seek_route: 0 to 1 along the route."),identifier:H.string().optional().describe("enter_region / exit_region (required): the region's identifier from get_location."),task:H.string().optional().describe("enter_region / exit_region: the geofencing task name, when two tasks share an identifier."),conditions:H.enum(["good","weak","no-signal","off"]).optional().describe("conditions (required): good, weak, no-signal or off."),on:H.boolean().optional().describe("signal / services (required): true = working, false = off."),accuracy:H.number().optional().describe("tune: reported accuracy radius in meters."),jitter:H.number().optional().describe("tune: GPS drift in meters (0 = none)."),interval:H.number().optional().describe("tune: ms between updates to live watches (default 1000)."),persist:H.boolean().optional().describe("settings: keep the override across reloads."),showChip:H.boolean().optional().describe("settings: show the floating location chip while overridden."),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(s,c={})=>n.callAction(cc,s,c,i);const o=(...s)=>{const c={};for(const l of s)if(t[l]!==void 0)c[l]=t[l];return c};let a;switch(t.action){case"set_location":a=await r("setLocation",o("latitude","longitude","label","query","place"));break;case"start_route":a=await r("startRoute",o("points","route","to","speed","heading","loop"));break;case"pause_route":a=await r("pauseRoute");break;case"resume_route":a=await r("resumeRoute");break;case"stop_route":a=await r("stopRoute");break;case"seek_route":a=await r("seekRoute",o("meters","fraction"));break;case"set_speed":if(t.speed===void 0)return x("set_speed requires `speed` in m/s.");a=await r("setSpeed",{speed:t.speed});break;case"enter_region":case"exit_region":if(!t.identifier)return x(`${t.action} requires \`identifier\`; get_location lists the regions.`);a=await r(t.action==="enter_region"?"enterRegion":"exitRegion",o("identifier","task"));break;case"conditions":if(!t.conditions)return x("conditions requires `conditions`: good, weak, no-signal or off.");a=await r("setConditions",{conditions:t.conditions});break;case"signal":if(t.on===void 0)return x("signal requires `on`.");a=await r("setSignal",{on:t.on});break;case"services":if(t.on===void 0)return x("services requires `on`.");a=await r("setServices",{on:t.on});break;case"tune":a=await r("tune",o("accuracy","jitter","heading","interval"));break;case"use_real":a=await r("useRealLocation");break;case"reset":a=await r("reset");break;case"settings":a=await r("updateSettings",o("persist","showChip"));break;case"clear_log":a=await r("clearLog");break}return M(lc(a))}catch(i){return x(j(i))}});e.registerTool("sim_location",{description:"Set the iOS Simulator's own location with `xcrun simctl location` (runs on this Mac, not in the app). Unlike location_action, this reaches native code: map views' blue dot (react-native-maps, expo-maps, Mapbox), native geofence monitoring, other SDKs. set pins `latitude`+`longitude`; route moves through `points` at `speed` m/s (updates every `interval` s); clear stops it. Needs no Buoy in the app. The in-app Location tool, when on, still decides what the app's JS location calls return. Pass `udid` when more than one simulator is booted.",inputSchema:{action:H.enum(["set","route","clear"]).describe("What to do."),latitude:H.number().optional().describe("set: latitude."),longitude:H.number().optional().describe("set: longitude."),points:H.array(H.object({latitude:H.number(),longitude:H.number()})).optional().describe("route: two or more waypoints."),speed:H.number().optional().describe("route: meters per second. Default 20."),interval:H.number().optional().describe("route: seconds between updates. Default 1."),udid:H.string().optional().describe("The simulator's UDID. Required when several are booted.")}},async t=>{try{const i=await rm(t.udid);const r=(...o)=>sc("xcrun",["simctl","location",i,...o],{timeout:15e3});switch(t.action){case"set":if(t.latitude===void 0||t.longitude===void 0)return x("set requires `latitude` and `longitude`.");await r("set",`${t.latitude},${t.longitude}`);return M(`Simulator ${i} location set to ${t.latitude}, ${t.longitude}.`);case"route":{const o=t.points??[];if(o.length<2)return x("route requires two or more `points`.");const a=[`--speed=${t.speed??20}`,`--interval=${t.interval??1}`];await r("start",...a,...o.map(s=>`${s.latitude},${s.longitude}`));return M(`Simulator ${i} is moving through ${o.length} points at ${t.speed??20} m/s.`)}case"clear":await r("clear");return M(`Simulator ${i} location cleared.`)}}catch(i){return x(j(i))}})}import{z as U}from"zod";var dc="scenarios";var Tt=e=>({content:[{type:"text",text:e}]});var Ue=e=>({isError:true,content:[{type:"text",text:e}]});var uc=e=>e instanceof Error?e.message:String(e);function am(e){const n=e.options?.length?` one of ${e.options.join("|")}`:"";const t=e.default!==void 0?` (default ${String(e.default)})`:"";return`${e.key}: ${e.type}${n}${t}`}function Ei(e){const n=e.vars?.length?`
712
+ variables: ${e.vars.map(am).join("; ")}`:"";const t=e.stepLines?.length?`
713
713
  ${e.stepLines.map(o=>` \xB7 ${o.label} [${o.durability}]`).join("\n")}`:"";const i=[`v${e.version}`,e.author,e.usage?.runs?`${e.usage.runs} run(s)`:null,e.authoredAgainst?`authored against ${e.authoredAgainst}`:null,e.unsavedToRepo?"unsaved to repo":null].filter(Boolean).join(" \xB7 ");const r=e.expectedOutcome?`
714
- expected: ${e.expectedOutcome}`:"";return`- \`${e.id}\` **${e.name}** \u2014 ${i}${n}${r}${t}`}function ic(e,n,t){if(!n.length)return`**${e} (0):**
714
+ expected: ${e.expectedOutcome}`:"";return`- \`${e.id}\` **${e.name}** \u2014 ${i}${n}${r}${t}`}function hc(e,n,t){if(!n.length)return`**${e} (0):**
715
715
  _none_`;if(!t.length){return`**${e} (${n.length}):**
716
- ${n.map(ki).join("\n")}`}const i=[`**${e} (${n.length}):**`];for(const o of t){const a=n.filter(s=>(s.folder??"").toLowerCase()===o.toLowerCase());if(!a.length)continue;i.push(`_${o} (${a.length})_
717
- ${a.map(ki).join("\n")}`)}const r=n.filter(o=>!o.folder);if(r.length){i.push(`_Ungrouped (${r.length})_
718
- ${r.map(ki).join("\n")}`)}return i.join("\n")}function Wf(e){const n=Object.entries(e.varValues).map(([t,i])=>`${t}=${String(i)}`).join(", ");return[`**${e.name}** (\`${e.scenarioId}\` v${e.version}) \u2014 active since ${new Date(e.runAt).toISOString()}`,n?`variables: ${n}`:null,`effects:
719
- ${e.effects.map(t=>` \xB7 ${t.label} [${t.kind}]`).join("\n")}`].filter(Boolean).join("\n")}function Hf(e,n){e.registerTool("get_scenarios",{description:"List saved scenarios, configured steps, available durability labels and the reported active scenario. Inspect these settings when app behavior may be simulated. A scenario covers its supported steps, not every source of app or backend state.",inputSchema:{deviceId:U.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(tc,i);const a=r??{};const s=a.code??[];const c=a.device??[];const l=a.drafts??[];const d=a.folders??[];const u=[];u.push(a.active?`\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
720
- ${Wf(a.active)}`:"**No scenario active** \u2014 the app is showing real data.");if(d.length){u.push(`**Folders:** ${d.join(" \xB7 ")} \u2014 the flows this team organizes by. Scenarios below are grouped under them; file new ones into an existing folder with \`scenario_action setFolder\` rather than inventing a name.`)}u.push(ic("Shipped with this app",s,d));u.push(ic("Saved on this device",c,d));if(l.length){u.push(`**Drafts awaiting review on the device (${l.length}):** these are INERT and cannot run until a human accepts them on the device.
721
- ${l.map(ki).join("\n")}`)}return _t(at(o)+u.join("\n\n"))}catch(i){return Le(nc(i))}});e.registerTool("scenario_action",{description:"Preview, run, deactivate, save, organize or delete a scenario. Preview unfamiliar scenarios before running; inspect applied steps, failures and unrestored effects afterward. Storage changes need suitable undo steps. Saved scenarios are drafts requiring acceptance on the device before running. Reuse existing folders when appropriate. A completed scenario does not guarantee a deterministic app or backend state; overrides and cache edits have different coverage and lifetimes.",inputSchema:{action:U.enum(["preview","run","deactivate","save","setFolder","delete"]).describe("What to do."),folder:U.string().optional().describe("Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."),id:U.string().optional().describe("Scenario id (required for preview/run/delete; from get_scenarios)."),params:U.record(U.union([U.string(),U.number(),U.boolean()])).optional().describe("Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."),scenario:U.object({id:U.string().describe("Stable kebab-case id."),name:U.string().describe("Human name; may contain {{variable}} placeholders."),description:U.string().optional(),expectedOutcome:U.string().optional().describe("What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."),author:U.string().optional(),authoredAgainst:U.string().optional().describe("App version this was written against."),folder:U.string().optional().describe('The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'),tags:U.array(U.string()).optional(),vars:U.array(U.object({key:U.string(),label:U.string().optional(),type:U.enum(["string","number","boolean","enum"]),options:U.array(U.union([U.string(),U.number(),U.boolean()])).optional(),default:U.union([U.string(),U.number(),U.boolean()]).optional()})).optional().describe("Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."),steps:U.array(U.object({id:U.string().optional(),tool:U.string().describe("Tool id: network | storage | impersonate | route-events | query | time-machine"),action:U.string().describe("Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."),params:U.unknown().optional(),label:U.string().optional()})).describe("Steps, in order."),undoSteps:U.array(U.object({id:U.string().optional(),tool:U.string(),action:U.string(),params:U.unknown().optional(),label:U.string().optional()})).optional().describe("How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored.")}).optional().describe("The scenario to save (action 'save')."),deviceId:U.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(o,a)=>n.callAction(tc,o,a,i);switch(t.action){case"preview":{if(!t.id)return Le("preview requires `id` (from get_scenarios).");const o=await r("preview",{id:t.id,params:t.params??{}});const a=[`**Dry run \u2014 \`${t.id}\`** (nothing has been changed)`,`variables: ${Object.entries(o.values).map(([s,c])=>`${s}=${String(c)}`).join(", ")||"none"}`,`**Would do:**
716
+ ${n.map(Ei).join("\n")}`}const i=[`**${e} (${n.length}):**`];for(const o of t){const a=n.filter(s=>(s.folder??"").toLowerCase()===o.toLowerCase());if(!a.length)continue;i.push(`_${o} (${a.length})_
717
+ ${a.map(Ei).join("\n")}`)}const r=n.filter(o=>!o.folder);if(r.length){i.push(`_Ungrouped (${r.length})_
718
+ ${r.map(Ei).join("\n")}`)}return i.join("\n")}function sm(e){const n=Object.entries(e.varValues).map(([t,i])=>`${t}=${String(i)}`).join(", ");return[`**${e.name}** (\`${e.scenarioId}\` v${e.version}) \u2014 active since ${new Date(e.runAt).toISOString()}`,n?`variables: ${n}`:null,`effects:
719
+ ${e.effects.map(t=>` \xB7 ${t.label} [${t.kind}]`).join("\n")}`].filter(Boolean).join("\n")}function cm(e,n){e.registerTool("get_scenarios",{description:"List saved scenarios, configured steps, available durability labels and the reported active scenario. Inspect these settings when app behavior may be simulated. A scenario covers its supported steps, not every source of app or backend state.",inputSchema:{deviceId:U.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const{payload:r,stale:o}=await n.getSnapshot(dc,i);const a=r??{};const s=a.code??[];const c=a.device??[];const l=a.drafts??[];const d=a.folders??[];const u=[];u.push(a.active?`\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
720
+ ${sm(a.active)}`:"**No scenario active** \u2014 the app is showing real data.");if(d.length){u.push(`**Folders:** ${d.join(" \xB7 ")} \u2014 the flows this team organizes by. Scenarios below are grouped under them; file new ones into an existing folder with \`scenario_action setFolder\` rather than inventing a name.`)}u.push(hc("Shipped with this app",s,d));u.push(hc("Saved on this device",c,d));if(l.length){u.push(`**Drafts awaiting review on the device (${l.length}):** these are INERT and cannot run until a human accepts them on the device.
721
+ ${l.map(Ei).join("\n")}`)}return Tt(ct(o)+u.join("\n\n"))}catch(i){return Ue(uc(i))}});e.registerTool("scenario_action",{description:"Preview, run, deactivate, save, organize or delete a scenario. Preview unfamiliar scenarios before running; inspect applied steps, failures and unrestored effects afterward. Storage changes need suitable undo steps. Saved scenarios are drafts requiring acceptance on the device before running. Reuse existing folders when appropriate. A completed scenario does not guarantee a deterministic app or backend state; overrides and cache edits have different coverage and lifetimes.",inputSchema:{action:U.enum(["preview","run","deactivate","save","setFolder","delete"]).describe("What to do."),folder:U.string().optional().describe("Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."),id:U.string().optional().describe("Scenario id (required for preview/run/delete; from get_scenarios)."),params:U.record(U.union([U.string(),U.number(),U.boolean()])).optional().describe("Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."),scenario:U.object({id:U.string().describe("Stable kebab-case id."),name:U.string().describe("Human name; may contain {{variable}} placeholders."),description:U.string().optional(),expectedOutcome:U.string().optional().describe("What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."),author:U.string().optional(),authoredAgainst:U.string().optional().describe("App version this was written against."),folder:U.string().optional().describe('The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'),tags:U.array(U.string()).optional(),vars:U.array(U.object({key:U.string(),label:U.string().optional(),type:U.enum(["string","number","boolean","enum"]),options:U.array(U.union([U.string(),U.number(),U.boolean()])).optional(),default:U.union([U.string(),U.number(),U.boolean()]).optional()})).optional().describe("Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."),steps:U.array(U.object({id:U.string().optional(),tool:U.string().describe("Tool id: network | storage | impersonate | route-events | query | time-machine"),action:U.string().describe("Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."),params:U.unknown().optional(),label:U.string().optional()})).describe("Steps, in order."),undoSteps:U.array(U.object({id:U.string().optional(),tool:U.string(),action:U.string(),params:U.unknown().optional(),label:U.string().optional()})).optional().describe("How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored.")}).optional().describe("The scenario to save (action 'save')."),deviceId:U.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=(o,a)=>n.callAction(dc,o,a,i);switch(t.action){case"preview":{if(!t.id)return Ue("preview requires `id` (from get_scenarios).");const o=await r("preview",{id:t.id,params:t.params??{}});const a=[`**Dry run \u2014 \`${t.id}\`** (nothing has been changed)`,`variables: ${Object.entries(o.values).map(([s,c])=>`${s}=${String(c)}`).join(", ")||"none"}`,`**Would do:**
722
722
  ${o.willDo.map(s=>` \xB7 ${s.label} [${s.durability}]`).join("\n")}`];if(o.expectedOutcome)a.push(`**Expected outcome:** ${o.expectedOutcome}`);if(o.conflict){a.push(`\u26A0\uFE0F **Conflict:** "${o.conflict.activeName}" is active and touches the same target(s): ${o.conflict.overlaps.join(", ")}. Running will SWAP \u2014 it deactivates first.`)}if(o.warnings?.length)a.push(`**Warnings:**
723
723
  ${o.warnings.map(s=>` \xB7 ${s}`).join("\n")}`);a.push(o.ok?"Pre-flight checks passed. Review the intended effects before running.":`\u274C **Cannot run:**
724
- ${o.errors.map(s=>` \xB7 ${s}`).join("\n")}`);return _t(a.join("\n\n"))}case"run":{if(!t.id)return Le("run requires `id` (from get_scenarios).");const o=await r("run",{id:t.id,params:t.params??{}});if(o.preflightErrors?.length){return Le(`Pre-flight refused to run \`${t.id}\` \u2014 nothing was changed:
724
+ ${o.errors.map(s=>` \xB7 ${s}`).join("\n")}`);return Tt(a.join("\n\n"))}case"run":{if(!t.id)return Ue("run requires `id` (from get_scenarios).");const o=await r("run",{id:t.id,params:t.params??{}});if(o.preflightErrors?.length){return Ue(`Pre-flight refused to run \`${t.id}\` \u2014 nothing was changed:
725
725
  ${o.preflightErrors.map(s=>` \xB7 ${s}`).join("\n")}`)}const a=(o.steps??[]).map(s=>{const c=s.skipped?"\u2013":s.ok?"\u2713":"\u2717";const l=s.error?`
726
- ${s.error}`:"";return` ${c} ${s.label} (${s.ms}ms)${l}`}).join("\n");if(!o.ok){return Le(`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
726
+ ${s.error}`:"";return` ${c} ${s.label} (${s.ms}ms)${l}`}).join("\n");if(!o.ok){return Ue(`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
727
727
  ${a}
728
728
 
729
- The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`)}return _t([`\u2705 **Scenario active \u2014 ${o.name??t.id}**`,a,`**Reported effects:**
729
+ The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`)}return Tt([`\u2705 **Scenario active \u2014 ${o.name??t.id}**`,a,`**Reported effects:**
730
730
  ${(o.effects??[]).map(s=>` \xB7 ${s.label} [${s.kind}]`).join("\n")}`,"The device shows a SIMULATED banner while this is active. Deactivate when done, and account for these effects when interpreting observations."].join("\n\n"))}case"deactivate":{const o=await r("deactivate",{});const a=o.report;const s=[`Removed ${a.removedRules} override rule(s)`,a.stoppedImpersonation?"Ended impersonation":null,a.undoStepsRan?`Ran ${a.undoStepsRan} undo step(s)`:null].filter(Boolean);const c=a.unrestoredState??[];const l=(a.unrestoredStorageKeys.length?`
731
731
 
732
732
  \u26A0\uFE0F **NOT restored:** ${a.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.`:"")+(c.length?`
733
733
 
734
734
  \u26A0\uFE0F **Stores NOT restored:** ${c.join(", ")} \u2014 a state write has no automatic inverse. These stores still hold what the scenario wrote.`:"");const d=a.errors.length?`
735
735
 
736
- Errors: ${a.errors.join("; ")}`:"";return _t(`**Deactivated.**
737
- ${s.map(u=>` \xB7 ${u}`).join("\n")}${l}${d}`)}case"save":{if(!t.scenario)return Le("save requires `scenario`.");const o=await r("save",{scenario:t.scenario});if(!o.ok){return Le(`Save rejected:
738
- ${(o.errors??[o.error??"unknown error"]).map(a=>` \xB7 ${a}`).join("\n")}`)}return _t(`Saved \`${o.id}\` **as a draft**.
736
+ Errors: ${a.errors.join("; ")}`:"";return Tt(`**Deactivated.**
737
+ ${s.map(u=>` \xB7 ${u}`).join("\n")}${l}${d}`)}case"save":{if(!t.scenario)return Ue("save requires `scenario`.");const o=await r("save",{scenario:t.scenario});if(!o.ok){return Ue(`Save rejected:
738
+ ${(o.errors??[o.error??"unknown error"]).map(a=>` \xB7 ${a}`).join("\n")}`)}return Tt(`Saved \`${o.id}\` **as a draft**.
739
739
 
740
- It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`)}case"setFolder":{if(!t.id)return Le("setFolder requires `id` (from get_scenarios).");const o=await r("setFolder",{id:t.id,folder:t.folder});return _t(o.folder?`Filed \`${o.id}\` under **${o.folder}**.`:`Removed \`${o.id}\` from its folder \u2014 it now shows under Ungrouped.`)}case"delete":{if(!t.id)return Le("delete requires `id`.");await r("delete",{id:t.id});return _t(`Deleted \`${t.id}\` from the device library.`)}}}catch(i){return Le(nc(i))}})}import{z as et}from"zod";var Ae=e=>({content:[{type:"text",text:e}]});var St=e=>Mn()?null:tt(Kc(e));var tt=e=>({isError:true,content:[{type:"text",text:e}]});function Jf(e){e.registerTool("camera_source",{description:"Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",inputSchema:{kind:et.enum(["barcode","image","video","camera","screen","pattern"]).describe("What the camera should show."),value:et.string().optional().describe("Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."),format:et.enum(["qr","pdf417","aztec","code128"]).optional().describe("Barcode symbology; defaults to qr.")}},async n=>{const t=St("camera_source");if(t)return t;const i=["source",n.kind];if(n.value)i.push(n.value);if(n.format)i.push("--format",n.format);const r=await Ne(i);if(!r.ok){return tt(r.error??r.data?.error??"could not set the camera source")}const o=n.kind==="barcode"?`a ${n.format??"qr"} code reading "${n.value}"`:n.kind==="screen"?`this Mac's screen (${n.value??"behind the simulator window"})`:n.value??n.kind;return Ae(`The simulator camera is now showing ${o}, at ${r.data?.fps??0} fps.
740
+ It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`)}case"setFolder":{if(!t.id)return Ue("setFolder requires `id` (from get_scenarios).");const o=await r("setFolder",{id:t.id,folder:t.folder});return Tt(o.folder?`Filed \`${o.id}\` under **${o.folder}**.`:`Removed \`${o.id}\` from its folder \u2014 it now shows under Ungrouped.`)}case"delete":{if(!t.id)return Ue("delete requires `id`.");await r("delete",{id:t.id});return Tt(`Deleted \`${t.id}\` from the device library.`)}}}catch(i){return Ue(uc(i))}})}import{z as nt}from"zod";var Ce=e=>({content:[{type:"text",text:e}]});var Dt=e=>jn()?null:it(ul(e));var it=e=>({isError:true,content:[{type:"text",text:e}]});function lm(e){e.registerTool("camera_source",{description:"Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",inputSchema:{kind:nt.enum(["barcode","image","video","camera","screen","pattern"]).describe("What the camera should show."),value:nt.string().optional().describe("Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."),format:nt.enum(["qr","pdf417","aztec","code128"]).optional().describe("Barcode symbology; defaults to qr.")}},async n=>{const t=Dt("camera_source");if(t)return t;const i=["source",n.kind];if(n.value)i.push(n.value);if(n.format)i.push("--format",n.format);const r=await Be(i);if(!r.ok){return it(r.error??r.data?.error??"could not set the camera source")}const o=n.kind==="barcode"?`a ${n.format??"qr"} code reading "${n.value}"`:n.kind==="screen"?`this Mac's screen (${n.value??"behind the simulator window"})`:n.value??n.kind;return Ce(`The simulator camera is now showing ${o}, at ${r.data?.fps??0} fps.
741
741
 
742
- _Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`)});e.registerTool("camera_status",{description:"Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",inputSchema:{}},async()=>{const n=St("camera_status");if(n)return n;const t=await Ne(["status"]);if(!t.ok||!t.data){return tt(t.error??"could not read the simulator camera status")}const{publishing:i,fps:r,codes:o}=t.data;if(!i){return Ae("The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._")}return Ae([`Publishing at ${r} fps.`,o.length?`Currently decoding: ${o.map(a=>`\`${a}\``).join(", ")}`:"No barcode in the current frame.","","_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"].join("\n"))});e.registerTool("camera_launch",{description:"Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",inputSchema:{bundleId:et.string().describe("The app's bundle identifier."),udid:et.string().optional().describe("Simulator udid, if more than one is booted.")}},async n=>{const t=St("camera_launch");if(t)return t;const i=["launch",n.bundleId];if(n.udid)i.push("--udid",n.udid);const r=await Ne(i);if(!r.ok){return tt(r.error??r.data?.error??"could not launch the app")}return Ae(`Relaunched \`${n.bundleId}\` with the camera attached${r.data?.pid?` (pid ${r.data.pid})`:""}.`)});e.registerTool("camera_diagnose",{description:"Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",inputSchema:{bundleId:et.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")}},async n=>{const t=["diagnose"];if(n.bundleId)t.push(n.bundleId);const i=await Ne(t);if(!i.data?.checks){return tt(i.error??"could not run the simulator camera diagnosis")}const r=Vc();const o=Mn(r)?"":"\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";return Ae(Jc(i.data.checks)+o)});e.registerTool("camera_stop",{description:"Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",inputSchema:{}},async()=>{const n=St("camera_stop");if(n)return n;const t=await Ne(["stop"]);if(!t.ok){return tt(t.error??t.data?.error??"could not stop the camera")}return Ae("The simulator camera is off.")});e.registerTool("camera_devices",{description:"List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",inputSchema:{udid:et.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")}},async n=>{const t=St("camera_devices");if(t)return t;const i=await Ne(["devices"]);if(!i.ok||!i.data){return tt(i.error??"could not list booted simulators")}const r=i.data.devices??[];let o=null;if(r.length===1||n.udid){const a=await Ne(["apps",...n.udid?["--udid",n.udid]:[]]);o=a.ok?a.data?.apps??[]:null}return Ae(Hc(r,o))});e.registerTool("camera_inputs",{description:"List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",inputSchema:{}},async()=>{const n=St("camera_inputs");if(n)return n;const[t,i]=await Promise.all([Ne(["cameras"]),Ne(["windows"])]);if(!t.ok&&!i.ok){return tt(t.error??i.error??"could not list the Mac's cameras or windows")}return Ae(Gc(t.data?.cameras??[],i.data?.windows??[]))});e.registerTool("camera_zero_setup",{description:"Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",inputSchema:{action:et.enum(["on","off","status"]).describe("Turn it on, off, or report the current state."),udid:et.string().optional().describe("Which simulator. Only needed when several are booted.")}},async n=>{const t=St("camera_zero_setup");if(t)return t;const i=await Ne(["zero-setup",n.action,...n.udid?["--udid",n.udid]:[]]);if(!i.ok){return tt(i.error??i.data?.error??"could not change zero-setup mode")}const r=i.data??{};if(n.action==="status"){if(!r.enabled)return Ae("Zero-setup mode is **off**.");return Ae(r.foreign?`Zero-setup is on, but **another tool owns it** (\`${r.path}\`). Quit that tool before turning Buoy's on.`:"Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera.")}if(n.action==="on"){return Ae("Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._")}return Ae("Zero-setup mode is **off**.")})}import{z as C}from"zod";function Gf(e,n){const t=Math.max(e+1,e*n);const i=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/i)*i}function Kf(e){if(!e.length)return null;const n=[...e].sort((i,r)=>i-r);const t=Math.floor(n.length/2);return n.length%2?n[t]:Math.round((n[t-1]+n[t])/2)}function rc(e,n,t){if(n-e<=1)return true;if(e<=0)return false;return(n-e)/e<=t/100}var oc=(e,n)=>`new Promise((resolve) => {
742
+ _Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`)});e.registerTool("camera_status",{description:"Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",inputSchema:{}},async()=>{const n=Dt("camera_status");if(n)return n;const t=await Be(["status"]);if(!t.ok||!t.data){return it(t.error??"could not read the simulator camera status")}const{publishing:i,fps:r,codes:o}=t.data;if(!i){return Ce("The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._")}return Ce([`Publishing at ${r} fps.`,o.length?`Currently decoding: ${o.map(a=>`\`${a}\``).join(", ")}`:"No barcode in the current frame.","","_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"].join("\n"))});e.registerTool("camera_launch",{description:"Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",inputSchema:{bundleId:nt.string().describe("The app's bundle identifier."),udid:nt.string().optional().describe("Simulator udid, if more than one is booted.")}},async n=>{const t=Dt("camera_launch");if(t)return t;const i=["launch",n.bundleId];if(n.udid)i.push("--udid",n.udid);const r=await Be(i);if(!r.ok){return it(r.error??r.data?.error??"could not launch the app")}return Ce(`Relaunched \`${n.bundleId}\` with the camera attached${r.data?.pid?` (pid ${r.data.pid})`:""}.`)});e.registerTool("camera_diagnose",{description:"Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",inputSchema:{bundleId:nt.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")}},async n=>{const t=["diagnose"];if(n.bundleId)t.push(n.bundleId);const i=await Be(t);if(!i.data?.checks){return it(i.error??"could not run the simulator camera diagnosis")}const r=hl();const o=jn(r)?"":"\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";return Ce(ll(i.data.checks)+o)});e.registerTool("camera_stop",{description:"Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",inputSchema:{}},async()=>{const n=Dt("camera_stop");if(n)return n;const t=await Be(["stop"]);if(!t.ok){return it(t.error??t.data?.error??"could not stop the camera")}return Ce("The simulator camera is off.")});e.registerTool("camera_devices",{description:"List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",inputSchema:{udid:nt.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")}},async n=>{const t=Dt("camera_devices");if(t)return t;const i=await Be(["devices"]);if(!i.ok||!i.data){return it(i.error??"could not list booted simulators")}const r=i.data.devices??[];let o=null;if(r.length===1||n.udid){const a=await Be(["apps",...n.udid?["--udid",n.udid]:[]]);o=a.ok?a.data?.apps??[]:null}return Ce(cl(r,o))});e.registerTool("camera_inputs",{description:"List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",inputSchema:{}},async()=>{const n=Dt("camera_inputs");if(n)return n;const[t,i]=await Promise.all([Be(["cameras"]),Be(["windows"])]);if(!t.ok&&!i.ok){return it(t.error??i.error??"could not list the Mac's cameras or windows")}return Ce(dl(t.data?.cameras??[],i.data?.windows??[]))});e.registerTool("camera_zero_setup",{description:"Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",inputSchema:{action:nt.enum(["on","off","status"]).describe("Turn it on, off, or report the current state."),udid:nt.string().optional().describe("Which simulator. Only needed when several are booted.")}},async n=>{const t=Dt("camera_zero_setup");if(t)return t;const i=await Be(["zero-setup",n.action,...n.udid?["--udid",n.udid]:[]]);if(!i.ok){return it(i.error??i.data?.error??"could not change zero-setup mode")}const r=i.data??{};if(n.action==="status"){if(!r.enabled)return Ce("Zero-setup mode is **off**.");return Ce(r.foreign?`Zero-setup is on, but **another tool owns it** (\`${r.path}\`). Quit that tool before turning Buoy's on.`:"Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera.")}if(n.action==="on"){return Ce("Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._")}return Ce("Zero-setup mode is **off**.")})}import{z as ve}from"zod";var en=e=>({content:[{type:"text",text:e}]});var Ne=e=>({isError:true,content:[{type:"text",text:e}]});var tn=e=>en(JSON.stringify(e,null,2));var pc;var Mn=e=>Bn(pc)?null:Ne(Fn(e));var An=`mcp-${process.pid}`;async function dm(){const e=il();if(!rl(e))await ol(e);await tl([process.argv[1],"farm","chrome","keeper"],e)}var fc=e=>({id:e.id,url:e.url,browserWs:e.browserWs,pageWs:e.pageWs,memMB:e.memMB,cpuPct:e.cpuPct,capMB:e.capMB,paused:e.paused||void 0,label:e.label,readyMs:e.readyMs,viewport:`${e.viewport.width}x${e.viewport.height}@${e.viewport.dpr}${e.viewport.mobile?" mobile":""}`});var Vr={ids:ve.array(ve.string()).optional().describe("Instance ids from chrome_farm_up or chrome_farm_status."),all:ve.boolean().optional().describe("Every instance.")};function um(e,n){pc=n;e.registerTool("chrome_farm_status",{description:"Show the Chrome Farm: running Chrome instances with their memory, CPU, memory cap and debugging endpoints, plus this computer's cores and how many instances to run. Each instance is a slim Chrome (chrome-headless-shell, about 230 MB idle) running one web app with its own storage. Start here before chrome_farm_up.",inputSchema:{}},async()=>{try{const t=await el();const i=sl(t);const r=await Ct()?await pt("GET","/status"):null;return tn({cores:t.cores,topCores:t.topCores,memGB:t.memGB,suggested:{functional:i.functional,perf:i.perf,note:"Pass/fail tests stay smooth up to `functional` busy instances. Frame times and input delay match a lone Chrome only up to `perf` busy at once; use chrome_farm_quiet for a perf sample on a busy farm."},instances:r?.instances.map(fc)??[],events:r?.events.slice(-10)??[]})}catch(t){return Ne(`Chrome Farm error: ${t?.message??t}`)}});e.registerTool("chrome_farm_up",{description:"Start Chrome instances on a web app. Each gets its own browser, storage and memory cap, and a debugging endpoint: `browserWs` works with Playwright's chromium.connectOverCDP(browserWs) (use contexts()[0].pages()[0]); `pageWs` works with Puppeteer or raw CDP. If the app has Buoy inside, each instance also shows up in list_devices as its own web device, so every Buoy tool works on it. Instances stay up until chrome_farm_down (the keeper closes all of them after 10 idle minutes with none left).",inputSchema:{url:ve.string().describe("The web app's address, e.g. http://localhost:3000."),count:ve.number().int().min(1).max(128).optional().describe("How many instances. Default 1."),ready:ve.string().optional().describe("Text or CSS selector the page must show before it counts as ready."),viewport:ve.enum(["phone","desktop"]).optional().describe("phone = 402x874 at 3x with touch (default); desktop = 1280x800 at 2x."),capMB:ve.number().int().min(256).max(16384).optional().describe("Memory cap per instance in MB (default 2048). An instance over it is closed."),label:ve.string().optional().describe("A short label shown in chrome_farm_status."),boost:ve.boolean().optional().describe("Give whichever instance a test is driving a top core while it acts (macOS, needs clang). Steps ran ~15-18% faster with 32-48 busy instances."),gpu:ve.boolean().optional().describe("Default true: draw with the GPU (Metal on a Mac), like a lone Chrome. false draws on the CPU: less memory, but heavy canvas and WebGL pages run much slower, so never compare speed that way.")}},async t=>{const i=Mn("chrome_farm_up");if(i)return i;try{await dm();const r=await pt("POST","/up",{n:t.count??1,url:t.url,readyText:t.ready,viewport:t.viewport,capMB:t.capMB,label:t.label,boost:t.boost,noGpu:t.gpu===false,owner:An},void 0,60*6e4);return tn({started:r.started.map(fc),failed:r.failed})}catch(r){return Ne(`Chrome Farm error: ${r?.message??r}`)}});e.registerTool("chrome_farm_down",{description:"Close Chrome Farm instances. Close the ones you started when your work is done.",inputSchema:Vr},async t=>{try{if(!await Ct())return en("No instances are running.");if(!t.all&&!t.ids?.length)return Ne("Give ids or all: true.");return tn(await pt("POST","/down",{ids:t.ids,all:t.all,owner:An}))}catch(i){return Ne(`Chrome Farm error: ${i?.message??i}`)}});e.registerTool("chrome_farm_reset",{description:"Clear instances' storage (cookies, localStorage, IndexedDB, caches) and reload their app in the same browser. About 4x faster than closing and starting a new instance, so reuse instances between tests.",inputSchema:Vr},async t=>{const i=Mn("chrome_farm_reset");if(i)return i;try{if(!t.all&&!t.ids?.length)return Ne("Give ids or all: true.");if(!await Ct())return en("No instances are running.");return tn(await pt("POST","/reset",{ids:t.ids,all:t.all,owner:An}))}catch(r){return Ne(`Chrome Farm error: ${r?.message??r}`)}});e.registerTool("chrome_farm_pause",{description:"Pause or wake Chrome Farm instances. A paused instance keeps its page, storage and memory but uses no CPU (a loaded app can do work every second even when no test touches it). Pause the ones you hold but aren't using right now, and wake them when you need them; waking is instant.",inputSchema:{...Vr,wake:ve.boolean().optional().describe("true = wake them instead of pausing.")}},async t=>{const i=Mn("chrome_farm_pause");if(i)return i;try{if(!t.all&&!t.ids?.length)return Ne("Give ids or all: true.");if(!await Ct())return en("No instances are running.");return tn(await pt("POST",t.wake?"/resume":"/pause",{ids:t.ids,all:t.all,owner:An}))}catch(r){return Ne(`Chrome Farm error: ${r?.message??r}`)}});e.registerTool("chrome_farm_quiet",{description:"Pause your other Chrome Farm instances for a few seconds so this one gets the computer to itself. Use it around a perf sample (frame times, input delay) when many instances are busy; they wake by themselves. Paused instances don't run JS, timers or frames, so keep the window short. Other sessions' instances are never paused.",inputSchema:{id:ve.string().describe("The instance that gets the quiet window."),seconds:ve.number().min(1).max(120).optional().describe("How long. Default 10.")}},async t=>{const i=Mn("chrome_farm_quiet");if(i)return i;try{if(!await Ct())return en("No instances are running.");return tn(await pt("POST","/quiet",{id:t.id,seconds:t.seconds??10,owner:An}))}catch(r){return Ne(`Chrome Farm error: ${r?.message??r}`)}});e.registerTool("chrome_farm_screenshot",{description:"Take a screenshot of one Chrome Farm instance.",inputSchema:{id:ve.string().describe("The instance id.")}},async t=>{const i=Mn("chrome_farm_screenshot");if(i)return i;try{if(!await Ct())return en("No instances are running.");const r=await pt("POST","/shot",{id:t.id});return{content:[{type:"image",data:r.png,mimeType:"image/png"}]}}catch(r){return Ne(`Chrome Farm error: ${r?.message??r}`)}})}import{z as E}from"zod";function hm(e,n){const t=Math.max(e+1,e*n);const i=Math.pow(10,Math.max(0,Math.floor(Math.log10(t))-1));return Math.ceil(t/i)*i}function pm(e){if(!e.length)return null;const n=[...e].sort((i,r)=>i-r);const t=Math.floor(n.length/2);return n.length%2?n[t]:Math.round((n[t-1]+n[t])/2)}function mc(e,n,t){if(n-e<=1)return true;if(e<=0)return false;return(n-e)/e<=t/100}var wc=(e,n)=>`new Promise((resolve) => {
743
743
  setTimeout(() => {
744
744
  const deltas = [];
745
745
  let start;
@@ -761,7 +761,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
761
761
  };
762
762
  requestAnimationFrame(loop);
763
763
  }, ${e});
764
- })`;async function Vf(e,n){const t=[];const i=(l,d,u,h)=>({status:l,stopReason:d,highestPass:u?{load:u.load,fps:u.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:l==="below_one"||l==="bracketed"&&!!u&&!!h&&rc(u.load,h.load,n.precisionPct),steps:t});const r=(l,d,u)=>i("invalid",l.fps==null?"measurement_failed":"page_error",d,u);const o=async(l,d=false)=>{const u=await e(l);if(d)u.confirm=true;t.push(u);return u};let a=null;let s=null;let c=await o(n.start);if(!c.valid)return r(c,null,null);if(c.pass)a=c;else{s=c;let l=n.start;while(!a){if(l<=1)return i("below_one","performance",null,s);if(!n.timeLeft())return i("lower_bound","deadline",null,s);l=Math.max(1,Math.floor(l/2));c=await o(l);if(!c.valid)return r(c,null,s);if(c.pass)a=c;else s=c}}while(!s||s.load<=a.load){if(a.load>=n.max)return i("lower_bound","max",a,null);if(!n.timeLeft())return i("lower_bound","deadline",a,null);c=await o(Math.min(n.max,Gf(a.load,n.growth)));if(!c.valid)return r(c,a,null);if(c.pass){a=c;continue}if(!n.timeLeft())return i("bracketed","performance",a,c);const l=await o(c.load,true);if(!l.valid)return r(l,a,null);if(l.pass)a=l;else s=l}while(!rc(a.load,s.load,n.precisionPct)&&n.timeLeft()){const l=Math.round((a.load+s.load)/2);if(l<=a.load||l>=s.load)break;c=await o(l);if(!c.valid)break;if(c.pass)a=c;else s=c}return i("bracketed","performance",a,s)}async function Yf(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.param)throw new Error('Pass `param`, the query param that sets the load (e.g. "lights").');const t=(v,$)=>{if(v!=null&&!Number.isFinite(v))throw new Error(`${$} must be a finite number.`)};for(const[v,$]of Object.entries({start:e.start,growth:e.growth,max:e.max,targetFps:e.targetFps,precisionPct:e.precisionPct,rounds:e.rounds,settleMs:e.settleMs,measureMs:e.measureMs,cpuSlowdown:e.cpuSlowdown}))t($,v);const i=Math.max(1,Math.floor(e.start??100));const r=Math.min(4,Math.max(1.05,e.growth??1.3));const o=Math.max(i,Math.floor(e.max??1e9));const a=e.targetFps??55;const s=Math.min(50,Math.max(.5,e.precisionPct??4));const c=Math.max(1,Math.min(5,Math.floor(e.rounds??1)));const l=Math.max(0,e.settleMs??1e3);const d=Math.max(500,e.measureMs??2e3);const u=Math.max(1,e.cpuSlowdown??1);const h={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const b=[];const f=Date.now();const w=f+(e.maxBatchMs??40*6e4);const g=()=>w-Date.now();const m=f>>>0;const p=await ro(Math.min(e.lockWaitMs??20*6e4,Math.max(0,g())),n);let y=false;try{const v=await io(e.projectRoot??process.cwd());const $=await v.launch({headless:!e.headed,args:["--enable-gpu","--disable-backgrounding-occluded-windows","--disable-renderer-backgrounding","--disable-background-timer-throttling"]});try{const T={viewport:{width:h.width,height:h.height},deviceScaleFactor:h.deviceScaleFactor};let k=null;let D=60;{const _=await $.newContext(T);try{const S=await _.newPage();await S.goto("about:blank");k=await S.evaluate(il)??null;const A=await oo(S.evaluate(oc(200,1e3)),1e4,async()=>({fps:60}));D=sl(A.fps)}finally{await Mt(_,5e3)}}n(`GPU ${k??"unknown"} \xB7 idle ${D} Hz`);if(k&&no(k))b.push(`Chrome is drawing with a software GPU (${k}), so ceilings are far lower than on a real screen.`);if(a>D*.95){b.push(`The target (${a} fps) is at or above this browser's idle frame rate (${D} Hz), so even an empty page can miss it. Use headed: true on a faster screen, or measure with a target your contract allows.`)}const E=e.cases.map(_=>({caseName:_.name,ceiling:null,atLeast:null,rounds:[],errors:[]}));const I=async(_,S,A,O)=>{const F=O.length;const P=to(e.url,{...S,params:{...S.params??{},[e.param]:String(A)}},e.path);const K=N=>Math.max(1e3,Math.min(N,g()+15e3));try{await _.goto(P,{waitUntil:"load",timeout:K(6e4)});if(e.readySelector)await _.waitForSelector(e.readySelector,{state:"attached",timeout:K(6e4)});const N=await oo(_.evaluate(oc(l,d)),l+d+6e4,async()=>{throw new Error("the page stopped drawing (no frames for 60 s)")});const q=O.length>F?O[O.length-1]:void 0;if(q)return{load:A,fps:N.fps,p95GapMs:N.p95,valid:false,pass:false,error:q};return{load:A,fps:N.fps,p95GapMs:N.p95,valid:true,pass:N.fps>=a}}catch(N){return{load:A,fps:null,p95GapMs:null,valid:false,pass:false,error:(N?.message??String(N)).split("\n")[0].slice(0,200)}}};let R=false;for(let _=0;_<c&&!R;_++){const S=al(e.cases.map((A,O)=>({c:A,i:O})),m+_*7919>>>0);for(const{c:A,i:O}of S){const F=E[O];if(R||g()<=0){F.rounds.push({round:_+1,status:"skipped",stopReason:"not_run",highestPass:null,lowestMiss:null,resolved:false,steps:[]});continue}n(`round ${_+1}/${c} \xB7 ${A.name}`);let P;try{P=await $.newContext(T);const K=await P.newPage();K.on("pageerror",q=>F.errors.push(String(q?.message??q).slice(0,200)));if(u>1){const q=await P.newCDPSession(K);await q.send("Emulation.setCPUThrottlingRate",{rate:u})}const N=await Vf(q=>I(K,A,q,F.errors),{start:i,growth:r,max:o,precisionPct:s,timeLeft:()=>g()>0});F.rounds.push({round:_+1,...N});n(` ${A.name}: ${N.status} ${N.highestPass?.load??"-"}`)}catch(K){F.errors.push((K?.message??String(K)).split("\n")[0].slice(0,200));F.rounds.push({round:_+1,status:"invalid",stopReason:"measurement_failed",highestPass:null,lowestMiss:null,resolved:false,steps:[]})}finally{if(P&&!await Mt(P,1e4)){y=true;R=true;b.push(`${A.name}'s browser context didn't close, so the batch stopped to keep later runs clean.`)}}}}for(const _ of E){const S=_.rounds.filter(O=>O.status==="bracketed"||O.status==="below_one");_.ceiling=Kf(S.map(O=>O.highestPass?.load??0));const A=_.rounds.filter(O=>O.status==="lower_bound"&&O.highestPass).map(O=>O.highestPass.load);_.atLeast=A.length?Math.max(...A):null}if(g()<=0)b.push("The batch hit its time limit. Rounds marked skipped or lower_bound didn't finish.");return{url:e.url,browser:`Chromium ${$.version()}${e.headed?"":" (headless)"}`,gpu:k,refreshHz:D,targetFps:a,cpuSlowdown:u,viewport:h,seed:m,results:E,warnings:b}}finally{if(!await Mt($,1e4))y=true}}finally{p(y)}}function Xf(e){const n=o=>o==null?"\u2014":o.toLocaleString("en-US");const t=o=>o?`${n(o.load)} @ ${o.fps.toFixed(1)}`:"\u2014";const i=[];i.push(`### Web ceiling \xB7 ${e.url}`);i.push(`${e.browser}${e.gpu?` \xB7 GPU: ${e.gpu}${no(e.gpu)?" \u26A0\uFE0F software":""}`:""} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 idle ${e.refreshHz} Hz \xB7 target ${e.targetFps} fps${e.cpuSlowdown>1?` \xB7 CPU slowed ${e.cpuSlowdown}x`:""}`);i.push("");i.push("**Ceiling** is the most load that held the target, timed from main-thread `requestAnimationFrame` callbacks (a stand-in for drawing speed; it can't see a canvas that stopped updating, so check the looks too). Each round ramps ~30% a step, confirms the first miss, then bisects. With several rounds the ceiling is their median. Ceilings within about 10% are a tie until more rounds agree.");i.push("");const r=Math.max(0,...e.results.map(o=>o.ceiling??0));i.push("| Case | Ceiling | Each round: highest pass @ fps \u2192 lowest miss @ fps | Notes |");i.push("|------|--------:|------------------------------------------------------|-------|");for(const o of e.results){const a=o.rounds.map(c=>{if(c.status==="skipped")return`r${c.round}: not run`;if(c.status==="invalid")return`r${c.round}: invalid (${c.stopReason}) after ${t(c.highestPass)}`;if(c.status==="lower_bound")return`r${c.round}: at least ${t(c.highestPass)} (${c.stopReason})`;if(c.status==="below_one")return`r${c.round}: even 1 missed (${t(c.lowestMiss)})`;return`r${c.round}: ${t(c.highestPass)} \u2192 ${t(c.lowestMiss)}${c.resolved?"":" (wide)"}`}).join("<br>");const s=[o.ceiling===r&&r>0?"best":"",o.atLeast!=null?`at least ${n(o.atLeast)}`:"",o.rounds.some(c=>c.status==="invalid")?"a round broke: check the page before trusting it":"",o.errors.length?`${o.errors.length} page error(s): ${o.errors[0]}`:""].filter(Boolean);i.push(`| ${o.caseName} | ${n(o.ceiling)} | ${a} | ${s.join("; ")} |`)}if(e.warnings.length){i.push("");for(const o of e.warnings)i.push(`- \u26A0\uFE0F ${o}`)}return i.join("\n")}import{mkdirSync as Zf,readFileSync as ac}from"fs";import{join as sc}from"path";var Qf=e=>e.replace(/[^\w.-]+/g,"_").slice(0,80);async function em(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.outDir)throw new Error("Pass outDir, the folder to save screenshots in.");const t=[...new Set((e.atMs?.length?e.atMs:[500,2e3]).map(c=>Math.max(0,Math.round(c))))].sort((c,l)=>c-l);const i=sc(e.outDir,new Date().toISOString().replace(/[:.]/g,"-"));Zf(i,{recursive:true});const r={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const o={dir:"",files:[],pageErrors:[],diffs:[],outliers:[]};o.dir=i;const a=await ro(e.lockWaitMs??20*6e4,n);let s=false;try{const c=await io(e.projectRoot??process.cwd());const l=await c.launch({headless:true,args:["--enable-gpu"]});try{for(const[d,u]of e.cases.entries()){let h;try{h=await l.newContext({viewport:{width:r.width,height:r.height},deviceScaleFactor:r.deviceScaleFactor});const b=await h.newPage();b.on("pageerror",w=>o.pageErrors.push({caseName:u.name,error:String(w?.message??w).slice(0,200)}));await b.goto(to(e.url,u,e.path),{waitUntil:"load",timeout:6e4});if(e.readySelector)await b.waitForSelector(e.readySelector,{state:"attached",timeout:6e4});const f=Date.now();for(const w of t){const g=f+w-Date.now();if(g>0)await b.waitForTimeout(g);const m=Date.now()-f;const p=sc(i,`${String(d+1).padStart(2,"0")}-${Qf(u.name)}-${w}ms.png`);try{if(e.clipSelector){const y=await b.$(e.clipSelector);if(!y)throw new Error(`clipSelector ${e.clipSelector} not found`);await y.screenshot({path:p})}else await b.screenshot({path:p});o.files.push({caseName:u.name,atMs:w,actualMs:m,file:p})}catch(y){o.files.push({caseName:u.name,atMs:w,actualMs:m,file:null,error:(y?.message??String(y)).split("\n")[0]})}}n(`${u.name}: ${t.length} shot(s)`)}catch(b){for(const f of t)o.files.push({caseName:u.name,atMs:f,file:null,error:(b?.message??String(b)).split("\n")[0]})}finally{if(h&&!await Mt(h,1e4))s=true}}if(e.cases.length>1){let d;try{d=await l.newContext({viewport:{width:400,height:300}});const u=await d.newPage();const h=e.cases[0].name;for(const m of t){const p=o.files.find(v=>v.caseName===h&&v.atMs===m&&v.file)?.file;if(!p)continue;const y=ac(p).toString("base64");for(const v of o.files.filter($=>$.caseName!==h&&$.atMs===m&&$.file)){const $=await u.evaluate(tm(y,ac(v.file).toString("base64")));if($)o.diffs.push({caseName:v.caseName,atMs:m,...$})}}const b=new Map;for(const m of o.diffs)b.set(m.caseName,[...b.get(m.caseName)??[],m.changedPct]);const f=[...b].map(([m,p])=>[m,p.reduce((y,v)=>y+v,0)/p.length]);const w=f.map(([,m])=>m).sort((m,p)=>m-p);const g=w.length?w[Math.floor(w.length/2)]:0;o.outliers=f.filter(([,m])=>m>Math.max(1,g*2)).map(([m])=>m)}catch{}finally{if(d)await Mt(d,1e4)}}}finally{if(!await Mt(l,1e4))s=true}}finally{a(s)}return o}var tm=(e,n)=>`(async () => {
764
+ })`;async function fm(e,n){const t=[];const i=(l,d,u,h)=>({status:l,stopReason:d,highestPass:u?{load:u.load,fps:u.fps??0}:null,lowestMiss:h?{load:h.load,fps:h.fps??0}:null,resolved:l==="below_one"||l==="bracketed"&&!!u&&!!h&&mc(u.load,h.load,n.precisionPct),steps:t});const r=(l,d,u)=>i("invalid",l.fps==null?"measurement_failed":"page_error",d,u);const o=async(l,d=false)=>{const u=await e(l);if(d)u.confirm=true;t.push(u);return u};let a=null;let s=null;let c=await o(n.start);if(!c.valid)return r(c,null,null);if(c.pass)a=c;else{s=c;let l=n.start;while(!a){if(l<=1)return i("below_one","performance",null,s);if(!n.timeLeft())return i("lower_bound","deadline",null,s);l=Math.max(1,Math.floor(l/2));c=await o(l);if(!c.valid)return r(c,null,s);if(c.pass)a=c;else s=c}}while(!s||s.load<=a.load){if(a.load>=n.max)return i("lower_bound","max",a,null);if(!n.timeLeft())return i("lower_bound","deadline",a,null);c=await o(Math.min(n.max,hm(a.load,n.growth)));if(!c.valid)return r(c,a,null);if(c.pass){a=c;continue}if(!n.timeLeft())return i("bracketed","performance",a,c);const l=await o(c.load,true);if(!l.valid)return r(l,a,null);if(l.pass)a=l;else s=l}while(!mc(a.load,s.load,n.precisionPct)&&n.timeLeft()){const l=Math.round((a.load+s.load)/2);if(l<=a.load||l>=s.load)break;c=await o(l);if(!c.valid)break;if(c.pass)a=c;else s=c}return i("bracketed","performance",a,s)}async function mm(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.param)throw new Error('Pass `param`, the query param that sets the load (e.g. "lights").');const t=(v,$)=>{if(v!=null&&!Number.isFinite(v))throw new Error(`${$} must be a finite number.`)};for(const[v,$]of Object.entries({start:e.start,growth:e.growth,max:e.max,targetFps:e.targetFps,precisionPct:e.precisionPct,rounds:e.rounds,settleMs:e.settleMs,measureMs:e.measureMs,cpuSlowdown:e.cpuSlowdown}))t($,v);const i=Math.max(1,Math.floor(e.start??100));const r=Math.min(4,Math.max(1.05,e.growth??1.3));const o=Math.max(i,Math.floor(e.max??1e9));const a=e.targetFps??55;const s=Math.min(50,Math.max(.5,e.precisionPct??4));const c=Math.max(1,Math.min(5,Math.floor(e.rounds??1)));const l=Math.max(0,e.settleMs??1e3);const d=Math.max(500,e.measureMs??2e3);const u=Math.max(1,e.cpuSlowdown??1);const h={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const b=[];const f=Date.now();const w=f+(e.maxBatchMs??40*6e4);const g=()=>w-Date.now();const m=f>>>0;const p=await fo(Math.min(e.lockWaitMs??20*6e4,Math.max(0,g())),n);let y=false;try{const v=await po(e.projectRoot??process.cwd());const $=await v.launch({headless:!e.headed,args:["--enable-gpu","--disable-backgrounding-occluded-windows","--disable-renderer-backgrounding","--disable-background-timer-throttling"]});try{const T={viewport:{width:h.width,height:h.height},deviceScaleFactor:h.deviceScaleFactor};let k=null;let D=60;{const _=await $.newContext(T);try{const S=await _.newPage();await S.goto("about:blank");k=await S.evaluate(yl)??null;const A=await mo(S.evaluate(wc(200,1e3)),1e4,async()=>({fps:60}));D=Sl(A.fps)}finally{await Ot(_,5e3)}}n(`GPU ${k??"unknown"} \xB7 idle ${D} Hz`);if(k&&ho(k))b.push(`Chrome is drawing with a software GPU (${k}), so ceilings are far lower than on a real screen.`);if(a>D*.95){b.push(`The target (${a} fps) is at or above this browser's idle frame rate (${D} Hz), so even an empty page can miss it. Use headed: true on a faster screen, or measure with a target your contract allows.`)}const C=e.cases.map(_=>({caseName:_.name,ceiling:null,atLeast:null,rounds:[],errors:[]}));const I=async(_,S,A,O)=>{const F=O.length;const P=uo(e.url,{...S,params:{...S.params??{},[e.param]:String(A)}},e.path);const K=N=>Math.max(1e3,Math.min(N,g()+15e3));try{await _.goto(P,{waitUntil:"load",timeout:K(6e4)});if(e.readySelector)await _.waitForSelector(e.readySelector,{state:"attached",timeout:K(6e4)});const N=await mo(_.evaluate(wc(l,d)),l+d+6e4,async()=>{throw new Error("the page stopped drawing (no frames for 60 s)")});const q=O.length>F?O[O.length-1]:void 0;if(q)return{load:A,fps:N.fps,p95GapMs:N.p95,valid:false,pass:false,error:q};return{load:A,fps:N.fps,p95GapMs:N.p95,valid:true,pass:N.fps>=a}}catch(N){return{load:A,fps:null,p95GapMs:null,valid:false,pass:false,error:(N?.message??String(N)).split("\n")[0].slice(0,200)}}};let R=false;for(let _=0;_<c&&!R;_++){const S=_l(e.cases.map((A,O)=>({c:A,i:O})),m+_*7919>>>0);for(const{c:A,i:O}of S){const F=C[O];if(R||g()<=0){F.rounds.push({round:_+1,status:"skipped",stopReason:"not_run",highestPass:null,lowestMiss:null,resolved:false,steps:[]});continue}n(`round ${_+1}/${c} \xB7 ${A.name}`);let P;try{P=await $.newContext(T);const K=await P.newPage();K.on("pageerror",q=>F.errors.push(String(q?.message??q).slice(0,200)));if(u>1){const q=await P.newCDPSession(K);await q.send("Emulation.setCPUThrottlingRate",{rate:u})}const N=await fm(q=>I(K,A,q,F.errors),{start:i,growth:r,max:o,precisionPct:s,timeLeft:()=>g()>0});F.rounds.push({round:_+1,...N});n(` ${A.name}: ${N.status} ${N.highestPass?.load??"-"}`)}catch(K){F.errors.push((K?.message??String(K)).split("\n")[0].slice(0,200));F.rounds.push({round:_+1,status:"invalid",stopReason:"measurement_failed",highestPass:null,lowestMiss:null,resolved:false,steps:[]})}finally{if(P&&!await Ot(P,1e4)){y=true;R=true;b.push(`${A.name}'s browser context didn't close, so the batch stopped to keep later runs clean.`)}}}}for(const _ of C){const S=_.rounds.filter(O=>O.status==="bracketed"||O.status==="below_one");_.ceiling=pm(S.map(O=>O.highestPass?.load??0));const A=_.rounds.filter(O=>O.status==="lower_bound"&&O.highestPass).map(O=>O.highestPass.load);_.atLeast=A.length?Math.max(...A):null}if(g()<=0)b.push("The batch hit its time limit. Rounds marked skipped or lower_bound didn't finish.");return{url:e.url,browser:`Chromium ${$.version()}${e.headed?"":" (headless)"}`,gpu:k,refreshHz:D,targetFps:a,cpuSlowdown:u,viewport:h,seed:m,results:C,warnings:b}}finally{if(!await Ot($,1e4))y=true}}finally{p(y)}}function wm(e){const n=o=>o==null?"\u2014":o.toLocaleString("en-US");const t=o=>o?`${n(o.load)} @ ${o.fps.toFixed(1)}`:"\u2014";const i=[];i.push(`### Web ceiling \xB7 ${e.url}`);i.push(`${e.browser}${e.gpu?` \xB7 GPU: ${e.gpu}${ho(e.gpu)?" \u26A0\uFE0F software":""}`:""} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 idle ${e.refreshHz} Hz \xB7 target ${e.targetFps} fps${e.cpuSlowdown>1?` \xB7 CPU slowed ${e.cpuSlowdown}x`:""}`);i.push("");i.push("**Ceiling** is the most load that held the target, timed from main-thread `requestAnimationFrame` callbacks (a stand-in for drawing speed; it can't see a canvas that stopped updating, so check the looks too). Each round ramps ~30% a step, confirms the first miss, then bisects. With several rounds the ceiling is their median. Ceilings within about 10% are a tie until more rounds agree.");i.push("");const r=Math.max(0,...e.results.map(o=>o.ceiling??0));i.push("| Case | Ceiling | Each round: highest pass @ fps \u2192 lowest miss @ fps | Notes |");i.push("|------|--------:|------------------------------------------------------|-------|");for(const o of e.results){const a=o.rounds.map(c=>{if(c.status==="skipped")return`r${c.round}: not run`;if(c.status==="invalid")return`r${c.round}: invalid (${c.stopReason}) after ${t(c.highestPass)}`;if(c.status==="lower_bound")return`r${c.round}: at least ${t(c.highestPass)} (${c.stopReason})`;if(c.status==="below_one")return`r${c.round}: even 1 missed (${t(c.lowestMiss)})`;return`r${c.round}: ${t(c.highestPass)} \u2192 ${t(c.lowestMiss)}${c.resolved?"":" (wide)"}`}).join("<br>");const s=[o.ceiling===r&&r>0?"best":"",o.atLeast!=null?`at least ${n(o.atLeast)}`:"",o.rounds.some(c=>c.status==="invalid")?"a round broke: check the page before trusting it":"",o.errors.length?`${o.errors.length} page error(s): ${o.errors[0]}`:""].filter(Boolean);i.push(`| ${o.caseName} | ${n(o.ceiling)} | ${a} | ${s.join("; ")} |`)}if(e.warnings.length){i.push("");for(const o of e.warnings)i.push(`- \u26A0\uFE0F ${o}`)}return i.join("\n")}import{mkdirSync as gm,readFileSync as gc}from"fs";import{join as bc}from"path";var bm=e=>e.replace(/[^\w.-]+/g,"_").slice(0,80);async function vm(e,n=()=>{}){if(!e.cases?.length)throw new Error("Pass at least one case.");if(!e.outDir)throw new Error("Pass outDir, the folder to save screenshots in.");const t=[...new Set((e.atMs?.length?e.atMs:[500,2e3]).map(c=>Math.max(0,Math.round(c))))].sort((c,l)=>c-l);const i=bc(e.outDir,new Date().toISOString().replace(/[:.]/g,"-"));gm(i,{recursive:true});const r={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const o={dir:"",files:[],pageErrors:[],diffs:[],outliers:[]};o.dir=i;const a=await fo(e.lockWaitMs??20*6e4,n);let s=false;try{const c=await po(e.projectRoot??process.cwd());const l=await c.launch({headless:true,args:["--enable-gpu"]});try{for(const[d,u]of e.cases.entries()){let h;try{h=await l.newContext({viewport:{width:r.width,height:r.height},deviceScaleFactor:r.deviceScaleFactor});const b=await h.newPage();b.on("pageerror",w=>o.pageErrors.push({caseName:u.name,error:String(w?.message??w).slice(0,200)}));await b.goto(uo(e.url,u,e.path),{waitUntil:"load",timeout:6e4});if(e.readySelector)await b.waitForSelector(e.readySelector,{state:"attached",timeout:6e4});const f=Date.now();for(const w of t){const g=f+w-Date.now();if(g>0)await b.waitForTimeout(g);const m=Date.now()-f;const p=bc(i,`${String(d+1).padStart(2,"0")}-${bm(u.name)}-${w}ms.png`);try{if(e.clipSelector){const y=await b.$(e.clipSelector);if(!y)throw new Error(`clipSelector ${e.clipSelector} not found`);await y.screenshot({path:p})}else await b.screenshot({path:p});o.files.push({caseName:u.name,atMs:w,actualMs:m,file:p})}catch(y){o.files.push({caseName:u.name,atMs:w,actualMs:m,file:null,error:(y?.message??String(y)).split("\n")[0]})}}n(`${u.name}: ${t.length} shot(s)`)}catch(b){for(const f of t)o.files.push({caseName:u.name,atMs:f,file:null,error:(b?.message??String(b)).split("\n")[0]})}finally{if(h&&!await Ot(h,1e4))s=true}}if(e.cases.length>1){let d;try{d=await l.newContext({viewport:{width:400,height:300}});const u=await d.newPage();const h=e.cases[0].name;for(const m of t){const p=o.files.find(v=>v.caseName===h&&v.atMs===m&&v.file)?.file;if(!p)continue;const y=gc(p).toString("base64");for(const v of o.files.filter($=>$.caseName!==h&&$.atMs===m&&$.file)){const $=await u.evaluate(ym(y,gc(v.file).toString("base64")));if($)o.diffs.push({caseName:v.caseName,atMs:m,...$})}}const b=new Map;for(const m of o.diffs)b.set(m.caseName,[...b.get(m.caseName)??[],m.changedPct]);const f=[...b].map(([m,p])=>[m,p.reduce((y,v)=>y+v,0)/p.length]);const w=f.map(([,m])=>m).sort((m,p)=>m-p);const g=w.length?w[Math.floor(w.length/2)]:0;o.outliers=f.filter(([,m])=>m>Math.max(1,g*2)).map(([m])=>m)}catch{}finally{if(d)await Ot(d,1e4)}}}finally{if(!await Ot(l,1e4))s=true}}finally{a(s)}return o}var ym=(e,n)=>`(async () => {
765
765
  const load = (s) => new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = "data:image/png;base64," + s; });
766
766
  const [ia, ib] = await Promise.all([load(${JSON.stringify(e)}), load(${JSON.stringify(n)})]);
767
767
  const w = ia.naturalWidth, h = ia.naturalHeight;
@@ -775,76 +775,76 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
775
775
  }
776
776
  const n = da.length / 4;
777
777
  return { meanDiff: Math.round((sum / n) * 100) / 100, changedPct: Math.round((changed / n) * 10000) / 100 };
778
- })()`;var cc=null;function lc(e){cc=e}var qr=e=>new Date(e).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function dc(e=Date.now(),n=cc){if(n==null)return null;const t=Math.round((n-e)/6e4);if(t>10){return`
778
+ })()`;var vc=null;function yc(e){vc=e}var Yr=e=>new Date(e).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"});function $c(e=Date.now(),n=vc){if(n==null)return null;const t=Math.round((n-e)/6e4);if(t>10){return`
779
779
 
780
- \u23F1 **Buoy Optimize: ${t} min left** (deadline ${qr(n)}). You're not done. When this step ends, start the next round: forks of the best, a new idea family, or an untried idea from the bank. A good score or a written RESULTS.md is not a reason to stop.`}if(t>0){return`
780
+ \u23F1 **Buoy Optimize: ${t} min left** (deadline ${Yr(n)}). You're not done. When this step ends, start the next round: forks of the best, a new idea family, or an untried idea from the bank. A good score or a written RESULTS.md is not a reason to stop.`}if(t>0){return`
781
781
 
782
- \u23F1 **Buoy Optimize: ${t} min left** (deadline ${qr(n)}). Confirm the best checked version, install it and finish the report.`}return`
782
+ \u23F1 **Buoy Optimize: ${t} min left** (deadline ${Yr(n)}). Confirm the best checked version, install it and finish the report.`}return`
783
783
 
784
- \u23F1 **Buoy Optimize: the deadline (${qr(n)}) has passed.** Leave the best checked version in place and report.`}var _n=e=>({content:[{type:"text",text:e}]});var nt=e=>({isError:true,content:[{type:"text",text:e}]});function nm(e){if(Mn())return true;return e.connectedDevices().some(n=>fe(n))}function im(e,n){e.registerTool("run_web_benchmark",{description:"Benchmark variants of a web page in a fresh headless Chromium (needs Playwright in the project). Each case is a path + query params on a running app; each run loads a fresh page, waits until ready, then records frame rate, frame gaps, long tasks, Long Animation Frames (with script names), layout shifts, DOM size, and Chrome's own main-thread, script, style and layout time. Cases run in rounds with the order shuffled each round. `cpuSlowdown` repeats everything with Chrome's CPU throttled (e.g. [1, 4, 6]) as a stand-in for slower phones; it doesn't slow the GPU. Blocks until done.",inputSchema:{url:C.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:C.string().optional().describe("Default path for cases, e.g. /bench/lights"),cases:C.array(C.object({name:C.string(),path:C.string().optional(),params:C.record(C.string()).optional(),control:C.boolean().optional().describe("Does different work (e.g. an empty page). Shown, never the baseline or a candidate.")})).min(1).describe("Variants to compare. The first is the baseline."),runsPerCase:C.number().optional().describe("Measured runs per case (1\u201310). Default 3."),perCaseDurationMs:C.number().optional().describe("Recording length per run. Default 5000."),settleMs:C.number().optional().describe("Wait after the page is ready, before recording. Default 1000."),cpuSlowdown:C.array(C.number()).optional().describe("CPU throttling rates; 1 = none. Default [1]."),viewport:C.object({width:C.number(),height:C.number(),deviceScaleFactor:C.number().optional()}).optional().describe("Fixed viewport. Default 1280\xD7800 at 1x."),readySelector:C.string().optional().describe("CSS selector that must appear before a run counts as ready."),steps:C.array(C.union([C.object({click:C.string()}),C.object({press:C.string()}),C.object({type:C.object({selector:C.string(),text:C.string()})}),C.object({scroll:C.number()}),C.object({wait:C.number()}),C.object({expect:C.object({selector:C.string(),text:C.string().optional()})})])).optional().describe("Real input during each run, in order: {click: selector}, {press: key}, {type: {selector, text}}, {scroll: px}, {wait: ms}, {expect: {selector, text?}} (checked after the run; fails runs that skipped the work). Click targets are located before recording and must not move. Adds input delay, handler time and time to next paint per case."),traceDir:C.string().optional().describe("Save one Chrome trace per case here (extra, unranked runs). Needs Buoy Pro."),headed:C.boolean().optional().describe("Show the browser window. Default false."),allowPageErrors:C.boolean().optional().describe("Keep runs whose page threw an uncaught error. Default false: such runs fail."),projectRoot:C.string().optional().describe("Folder to load Playwright from. Default: the server's working directory."),outFile:C.string().optional().describe("Also save the full result and options as JSON here, to read again later."),extraRoundsWhenClose:C.number().optional().describe("If the two smoothest cases are too close to call, run up to this many extra rounds of just those two. Default 0."),maxBatchMs:C.number().optional().describe("Stop starting new runs after this long. Default 30 minutes."),budgets:C.object({droppedPct:C.number().optional(),inputMs:C.number().optional(),freezeMs:C.number().optional(),mainThreadPct:C.number().optional(),jsHeapMb:C.number().optional()}).optional().describe("Hard limits. The report says which cases meet all of them; an unmeasured limit counts as not met.")}},async t=>{if(!Mi(n))return nt(Ii("run_web_benchmark"));let i=En();const r=nm(n)?void 0:o=>{const a=nl(o);i=a.locked;return a.result};try{const o=await ol({url:t.url,path:t.path,cases:t.cases,runsPerCase:t.runsPerCase,durationMs:t.perCaseDurationMs,settleMs:t.settleMs,cpuSlowdown:t.cpuSlowdown,viewport:t.viewport,readySelector:t.readySelector,steps:t.steps,traceDir:t.traceDir,headed:t.headed,allowPageErrors:t.allowPageErrors,projectRoot:t.projectRoot,outFile:t.outFile,extraRoundsWhenClose:t.extraRoundsWhenClose,maxBatchMs:t.maxBatchMs,budgets:t.budgets,redact:r},c=>process.stderr.write(`[buoy-mcp] web-bench: ${c}
785
- `));const a={status:o.status,browser:o.browser,gpu:o.gpu??null,viewport:o.viewport,results:o.results.map(c=>({caseName:c.caseName,cpu:c.cpu,validRuns:c.validRuns,fpsMin:c.fpsMin,fpsMax:c.fpsMax,plannedRuns:c.plannedRuns,isBaseline:c.isBaseline,control:c.control===true,deltaDroppedPct:c.deltaDroppedPct,droppedRange:c.droppedMin==null?null:[c.droppedMin,c.droppedMax],flags:c.flags,budget:c.budget,median:c.median,failures:c.runs.filter(l=>l.failed).map(l=>l.failed)})),traces:o.traces,warnings:o.warnings,replay:o.replay};const s=`${rl(o)}
784
+ \u23F1 **Buoy Optimize: the deadline (${Yr(n)}) has passed.** Leave the best checked version in place and report.`}var Cn=e=>({content:[{type:"text",text:e}]});var rt=e=>({isError:true,content:[{type:"text",text:e}]});function $m(e){if(jn())return true;return e.connectedDevices().some(n=>fe(n))}function km(e,n){e.registerTool("run_web_benchmark",{description:"Benchmark variants of a web page in a fresh headless Chromium (needs Playwright in the project). Each case is a path + query params on a running app; each run loads a fresh page, waits until ready, then records frame rate, frame gaps, long tasks, Long Animation Frames (with script names), layout shifts, DOM size, and Chrome's own main-thread, script, style and layout time. Cases run in rounds with the order shuffled each round. `cpuSlowdown` repeats everything with Chrome's CPU throttled (e.g. [1, 4, 6]) as a stand-in for slower phones; it doesn't slow the GPU. Blocks until done.",inputSchema:{url:E.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:E.string().optional().describe("Default path for cases, e.g. /bench/lights"),cases:E.array(E.object({name:E.string(),path:E.string().optional(),params:E.record(E.string()).optional(),control:E.boolean().optional().describe("Does different work (e.g. an empty page). Shown, never the baseline or a candidate.")})).min(1).describe("Variants to compare. The first is the baseline."),runsPerCase:E.number().optional().describe("Measured runs per case (1\u201310). Default 3."),perCaseDurationMs:E.number().optional().describe("Recording length per run. Default 5000."),settleMs:E.number().optional().describe("Wait after the page is ready, before recording. Default 1000."),cpuSlowdown:E.array(E.number()).optional().describe("CPU throttling rates; 1 = none. Default [1]."),viewport:E.object({width:E.number(),height:E.number(),deviceScaleFactor:E.number().optional()}).optional().describe("Fixed viewport. Default 1280\xD7800 at 1x."),readySelector:E.string().optional().describe("CSS selector that must appear before a run counts as ready."),steps:E.array(E.union([E.object({click:E.string()}),E.object({press:E.string()}),E.object({type:E.object({selector:E.string(),text:E.string()})}),E.object({scroll:E.number()}),E.object({wait:E.number()}),E.object({expect:E.object({selector:E.string(),text:E.string().optional()})})])).optional().describe("Real input during each run, in order: {click: selector}, {press: key}, {type: {selector, text}}, {scroll: px}, {wait: ms}, {expect: {selector, text?}} (checked after the run; fails runs that skipped the work). Click targets are located before recording and must not move. Adds input delay, handler time and time to next paint per case."),traceDir:E.string().optional().describe("Save one Chrome trace per case here (extra, unranked runs). Needs Buoy Pro."),headed:E.boolean().optional().describe("Show the browser window. Default false."),allowPageErrors:E.boolean().optional().describe("Keep runs whose page threw an uncaught error. Default false: such runs fail."),projectRoot:E.string().optional().describe("Folder to load Playwright from. Default: the server's working directory."),outFile:E.string().optional().describe("Also save the full result and options as JSON here, to read again later."),extraRoundsWhenClose:E.number().optional().describe("If the two smoothest cases are too close to call, run up to this many extra rounds of just those two. Default 0."),maxBatchMs:E.number().optional().describe("Stop starting new runs after this long. Default 30 minutes."),budgets:E.object({droppedPct:E.number().optional(),inputMs:E.number().optional(),freezeMs:E.number().optional(),mainThreadPct:E.number().optional(),jsHeapMb:E.number().optional()}).optional().describe("Hard limits. The report says which cases meet all of them; an unmeasured limit counts as not met.")}},async t=>{if(!Bn(n))return rt(Fn("run_web_benchmark"));let i=qn();const r=$m(n)?void 0:o=>{const a=vl(o);i=a.locked;return a.result};try{const o=await kl({url:t.url,path:t.path,cases:t.cases,runsPerCase:t.runsPerCase,durationMs:t.perCaseDurationMs,settleMs:t.settleMs,cpuSlowdown:t.cpuSlowdown,viewport:t.viewport,readySelector:t.readySelector,steps:t.steps,traceDir:t.traceDir,headed:t.headed,allowPageErrors:t.allowPageErrors,projectRoot:t.projectRoot,outFile:t.outFile,extraRoundsWhenClose:t.extraRoundsWhenClose,maxBatchMs:t.maxBatchMs,budgets:t.budgets,redact:r},c=>process.stderr.write(`[buoy-mcp] web-bench: ${c}
785
+ `));const a={status:o.status,browser:o.browser,gpu:o.gpu??null,viewport:o.viewport,results:o.results.map(c=>({caseName:c.caseName,cpu:c.cpu,validRuns:c.validRuns,fpsMin:c.fpsMin,fpsMax:c.fpsMax,plannedRuns:c.plannedRuns,isBaseline:c.isBaseline,control:c.control===true,deltaDroppedPct:c.deltaDroppedPct,droppedRange:c.droppedMin==null?null:[c.droppedMin,c.droppedMax],flags:c.flags,budget:c.budget,median:c.median,failures:c.runs.filter(l=>l.failed).map(l=>l.failed)})),traces:o.traces,warnings:o.warnings,replay:o.replay};const s=`${$l(o)}
786
786
 
787
787
  \`\`\`json
788
788
  ${JSON.stringify(a,null,2)}
789
- \`\`\``;return o.status==="failed"?nt(`Every case failed.
789
+ \`\`\``;return o.status==="failed"?rt(`Every case failed.
790
790
 
791
- ${s}`):_n(It(s,i))}catch(o){return nt(o?.message??String(o))}});e.registerTool("find_web_ceiling",{description:"Find the most load each web variant can hold at a target frame rate, with no cap. For each case, a fresh headless Chromium (real GPU) opens the page with `param` set (e.g. ?lights=100), waits until ready, settles, then counts requestAnimationFrame callbacks. It raises the load ~30% a step until a valid sample misses `targetFps`, confirms the miss, then bisects to within a few percent; if the first load misses it searches down. A page error ends the round as invalid instead of counting as a miss. An app's own capped test is not the ceiling. Needs Playwright in the project. Blocks until done.",inputSchema:{url:C.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:C.string().optional().describe("Default path for cases."),cases:C.array(C.object({name:C.string(),path:C.string().optional(),params:C.record(C.string()).optional()})).min(1).describe("Variants to ramp, e.g. [{name: 'baseline', params: {variant: 'baseline'}}, {name: 'atlas', params: {variant: 'atlas'}}]."),param:C.string().describe("Query param that sets the load, e.g. 'lights' or 'count'."),start:C.number().optional().describe("First load. Default 100."),growth:C.number().optional().describe("Growth per step. Default 1.3."),max:C.number().optional().describe("Highest load to try. Default 1,000,000,000."),targetFps:C.number().optional().describe("Frame rate each step must hold. Default 55."),precisionPct:C.number().optional().describe("Bisect until the pass/miss gap is within this percent. Default 4."),rounds:C.number().optional().describe("Ramps per case (1\u20135); the ceiling is the median of valid rounds. Default 1. Use 3 to settle close calls."),maxBatchMs:C.number().optional().describe("Stop the whole batch after this long. Default 40 minutes."),settleMs:C.number().optional().describe("Wait after ready before counting frames. Default 1000."),measureMs:C.number().optional().describe("How long to count frames per step. Default 2000."),cpuSlowdown:C.number().optional().describe("CPU throttling rate; 1 = none. Default 1."),viewport:C.object({width:C.number(),height:C.number(),deviceScaleFactor:C.number().optional()}).optional(),readySelector:C.string().optional().describe("CSS selector that must exist before counting frames."),headed:C.boolean().optional().describe("Show the browser window (uses the screen's refresh rate). Default false."),projectRoot:C.string().optional().describe("Folder to load Playwright from.")}},async t=>{if(!Mi(n))return nt(Ii("find_web_ceiling"));try{const i=await Yf(t,a=>process.stderr.write(`[buoy-mcp] web-ceiling: ${a}
792
- `));const r=i.results.map(a=>({caseName:a.caseName,ceiling:a.ceiling,atLeast:a.atLeast,rounds:a.rounds.map(s=>({status:s.status,stopReason:s.stopReason,highestPass:s.highestPass,lowestMiss:s.lowestMiss,resolved:s.resolved})),errors:a.errors.slice(0,3)}));const o=`${Xf(i)}
791
+ ${s}`):Cn(Et(s,i))}catch(o){return rt(o?.message??String(o))}});e.registerTool("find_web_ceiling",{description:"Find the most load each web variant can hold at a target frame rate, with no cap. For each case, a fresh headless Chromium (real GPU) opens the page with `param` set (e.g. ?lights=100), waits until ready, settles, then counts requestAnimationFrame callbacks. It raises the load ~30% a step until a valid sample misses `targetFps`, confirms the miss, then bisects to within a few percent; if the first load misses it searches down. A page error ends the round as invalid instead of counting as a miss. An app's own capped test is not the ceiling. Needs Playwright in the project. Blocks until done.",inputSchema:{url:E.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:E.string().optional().describe("Default path for cases."),cases:E.array(E.object({name:E.string(),path:E.string().optional(),params:E.record(E.string()).optional()})).min(1).describe("Variants to ramp, e.g. [{name: 'baseline', params: {variant: 'baseline'}}, {name: 'atlas', params: {variant: 'atlas'}}]."),param:E.string().describe("Query param that sets the load, e.g. 'lights' or 'count'."),start:E.number().optional().describe("First load. Default 100."),growth:E.number().optional().describe("Growth per step. Default 1.3."),max:E.number().optional().describe("Highest load to try. Default 1,000,000,000."),targetFps:E.number().optional().describe("Frame rate each step must hold. Default 55."),precisionPct:E.number().optional().describe("Bisect until the pass/miss gap is within this percent. Default 4."),rounds:E.number().optional().describe("Ramps per case (1\u20135); the ceiling is the median of valid rounds. Default 1. Use 3 to settle close calls."),maxBatchMs:E.number().optional().describe("Stop the whole batch after this long. Default 40 minutes."),settleMs:E.number().optional().describe("Wait after ready before counting frames. Default 1000."),measureMs:E.number().optional().describe("How long to count frames per step. Default 2000."),cpuSlowdown:E.number().optional().describe("CPU throttling rate; 1 = none. Default 1."),viewport:E.object({width:E.number(),height:E.number(),deviceScaleFactor:E.number().optional()}).optional(),readySelector:E.string().optional().describe("CSS selector that must exist before counting frames."),headed:E.boolean().optional().describe("Show the browser window (uses the screen's refresh rate). Default false."),projectRoot:E.string().optional().describe("Folder to load Playwright from.")}},async t=>{if(!Bn(n))return rt(Fn("find_web_ceiling"));try{const i=await mm(t,a=>process.stderr.write(`[buoy-mcp] web-ceiling: ${a}
792
+ `));const r=i.results.map(a=>({caseName:a.caseName,ceiling:a.ceiling,atLeast:a.atLeast,rounds:a.rounds.map(s=>({status:s.status,stopReason:s.stopReason,highestPass:s.highestPass,lowestMiss:s.lowestMiss,resolved:s.resolved})),errors:a.errors.slice(0,3)}));const o=`${wm(i)}
793
793
 
794
794
  \`\`\`json
795
795
  ${JSON.stringify({gpu:i.gpu,refreshHz:i.refreshHz,seed:i.seed,results:r},null,2)}
796
- \`\`\``;return _n(o)}catch(i){return nt(i?.message??String(i))}});e.registerTool("screenshot_web_variants",{description:"Screenshot every web variant at the same moments after it's ready, in a fresh headless Chromium (real GPU), and save PNGs in a new timestamped folder inside outDir. Use it to check each variant still looks right: open the files and compare them side by side with the baseline. Use the same params (e.g. the same count and pattern) for every case. Animated scenes differ by timing, so compare shapes, colors, glow and layout, and take several moments to catch frozen or broken motion.",inputSchema:{url:C.string().describe("Origin of the running app."),path:C.string().optional(),cases:C.array(C.object({name:C.string(),path:C.string().optional(),params:C.record(C.string()).optional()})).min(1),atMs:C.array(C.number()).optional().describe("Times after ready to capture, in ms. Default [500, 2000]."),outDir:C.string().describe("Folder to save the PNGs in."),viewport:C.object({width:C.number(),height:C.number(),deviceScaleFactor:C.number().optional()}).optional(),readySelector:C.string().optional(),clipSelector:C.string().optional().describe("Crop to this element, e.g. 'canvas'."),projectRoot:C.string().optional()}},async t=>{if(!Mi(n))return nt(Ii("screenshot_web_variants"));try{const i=await em(t,c=>process.stderr.write(`[buoy-mcp] web-shots: ${c}
796
+ \`\`\``;return Cn(o)}catch(i){return rt(i?.message??String(i))}});e.registerTool("screenshot_web_variants",{description:"Screenshot every web variant at the same moments after it's ready, in a fresh headless Chromium (real GPU), and save PNGs in a new timestamped folder inside outDir. Use it to check each variant still looks right: open the files and compare them side by side with the baseline. Use the same params (e.g. the same count and pattern) for every case. Animated scenes differ by timing, so compare shapes, colors, glow and layout, and take several moments to catch frozen or broken motion.",inputSchema:{url:E.string().describe("Origin of the running app."),path:E.string().optional(),cases:E.array(E.object({name:E.string(),path:E.string().optional(),params:E.record(E.string()).optional()})).min(1),atMs:E.array(E.number()).optional().describe("Times after ready to capture, in ms. Default [500, 2000]."),outDir:E.string().describe("Folder to save the PNGs in."),viewport:E.object({width:E.number(),height:E.number(),deviceScaleFactor:E.number().optional()}).optional(),readySelector:E.string().optional(),clipSelector:E.string().optional().describe("Crop to this element, e.g. 'canvas'."),projectRoot:E.string().optional()}},async t=>{if(!Bn(n))return rt(Fn("screenshot_web_variants"));try{const i=await vm(t,c=>process.stderr.write(`[buoy-mcp] web-shots: ${c}
797
797
  `));const r=i.files.map(c=>`- ${c.caseName} @ ${c.atMs} ms${c.actualMs!=null&&Math.abs(c.actualMs-c.atMs)>50?` (taken at ${c.actualMs} ms)`:""}: ${c.file??`failed (${c.error})`}`);const o=i.pageErrors.map(c=>`- ${c.caseName}: ${c.error}`);const a=t.cases[0]?.name;const s=i.diffs.length?`
798
798
 
799
799
  **Difference from ${a}** (same moment; mean 0\u2013255, and % of pixels off by more than 32):
800
800
  `+i.diffs.map(c=>`- ${c.caseName} @ ${c.atMs} ms: mean ${c.meanDiff}, ${c.changedPct}% changed${i.outliers.includes(c.caseName)?" \u26A0\uFE0F outlier":""}`).join("\n")+(i.outliers.length?`
801
801
 
802
- \u26A0\uFE0F ${i.outliers.join(", ")} differ from ${a} far more than the other variants. Something may be missing or wrong (lights not drawn, wrong colors, frozen motion). Open those images first.`:""):"";return _n(`### Variant screenshots
802
+ \u26A0\uFE0F ${i.outliers.join(", ")} differ from ${a} far more than the other variants. Something may be missing or wrong (lights not drawn, wrong colors, frozen motion). Open those images first.`:""):"";return Cn(`### Variant screenshots
803
803
 
804
804
  ${r.join("\n")}${o.length?`
805
805
 
806
806
  **Page errors**
807
807
  ${o.join("\n")}`:""}${s}
808
808
 
809
- Open the files and compare each variant with the baseline at the same moment. Put the baseline first in \`cases\` so the difference numbers compare against it. Animation makes some difference normal; a variant far from the rest needs a close look. A variant that looks wrong can't win.`)}catch(i){return nt(i?.message??String(i))}});e.registerTool("start_optimize_session",{description:"Start the Buoy Optimize clock. Pass how long the session lasts (`minutes`) or when it ends (`deadline`, ISO time). After this, every Buoy tool result shows the time left, so the loop keeps going until the deadline. Call it once at the start of an optimize session. Pass minutes: 0 to stop the clock.",inputSchema:{minutes:C.number().optional().describe("Session length from now, in minutes."),deadline:C.string().optional().describe("When the session ends, as an ISO time, e.g. 2026-10-04T07:12:00-06:00.")}},async t=>{if(t.minutes===0){lc(null);return _n("Optimize clock stopped.")}const i=t.deadline?Date.parse(t.deadline):t.minutes!=null?Date.now()+t.minutes*6e4:NaN;if(!Number.isFinite(i))return nt("Pass `minutes` (a number) or `deadline` (an ISO time).");if(i<=Date.now())return nt("That deadline has already passed.");lc(i);return _n(`Optimize clock started. Deadline: ${new Date(i).toISOString()}.${dc()??""}`)})}import{z as Yt}from"zod";import{execFile as rm,spawn as om}from"child_process";import{promisify as am}from"util";import{promises as uc}from"fs";import sm from"os";import cm from"path";import{randomUUID as lm}from"crypto";var dm=am(rm);async function hc(e,n,t=2e4){try{const{stdout:i}=await dm(e,n,{timeout:t});return i}catch(i){const r=i;throw new Error(`${e} ${n.slice(0,3).join(" ")} failed: ${(r.stderr||r.message||"").trim().split("\n").slice(-2).join(" ").slice(0,300)}`)}}var Ur=e=>hc("xcrun",["devicectl",...e,"--quiet"]);var um={portrait:"portrait",landscape_left:"landscapeLeft",landscape_right:"landscapeRight",upside_down:"portraitUpsideDown"};var hm={portrait:0,landscape_left:1,upside_down:2,landscape_right:3};async function pm(e,n){if(e.platform==="android"){await J(e.serial,"settings put system accelerometer_rotation 0");await J(e.serial,`settings put system user_rotation ${hm[n]}`);return`Rotated to ${n.replace("_"," ")} (auto-rotate is now off on this device; rotate to portrait to go back).`}await Ur(["device","orientation","set","--device",e.udid,um[n]]);return`Rotated the device to ${n.replace("_"," ")}. An app that only supports portrait keeps its screen upright.`}async function fm(e){if(e.platform==="ios"){await hc("xcrun",["simctl","spawn",e.udid,"notifyutil","-p","com.apple.UIKit.SimulatorShake"]);return"Sent a shake to the simulator (the same event as Device > Shake). Check the screen: an app only reacts if it listens for shakes."}if(e.platform==="android"){if(!/^emulator-\d+$/.test(e.serial))throw new Error("A physical Android phone can't be shaken from the computer. Use an emulator, or shake the phone.");for(const n of["0:25:0","0:-25:0","0:25:0","0:-25:0","0:9.81:0"]){await Be(e.serial,["emu","sensor","set","acceleration",n]);await new Promise(t=>setTimeout(t,80))}return"Shook the emulator (an accelerometer burst). Check the screen: an app only reacts if it listens for shakes."}throw new Error("A physical iPhone can't be shaken from the computer. Use a simulator, or shake the phone.")}async function mm(e,n){if(e.platform==="ios"){await new Promise((t,i)=>{const r=om("xcrun",["simctl","pbcopy",e.udid],{stdio:["pipe","ignore","pipe"]});let o="";r.stderr.on("data",a=>o+=a);r.on("error",i);r.on("exit",a=>a===0?t():i(new Error(`simctl pbcopy failed: ${o.trim()}`)));r.stdin.end(n)})}else if(e.platform==="ios-device"){const t=cm.join(sm.tmpdir(),`buoy-clip-${lm()}.txt`);await uc.writeFile(t,n,{mode:384});try{await Ur(["device","pasteboard","copy","--device",e.udid,"--type","public.utf8-plain-text","--file",t])}finally{await uc.unlink(t).catch(()=>void 0)}}else{throw new Error("Setting the clipboard from the computer isn't supported on Android. Use native_type to type the text instead.")}return e.platform==="ios-device"?"Put the text on the iPhone's clipboard. To paste it for real, long-press the field (native_tap with holdMs: 600) and tap Paste (native_tap with x and y; labels don't work on a phone).":'Put the text on the clipboard. To paste it for real, long-press the field (native_tap with holdMs: 600) and tap Paste (native_tap label: "Paste").'}async function wm(e,n,t){if(e.platform==="android"){if(!/^emulator-\d+$/.test(e.serial))throw new Error("Biometrics can be simulated on an Android emulator only.");const i=await J(e.serial,"dumpsys fingerprint").catch(()=>"");if(n&&/"count"\s*:\s*0|enrolled[^\n]*\b0\b/i.test(i)){throw new Error("No fingerprint is enrolled on this emulator. Enroll one in Settings > Security, then try again.")}const r=t??(n?1:9731);await Be(e.serial,["emu","finger","touch",String(r)]);return n?`Touched fingerprint id ${r}. It matches only if that finger is enrolled in the emulator's Settings (enroll one there first if the app says no fingerprints).`:`Touched fingerprint id ${r}, meant to fail. It would match instead if id ${r} is enrolled; pass another fingerId if so.`}await Ur(["device","simulate","biometrics","--device",e.udid,n?"--success":"--failure"]);return n?"Simulated a matching Face ID / Touch ID.":"Simulated a Face ID / Touch ID that doesn't match."}function gm(e,n){e.registerTool("device_control",{_meta:He(),description:"Do something to the device itself: rotate (orientation), shake (shake-to-undo, shake-to-report, dev menus), clipboard (text on the clipboard for a real paste), biometrics (Face ID / Touch ID / fingerprint match or fail). iOS Simulator, physical iPhone (rotate, clipboard) and Android (rotate, emulator shake and fingerprint). Returns the screen after when the app is connected (not for clipboard).",inputSchema:{action:Yt.enum(["rotate","shake","clipboard","biometrics"]),orientation:Yt.enum(["portrait","landscape_left","landscape_right","upside_down"]).optional().describe("rotate (required)."),text:Yt.string().optional().describe("clipboard (required). {{secret:NAME}} works here too."),match:Yt.boolean().optional().describe("biometrics: true for a match (default), false for a failed one."),fingerId:Yt.number().int().min(1).optional().describe("biometrics on an Android emulator: the finger id to touch: an enrolled one for a match (default 1), one that is not enrolled for a failure (default 9731)."),serial:Yt.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{const{id:i,host:r,online:o}=await ge(n,t.deviceId,{serial:t.serial},{allowOffline:t.action!=="clipboard"});let a;switch(t.action){case"rotate":if(!t.orientation)return x("rotate requires `orientation`.");a=await pm(r,t.orientation);if(r.platform!=="android")zp(r.udid);break;case"shake":a=await fm(r);break;case"clipboard":if(t.text===void 0)return x("clipboard requires `text`.");a=await mm(r,Bn(t.text));break;case"biometrics":a=await wm(r,t.match!==false,t.fingerId);break}if(t.action!=="rotate"||r.platform!=="ios")await new Promise(c=>setTimeout(c,700));const s=o&&t.action!=="clipboard"?await Re(n,i):"";return M(`${a} (${Rt(r)})${s}`)}catch(i){return x(j(i))}})}import pc from"path";import{fileURLToPath as bm}from"url";import{z as fc}from"zod";function vm(e,n=10){const t=[];for(const i of e.split("\n")){if(t.length>=n)break;const r=mc(i);if(r)t.push(r)}return t}function mc(e){for(const n of e.split("\n")){if(/\b(?:react[-_]stack[-_](?:top|bottom)[-_]frame|jsxDEV(?:Impl)?|jsxWithValidation|createElement)\b/.test(n))continue;const t=n.trim();const i=/:(\d+):(\d+)\)?\s*$/.exec(t);if(!i)continue;const r=t.slice(0,i.index);const o=/\s\((?=https?:\/\/|file:\/\/|address at |\/|[A-Za-z]:[\\/])/.exec(r);let a;let s;if(o){a=r.slice(o.index+2);s=o.index}else{const u=/(?:^at\s+|@)((?:https?:\/\/|file:\/\/|\/|[A-Za-z]:[\\/]).*)$/.exec(r);if(!u)continue;a=u[1];s=u.index}const c=Object.assign([t,a,i[1],i[2]],{index:s});const l=Number(c[2]);const d=Number(c[3]);if(!Number.isSafeInteger(l)||l<1||!Number.isSafeInteger(d))continue;return{file:c[1].replace(/^address at\s+/,""),lineNumber:l,column:d,methodName:t.slice(0,c.index).replace(/^at\s+/,"").replace(/[(@\s]+$/,"")||"x"}}return null}function xt(e){return/^https?:\/\//i.test(e)}function wc(e,n=process.env.BUOY_PROJECT_ROOT||process.cwd()){let t=e;if(t.startsWith("file://")){try{t=bm(t)}catch{return e}}if(xt(t))return t;const i=/^[A-Za-z]:[\\/]/.test(t)&&/^[A-Za-z]:[\\/]/.test(n)?pc.win32:pc;if(!i.isAbsolute(t))return t;const r=i.relative(i.resolve(n),t);return r&&r!==".."&&!r.startsWith(`..${i.sep}`)&&!i.isAbsolute(r)?r.split(i.sep).join("/"):t}async function ym(e,n,t=fetch){const i=await t(`${n.replace(/\/$/,"")}/symbolicate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({stack:[e]}),signal:AbortSignal.timeout(5e3)});if(!i.ok)throw new Error(`Metro could not map this frame (HTTP ${i.status}).`);const r=await i.json();const o=r?.stack?.[0];if(typeof o?.file!=="string"||!o.file||xt(o.file)||typeof o.lineNumber!=="number"||!Number.isInteger(o.lineNumber)||o.lineNumber<1){throw new Error("Metro did not return a source file and line.")}return{fileName:o.file,lineNumber:o.lineNumber,...typeof o.column==="number"&&Number.isInteger(o.column)&&o.column>=0?{columnNumber:o.column}:{}}}var $m=/node_modules\/|\/react-native\/|\/Libraries\/|\/expo-router\//;async function km(e,n,t=fetch){const i=await t(`${n.replace(/\/$/,"")}/symbolicate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({stack:e}),signal:AbortSignal.timeout(5e3)});if(!i.ok){const o=(await i.json().catch(()=>null))?.error;const a=typeof o==="string"&&o?` Metro said: ${o.slice(0,200)}`:"";const s=/serialized source map/i.test(a)?" Its cache looks stale; restarting Metro with --clear usually fixes this.":"";throw new Error(`Metro could not map these frames (HTTP ${i.status}).${a}${s}`)}const r=await i.json();for(const o of r?.stack??[]){if(typeof o?.file!=="string"||!o.file||xt(o.file)||$m.test(o.file))continue;if(typeof o.lineNumber!=="number"||!Number.isInteger(o.lineNumber)||o.lineNumber<1)continue;return{fileName:o.file,lineNumber:o.lineNumber,...typeof o.column==="number"&&o.column>=0?{columnNumber:o.column}:{}}}throw new Error("Every frame mapped into library code, none into the app.")}async function gc(e,n){if(!e.source&&e.rawStack&&n){const i=vm(e.rawStack).filter(r=>xt(r.file));if(i.length){try{return{...e,source:await km(i,n)}}catch(r){const o=/library code/.test(j(r));return{...e,source:void 0,reason:o?"rendered by library code (no line in the app).":`Could not map the source frame: ${j(r)}`}}}}const t=e.source&&xt(e.source.fileName)?{file:e.source.fileName,lineNumber:e.source.lineNumber??0,column:e.source.columnNumber??0,methodName:"x"}:!e.source&&e.rawStack?mc(e.rawStack):null;if(e.source&&!xt(e.source.fileName))return e;if(!t)return{...e,source:void 0,reason:"React gave no source frame. Use a dev build."};if(!xt(t.file)){return{...e,source:{fileName:t.file,lineNumber:t.lineNumber,columnNumber:t.column}}}if(!n)return{...e,source:void 0,reason:"The dev server address is not known."};try{return{...e,source:await ym(t,n)}}catch(i){return{...e,source:void 0,reason:`Could not map the source frame: ${j(i)}`}}}function _m(e,n){if(!e.found)return e.reason;let t="";const i=r=>{const o=r.source;let a;if(o){a=wc(o.fileName,n)+(o.lineNumber!=null?`:${o.lineNumber}`:"")+(o.lineNumber!=null&&o.columnNumber!=null?`:${o.columnNumber}`:"")}else if(r.reason?.startsWith("rendered by library")){a=r.reason}else{const s=r.reason??"React gave no source frame. Use a dev build.";a=s===t?"no source (same reason as above).":`no source. ${s}`;t=s}return`${r.name}: ${a} [memo: ${r.memo?"yes":"no"}; forwardRef: ${r.forwardRef?"yes":"no"}]`};return[i(e.component),...e.owners.slice(0,6).map(r=>` owner ${i(r)}`),"Lines after the name are where each component is used (its JSX call site in the parent's file)."].join("\n")}function Sm(e,n){e.registerTool("get_component_source",{description:"Find where a React view is used in code. Use its name, testID, text, or nativeTag from describe_screen. Get file paths, lines, and up to six owners. See memo and forwardRef flags too. These paths point to JSX calls. Needs a dev build and Buoy Pro.",inputSchema:{query:fc.string().trim().min(1).optional().describe("A component name, testID, or text from describe_screen."),nativeTag:fc.number().int().nonnegative().optional().describe("The nativeTag from describe_screen. Takes priority over query."),deviceId:L}},async({query:t,nativeTag:i,deviceId:r})=>{try{const o=n.requireProDevice(r);if(i==null&&!t?.trim())return x("Pass a query or nativeTag.");const a=await n.callAction("highlight-updates","getComponentSource",{query:t,nativeTag:i},o);if(!a)return x("The app gave no source result. Turn on highlight-updates or update Buoy.");if(!a.found)return M(a.reason);const s=n.devServerOrigin(o);const c=await Promise.all([a.component,...a.owners.slice(0,6)].map(u=>gc(u,s)));const l=a.component.definedIn?await gc({name:a.component.name,memo:false,forwardRef:false,source:a.component.definedIn.source,rawStack:a.component.definedIn.rawStack},s):null;const d=l?.source?`Its own JSX starts in: ${wc(l.source.fileName)}${l.source.lineNumber!=null?` (line ${l.source.lineNumber})`:""} (usually its definition; a helper that builds JSX can show here instead)
810
- `:"";return M(d+_m({found:true,component:c[0],owners:c.slice(1)}))}catch(o){return x(j(o))}})}import{z as zr}from"zod";var Wr="js-top";var xm="\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";function Tm(e){const n=Math.max(1,...e);return e.map(t=>xm[Math.min(7,Math.floor(t/n*7.999))]).join("")}function Dm(e,n){const t=[];t.push(`## JS thread top (${(n/1e3).toFixed(1)}s sample)`);t.push("");t.push(`Thread busy: **${e.busyPct.toFixed(1)}%** of the trailing 5s window.`);if(e.freezeSummary.count>0){t.push(`Freezes (\u226550ms single-task blocks, last 60s): **${e.freezeSummary.count}**, worst **${e.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`)}if(!e.tiers.bridge){t.push("_New Architecture device: touch/React commit work appears as \u201Cunattributed\u201D; timers, rAF and Promise chains are fully attributed._")}t.push("");t.push("| # | task origin | api | 5s ms | % busy | calls | avg ms | max ms | freezes |");t.push("|---|---|---|---|---|---|---|---|---|");e.rows.slice(0,15).forEach((i,r)=>{const o=i.freezes>0?`\u26A0 ${i.freezes} (worst ${i.worstFreezeMs}ms)`:"\u2013";t.push(`| ${r+1} | ${i.label} | ${i.api} | ${i.windowMs.toFixed(1)} | ${i.pctOfBusy.toFixed(1)} | ${i.calls} | ${i.avgMs.toFixed(2)} | ${i.maxMs.toFixed(1)} | ${o} |`)});t.push("");t.push("_Drill into any row with `get_js_thread_origin_detail` using its `key` from the JSON below._");return t.join("\n")}function Im(e,n){const t=[];t.push(`## JS thread (${(n/1e3).toFixed(1)}s sample)`);t.push("");t.push(`Thread busy: **${e.busyPct.toFixed(1)}%** of the trailing 5s window.`);if(e.freezeSummary.count>0){t.push(`Freezes (\u226550ms single-task blocks, last 60s): **${e.freezeSummary.count}**, worst **${e.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`)}return t.join("\n")}function Mm(e){const n=[];n.push(`## Origin detail \u2014 ${e.label}`);n.push("");n.push(`API: \`${e.api}\` \xB7 key: \`${e.key}\``);n.push("");n.push(`Activity last 5s (250ms buckets): \`${Tm(e.buckets)}\` \u2014 ${e.windowMs.toFixed(1)}ms in window, ${e.pctOfBusy.toFixed(1)}% of busy time.`);n.push("");n.push("| runs | scheduled | avg/run | max/run | total | last run | freezes (60s) | worst freeze |");n.push("|---|---|---|---|---|---|---|---|");n.push(`| ${e.calls} | ${e.scheduleCount} | ${e.avgMs.toFixed(2)}ms | ${e.maxMs.toFixed(1)}ms | ${e.totalMs.toFixed(1)}ms | ${e.lastSeenAgoMs>0?`${Math.round(e.lastSeenAgoMs/1e3)}s ago`:"\u2014"} | ${e.freezes} | ${e.worstFreezeMs>0?`${e.worstFreezeMs}ms`:"\u2014"} |`);if(e.caller||e.file){n.push("");n.push(`Scheduled from: **${e.caller?`${e.caller}()`:"unknown caller"}**${e.file?` \u2014 ${e.file}${e.line!=null?`:${e.line}`:""}`:""}`);if(e.frames?.length){n.push(`Raw frames (Hermes bundle offsets): ${e.frames.map(t=>`${t.file}:${t.line}:${t.col}`).join(" \xB7 ")}`)}}else if(e.api!=="unattributed"&&e.api!=="other"){n.push("");n.push(e.promoted?"_Scheduling stack not captured yet \u2014 the next schedule from this site records it._":"_Not hot enough for stack capture yet (threshold: 25 schedules or 50ms total)._")}return n.join("\n")}function Am(e,n){e.registerTool("get_js_thread_top",{description:"Sample instrumented JavaScript task origins and return available timing, busy percentage and freeze attribution. Coverage depends on installed hooks; unattributed work can remain. Requires @buoy-gg/js-top. On a free account it returns the totals only (busy %, freezes); the table of origins needs Buoy Pro. The call waits for the sample: arrange the interaction during that window using an available concurrent driver or a person, rather than starting it after this call returns. clearFirst resets tracked statistics before sampling. Use get_js_thread_origin_detail for a returned row key and measure_renders for component render diagnostics.",inputSchema:{deviceId:L,durationMs:zr.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),clearFirst:zr.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")}},async({deviceId:t,durationMs:i,clearFirst:r})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(i??3e3,250),3e4);const s=await n.callAction(Wr,"sample",{durationMs:a,clearFirst:r===true},o);if(!s)return x("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");if(!n.fullResultsFor(o)){const{snap:d,locked:u}=tl(s);const h={busyPct:Math.round(d.busyPct*10)/10,freezeSummary:d.freezeSummary,tiers:d.tiers};return M(It(`${Im(d,a)}
809
+ Open the files and compare each variant with the baseline at the same moment. Put the baseline first in \`cases\` so the difference numbers compare against it. Animation makes some difference normal; a variant far from the rest needs a close look. A variant that looks wrong can't win.`)}catch(i){return rt(i?.message??String(i))}});e.registerTool("start_optimize_session",{description:"Start the Buoy Optimize clock. Pass how long the session lasts (`minutes`) or when it ends (`deadline`, ISO time). After this, every Buoy tool result shows the time left, so the loop keeps going until the deadline. Call it once at the start of an optimize session. Pass minutes: 0 to stop the clock.",inputSchema:{minutes:E.number().optional().describe("Session length from now, in minutes."),deadline:E.string().optional().describe("When the session ends, as an ISO time, e.g. 2026-10-04T07:12:00-06:00.")}},async t=>{if(t.minutes===0){yc(null);return Cn("Optimize clock stopped.")}const i=t.deadline?Date.parse(t.deadline):t.minutes!=null?Date.now()+t.minutes*6e4:NaN;if(!Number.isFinite(i))return rt("Pass `minutes` (a number) or `deadline` (an ISO time).");if(i<=Date.now())return rt("That deadline has already passed.");yc(i);return Cn(`Optimize clock started. Deadline: ${new Date(i).toISOString()}.${$c()??""}`)})}import{z as nn}from"zod";import{execFile as _m,spawn as Sm}from"child_process";import{promisify as xm}from"util";import{promises as kc}from"fs";import Tm from"os";import Dm from"path";import{randomUUID as Im}from"crypto";var Mm=xm(_m);async function _c(e,n,t=2e4){try{const{stdout:i}=await Mm(e,n,{timeout:t});return i}catch(i){const r=i;throw new Error(`${e} ${n.slice(0,3).join(" ")} failed: ${(r.stderr||r.message||"").trim().split("\n").slice(-2).join(" ").slice(0,300)}`)}}var Xr=e=>_c("xcrun",["devicectl",...e,"--quiet"]);var Am={portrait:"portrait",landscape_left:"landscapeLeft",landscape_right:"landscapeRight",upside_down:"portraitUpsideDown"};var Cm={portrait:0,landscape_left:1,upside_down:2,landscape_right:3};async function Em(e,n){if(e.platform==="android"){await J(e.serial,"settings put system accelerometer_rotation 0");await J(e.serial,`settings put system user_rotation ${Cm[n]}`);return`Rotated to ${n.replace("_"," ")} (auto-rotate is now off on this device; rotate to portrait to go back).`}await Xr(["device","orientation","set","--device",e.udid,Am[n]]);return`Rotated the device to ${n.replace("_"," ")}. An app that only supports portrait keeps its screen upright.`}async function Om(e){if(e.platform==="ios"){await _c("xcrun",["simctl","spawn",e.udid,"notifyutil","-p","com.apple.UIKit.SimulatorShake"]);return"Sent a shake to the simulator (the same event as Device > Shake). Check the screen: an app only reacts if it listens for shakes."}if(e.platform==="android"){if(!/^emulator-\d+$/.test(e.serial))throw new Error("A physical Android phone can't be shaken from the computer. Use an emulator, or shake the phone.");for(const n of["0:25:0","0:-25:0","0:25:0","0:-25:0","0:9.81:0"]){await Le(e.serial,["emu","sensor","set","acceleration",n]);await new Promise(t=>setTimeout(t,80))}return"Shook the emulator (an accelerometer burst). Check the screen: an app only reacts if it listens for shakes."}throw new Error("A physical iPhone can't be shaken from the computer. Use a simulator, or shake the phone.")}async function Rm(e,n){if(e.platform==="ios"){await new Promise((t,i)=>{const r=Sm("xcrun",["simctl","pbcopy",e.udid],{stdio:["pipe","ignore","pipe"]});let o="";r.stderr.on("data",a=>o+=a);r.on("error",i);r.on("exit",a=>a===0?t():i(new Error(`simctl pbcopy failed: ${o.trim()}`)));r.stdin.end(n)})}else if(e.platform==="ios-device"){const t=Dm.join(Tm.tmpdir(),`buoy-clip-${Im()}.txt`);await kc.writeFile(t,n,{mode:384});try{await Xr(["device","pasteboard","copy","--device",e.udid,"--type","public.utf8-plain-text","--file",t])}finally{await kc.unlink(t).catch(()=>void 0)}}else{throw new Error("Setting the clipboard from the computer isn't supported on Android. Use native_type to type the text instead.")}return e.platform==="ios-device"?"Put the text on the iPhone's clipboard. To paste it for real, long-press the field (native_tap with holdMs: 600) and tap Paste (native_tap with x and y; labels don't work on a phone).":'Put the text on the clipboard. To paste it for real, long-press the field (native_tap with holdMs: 600) and tap Paste (native_tap label: "Paste").'}async function Pm(e,n,t){if(e.platform==="android"){if(!/^emulator-\d+$/.test(e.serial))throw new Error("Biometrics can be simulated on an Android emulator only.");const i=await J(e.serial,"dumpsys fingerprint").catch(()=>"");if(n&&/"count"\s*:\s*0|enrolled[^\n]*\b0\b/i.test(i)){throw new Error("No fingerprint is enrolled on this emulator. Enroll one in Settings > Security, then try again.")}const r=t??(n?1:9731);await Le(e.serial,["emu","finger","touch",String(r)]);return n?`Touched fingerprint id ${r}. It matches only if that finger is enrolled in the emulator's Settings (enroll one there first if the app says no fingerprints).`:`Touched fingerprint id ${r}, meant to fail. It would match instead if id ${r} is enrolled; pass another fingerId if so.`}await Xr(["device","simulate","biometrics","--device",e.udid,n?"--success":"--failure"]);return n?"Simulated a matching Face ID / Touch ID.":"Simulated a Face ID / Touch ID that doesn't match."}function Nm(e,n){e.registerTool("device_control",{_meta:Ge(),description:"Do something to the device itself: rotate (orientation), shake (shake-to-undo, shake-to-report, dev menus), clipboard (text on the clipboard for a real paste), biometrics (Face ID / Touch ID / fingerprint match or fail). iOS Simulator, physical iPhone (rotate, clipboard) and Android (rotate, emulator shake and fingerprint). Returns the screen after when the app is connected (not for clipboard).",inputSchema:{action:nn.enum(["rotate","shake","clipboard","biometrics"]),orientation:nn.enum(["portrait","landscape_left","landscape_right","upside_down"]).optional().describe("rotate (required)."),text:nn.string().optional().describe("clipboard (required). {{secret:NAME}} works here too."),match:nn.boolean().optional().describe("biometrics: true for a match (default), false for a failed one."),fingerId:nn.number().int().min(1).optional().describe("biometrics on an Android emulator: the finger id to touch: an enrolled one for a match (default 1), one that is not enrolled for a failure (default 9731)."),serial:nn.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:L}},async t=>{try{const{id:i,host:r,online:o}=await ge(n,t.deviceId,{serial:t.serial},{allowOffline:t.action!=="clipboard"});let a;switch(t.action){case"rotate":if(!t.orientation)return x("rotate requires `orientation`.");a=await Em(r,t.orientation);if(r.platform!=="android")sf(r.udid);break;case"shake":a=await Om(r);break;case"clipboard":if(t.text===void 0)return x("clipboard requires `text`.");a=await Rm(r,Kn(t.text));break;case"biometrics":a=await Pm(r,t.match!==false,t.fingerId);break}if(t.action!=="rotate"||r.platform!=="ios")await new Promise(c=>setTimeout(c,700));const s=o&&t.action!=="clipboard"?await Pe(n,i):"";return M(`${a} (${Bt(r)})${s}`)}catch(i){return x(j(i))}})}import Sc from"path";import{fileURLToPath as Fm}from"url";import{z as xc}from"zod";function Bm(e,n=10){const t=[];for(const i of e.split("\n")){if(t.length>=n)break;const r=Tc(i);if(r)t.push(r)}return t}function Tc(e){for(const n of e.split("\n")){if(/\b(?:react[-_]stack[-_](?:top|bottom)[-_]frame|jsxDEV(?:Impl)?|jsxWithValidation|createElement)\b/.test(n))continue;const t=n.trim();const i=/:(\d+):(\d+)\)?\s*$/.exec(t);if(!i)continue;const r=t.slice(0,i.index);const o=/\s\((?=https?:\/\/|file:\/\/|address at |\/|[A-Za-z]:[\\/])/.exec(r);let a;let s;if(o){a=r.slice(o.index+2);s=o.index}else{const u=/(?:^at\s+|@)((?:https?:\/\/|file:\/\/|\/|[A-Za-z]:[\\/]).*)$/.exec(r);if(!u)continue;a=u[1];s=u.index}const c=Object.assign([t,a,i[1],i[2]],{index:s});const l=Number(c[2]);const d=Number(c[3]);if(!Number.isSafeInteger(l)||l<1||!Number.isSafeInteger(d))continue;return{file:c[1].replace(/^address at\s+/,""),lineNumber:l,column:d,methodName:t.slice(0,c.index).replace(/^at\s+/,"").replace(/[(@\s]+$/,"")||"x"}}return null}function It(e){return/^https?:\/\//i.test(e)}function Dc(e,n=process.env.BUOY_PROJECT_ROOT||process.cwd()){let t=e;if(t.startsWith("file://")){try{t=Fm(t)}catch{return e}}if(It(t))return t;const i=/^[A-Za-z]:[\\/]/.test(t)&&/^[A-Za-z]:[\\/]/.test(n)?Sc.win32:Sc;if(!i.isAbsolute(t))return t;const r=i.relative(i.resolve(n),t);return r&&r!==".."&&!r.startsWith(`..${i.sep}`)&&!i.isAbsolute(r)?r.split(i.sep).join("/"):t}async function jm(e,n,t=fetch){const i=await t(`${n.replace(/\/$/,"")}/symbolicate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({stack:[e]}),signal:AbortSignal.timeout(5e3)});if(!i.ok)throw new Error(`Metro could not map this frame (HTTP ${i.status}).`);const r=await i.json();const o=r?.stack?.[0];if(typeof o?.file!=="string"||!o.file||It(o.file)||typeof o.lineNumber!=="number"||!Number.isInteger(o.lineNumber)||o.lineNumber<1){throw new Error("Metro did not return a source file and line.")}return{fileName:o.file,lineNumber:o.lineNumber,...typeof o.column==="number"&&Number.isInteger(o.column)&&o.column>=0?{columnNumber:o.column}:{}}}var Lm=/node_modules\/|\/react-native\/|\/Libraries\/|\/expo-router\//;async function qm(e,n,t=fetch){const i=await t(`${n.replace(/\/$/,"")}/symbolicate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({stack:e}),signal:AbortSignal.timeout(5e3)});if(!i.ok){const o=(await i.json().catch(()=>null))?.error;const a=typeof o==="string"&&o?` Metro said: ${o.slice(0,200)}`:"";const s=/serialized source map/i.test(a)?" Its cache looks stale; restarting Metro with --clear usually fixes this.":"";throw new Error(`Metro could not map these frames (HTTP ${i.status}).${a}${s}`)}const r=await i.json();for(const o of r?.stack??[]){if(typeof o?.file!=="string"||!o.file||It(o.file)||Lm.test(o.file))continue;if(typeof o.lineNumber!=="number"||!Number.isInteger(o.lineNumber)||o.lineNumber<1)continue;return{fileName:o.file,lineNumber:o.lineNumber,...typeof o.column==="number"&&o.column>=0?{columnNumber:o.column}:{}}}throw new Error("Every frame mapped into library code, none into the app.")}async function Ic(e,n){if(!e.source&&e.rawStack&&n){const i=Bm(e.rawStack).filter(r=>It(r.file));if(i.length){try{return{...e,source:await qm(i,n)}}catch(r){const o=/library code/.test(j(r));return{...e,source:void 0,reason:o?"rendered by library code (no line in the app).":`Could not map the source frame: ${j(r)}`}}}}const t=e.source&&It(e.source.fileName)?{file:e.source.fileName,lineNumber:e.source.lineNumber??0,column:e.source.columnNumber??0,methodName:"x"}:!e.source&&e.rawStack?Tc(e.rawStack):null;if(e.source&&!It(e.source.fileName))return e;if(!t)return{...e,source:void 0,reason:"React gave no source frame. Use a dev build."};if(!It(t.file)){return{...e,source:{fileName:t.file,lineNumber:t.lineNumber,columnNumber:t.column}}}if(!n)return{...e,source:void 0,reason:"The dev server address is not known."};try{return{...e,source:await jm(t,n)}}catch(i){return{...e,source:void 0,reason:`Could not map the source frame: ${j(i)}`}}}function Um(e,n){if(!e.found)return e.reason;let t="";const i=r=>{const o=r.source;let a;if(o){a=Dc(o.fileName,n)+(o.lineNumber!=null?`:${o.lineNumber}`:"")+(o.lineNumber!=null&&o.columnNumber!=null?`:${o.columnNumber}`:"")}else if(r.reason?.startsWith("rendered by library")){a=r.reason}else{const s=r.reason??"React gave no source frame. Use a dev build.";a=s===t?"no source (same reason as above).":`no source. ${s}`;t=s}return`${r.name}: ${a} [memo: ${r.memo?"yes":"no"}; forwardRef: ${r.forwardRef?"yes":"no"}]`};return[i(e.component),...e.owners.slice(0,6).map(r=>` owner ${i(r)}`),"Lines after the name are where each component is used (its JSX call site in the parent's file)."].join("\n")}function zm(e,n){e.registerTool("get_component_source",{description:"Find where a React view is used in code. Use its name, testID, text, or nativeTag from describe_screen. Get file paths, lines, and up to six owners. See memo and forwardRef flags too. These paths point to JSX calls. Needs a dev build and Buoy Pro.",inputSchema:{query:xc.string().trim().min(1).optional().describe("A component name, testID, or text from describe_screen."),nativeTag:xc.number().int().nonnegative().optional().describe("The nativeTag from describe_screen. Takes priority over query."),deviceId:L}},async({query:t,nativeTag:i,deviceId:r})=>{try{const o=n.requireProDevice(r);if(i==null&&!t?.trim())return x("Pass a query or nativeTag.");const a=await n.callAction("highlight-updates","getComponentSource",{query:t,nativeTag:i},o);if(!a)return x("The app gave no source result. Turn on highlight-updates or update Buoy.");if(!a.found)return M(a.reason);const s=n.devServerOrigin(o);const c=await Promise.all([a.component,...a.owners.slice(0,6)].map(u=>Ic(u,s)));const l=a.component.definedIn?await Ic({name:a.component.name,memo:false,forwardRef:false,source:a.component.definedIn.source,rawStack:a.component.definedIn.rawStack},s):null;const d=l?.source?`Its own JSX starts in: ${Dc(l.source.fileName)}${l.source.lineNumber!=null?` (line ${l.source.lineNumber})`:""} (usually its definition; a helper that builds JSX can show here instead)
810
+ `:"";return M(d+Um({found:true,component:c[0],owners:c.slice(1)}))}catch(o){return x(j(o))}})}import{z as Zr}from"zod";var Qr="js-top";var Wm="\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";function Hm(e){const n=Math.max(1,...e);return e.map(t=>Wm[Math.min(7,Math.floor(t/n*7.999))]).join("")}function Jm(e,n){const t=[];t.push(`## JS thread top (${(n/1e3).toFixed(1)}s sample)`);t.push("");t.push(`Thread busy: **${e.busyPct.toFixed(1)}%** of the trailing 5s window.`);if(e.freezeSummary.count>0){t.push(`Freezes (\u226550ms single-task blocks, last 60s): **${e.freezeSummary.count}**, worst **${e.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`)}if(!e.tiers.bridge){t.push("_New Architecture device: touch/React commit work appears as \u201Cunattributed\u201D; timers, rAF and Promise chains are fully attributed._")}t.push("");t.push("| # | task origin | api | 5s ms | % busy | calls | avg ms | max ms | freezes |");t.push("|---|---|---|---|---|---|---|---|---|");e.rows.slice(0,15).forEach((i,r)=>{const o=i.freezes>0?`\u26A0 ${i.freezes} (worst ${i.worstFreezeMs}ms)`:"\u2013";t.push(`| ${r+1} | ${i.label} | ${i.api} | ${i.windowMs.toFixed(1)} | ${i.pctOfBusy.toFixed(1)} | ${i.calls} | ${i.avgMs.toFixed(2)} | ${i.maxMs.toFixed(1)} | ${o} |`)});t.push("");t.push("_Drill into any row with `get_js_thread_origin_detail` using its `key` from the JSON below._");return t.join("\n")}function Gm(e,n){const t=[];t.push(`## JS thread (${(n/1e3).toFixed(1)}s sample)`);t.push("");t.push(`Thread busy: **${e.busyPct.toFixed(1)}%** of the trailing 5s window.`);if(e.freezeSummary.count>0){t.push(`Freezes (\u226550ms single-task blocks, last 60s): **${e.freezeSummary.count}**, worst **${e.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`)}return t.join("\n")}function Km(e){const n=[];n.push(`## Origin detail \u2014 ${e.label}`);n.push("");n.push(`API: \`${e.api}\` \xB7 key: \`${e.key}\``);n.push("");n.push(`Activity last 5s (250ms buckets): \`${Hm(e.buckets)}\` \u2014 ${e.windowMs.toFixed(1)}ms in window, ${e.pctOfBusy.toFixed(1)}% of busy time.`);n.push("");n.push("| runs | scheduled | avg/run | max/run | total | last run | freezes (60s) | worst freeze |");n.push("|---|---|---|---|---|---|---|---|");n.push(`| ${e.calls} | ${e.scheduleCount} | ${e.avgMs.toFixed(2)}ms | ${e.maxMs.toFixed(1)}ms | ${e.totalMs.toFixed(1)}ms | ${e.lastSeenAgoMs>0?`${Math.round(e.lastSeenAgoMs/1e3)}s ago`:"\u2014"} | ${e.freezes} | ${e.worstFreezeMs>0?`${e.worstFreezeMs}ms`:"\u2014"} |`);if(e.caller||e.file){n.push("");n.push(`Scheduled from: **${e.caller?`${e.caller}()`:"unknown caller"}**${e.file?` \u2014 ${e.file}${e.line!=null?`:${e.line}`:""}`:""}`);if(e.frames?.length){n.push(`Raw frames (Hermes bundle offsets): ${e.frames.map(t=>`${t.file}:${t.line}:${t.col}`).join(" \xB7 ")}`)}}else if(e.api!=="unattributed"&&e.api!=="other"){n.push("");n.push(e.promoted?"_Scheduling stack not captured yet \u2014 the next schedule from this site records it._":"_Not hot enough for stack capture yet (threshold: 25 schedules or 50ms total)._")}return n.join("\n")}function Vm(e,n){e.registerTool("get_js_thread_top",{description:"Sample instrumented JavaScript task origins and return available timing, busy percentage and freeze attribution. Coverage depends on installed hooks; unattributed work can remain. Requires @buoy-gg/js-top. On a free account it returns the totals only (busy %, freezes); the table of origins needs Buoy Pro. The call waits for the sample: arrange the interaction during that window using an available concurrent driver or a person, rather than starting it after this call returns. clearFirst resets tracked statistics before sampling. Use get_js_thread_origin_detail for a returned row key and measure_renders for component render diagnostics.",inputSchema:{deviceId:L,durationMs:Zr.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),clearFirst:Zr.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")}},async({deviceId:t,durationMs:i,clearFirst:r})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(i??3e3,250),3e4);const s=await n.callAction(Qr,"sample",{durationMs:a,clearFirst:r===true},o);if(!s)return x("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");if(!n.fullResultsFor(o)){const{snap:d,locked:u}=bl(s);const h={busyPct:Math.round(d.busyPct*10)/10,freezeSummary:d.freezeSummary,tiers:d.tiers};return M(Et(`${Gm(d,a)}
811
811
 
812
812
  \`\`\`json
813
813
  ${JSON.stringify(h)}
814
- \`\`\``,u))}const c=Dm(s,a);const l={busyPct:Math.round(s.busyPct*10)/10,freezeSummary:s.freezeSummary,tiers:s.tiers,rows:s.rows.slice(0,15).map(d=>({key:d.key,api:d.api,windowMs:Math.round(d.windowMs*10)/10,pctOfBusy:Math.round(d.pctOfBusy*10)/10,calls:d.calls,avgMs:Math.round(d.avgMs*100)/100,maxMs:Math.round(d.maxMs*10)/10,totalMs:Math.round(d.totalMs),freezes:d.freezes,worstFreezeMs:d.worstFreezeMs}))};return M(`${c}
814
+ \`\`\``,u))}const c=Jm(s,a);const l={busyPct:Math.round(s.busyPct*10)/10,freezeSummary:s.freezeSummary,tiers:s.tiers,rows:s.rows.slice(0,15).map(d=>({key:d.key,api:d.api,windowMs:Math.round(d.windowMs*10)/10,pctOfBusy:Math.round(d.pctOfBusy*10)/10,calls:d.calls,avgMs:Math.round(d.avgMs*100)/100,maxMs:Math.round(d.maxMs*10)/10,totalMs:Math.round(d.totalMs),freezes:d.freezes,worstFreezeMs:d.worstFreezeMs}))};return M(`${c}
815
815
 
816
816
  \`\`\`json
817
817
  ${JSON.stringify(l)}
818
- \`\`\``)}catch(o){return x(j(o))}});e.registerTool("get_js_thread_origin_detail",{description:'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',inputSchema:{deviceId:L,key:zr.string().describe("Origin key from get_js_thread_top's JSON rows.")}},async({deviceId:t,key:i})=>{try{const r=n.requireProDevice(t);const o=await n.callAction(Wr,"getOriginDetail",{key:i},r);if(!o){return x(`Origin "${i}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`)}const a=Mm(o);return M(`${a}
818
+ \`\`\``)}catch(o){return x(j(o))}});e.registerTool("get_js_thread_origin_detail",{description:'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',inputSchema:{deviceId:L,key:Zr.string().describe("Origin key from get_js_thread_top's JSON rows.")}},async({deviceId:t,key:i})=>{try{const r=n.requireProDevice(t);const o=await n.callAction(Qr,"getOriginDetail",{key:i},r);if(!o){return x(`Origin "${i}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`)}const a=Km(o);return M(`${a}
819
819
 
820
820
  \`\`\`json
821
821
  ${JSON.stringify(o)}
822
- \`\`\``)}catch(r){return x(j(r))}});e.registerTool("clear_js_thread_stats",{description:"Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",inputSchema:{deviceId:L}},async({deviceId:t})=>{try{const i=n.requireProDevice(t);await n.callAction(Wr,"clear",{},i);return M("Cleared js-top stats.")}catch(i){return x(j(i))}})}import{z as qe}from"zod";var it="images";function bc(e){if(!e)return"(no source)";const n=e.match(/[?&]unstable_path=([^&?]+)/);const t=n?decodeURIComponent(n[1]):e;const i=t.split("?")[0]??t;const r=i.split("/").filter(Boolean);return r[r.length-1]??t}function Em(e){if(e.oversizeFactor==null)return"\u2013";if(e.oversizeFactor<.9)return`${e.oversizeFactor.toFixed(1)}\xD7 upscaled`;if(e.oversizeFactor<=1.1)return"right-sized";return`${e.oversizeFactor.toFixed(1)}\xD7 oversized (${e.wastedKB}KB wasted)`}function vc(e){if(e.cache)return e.cache;if(e.progressSeen)return"network";return"?"}function yc(e){return`${(e/(1024*1024)).toFixed(1)}MB`}function Cm(e,n){const t=[];const i=e.stats;t.push(`## Images registry (${e.total} records, showing ${Math.min(n,e.records.length)})`);t.push("");t.push(`${i.loaded} loaded \xB7 ${i.errors} failed \xB7 ${i.loading} in flight \xB7 \u2248${yc(i.estDecodedBytes)} decoded`+(i.estWastedBytes>0?` \xB7 **${yc(i.estWastedBytes)} wasted on oversized sources**`:""));const r=e.insights;if(r){const a=[];if(r.duplicates.length>0)a.push(`${r.duplicates.length} duplicate URL(s): ${r.duplicates.slice(0,3).map(s=>`${bc(s.uri)}\xD7${s.count}`).join(", ")}`);if(r.retryStorms.length>0)a.push(`${r.retryStorms.length} retry storm(s): ${r.retryStorms.slice(0,3).map(s=>`#${s.id}(${s.loadCount} loads)`).join(", ")}`);if(r.queueSaturated)a.push("iOS image queue saturated (>4 concurrent loads)");if(r.missingAlt>0)a.push(`${r.missingAlt} image(s) missing alt/accessibilityLabel`);if(r.layoutShifters.length>0)a.push(`${r.layoutShifters.length} layout-shifting image(s): ${r.layoutShifters.slice(0,3).map(s=>`#${s.id}`).join(", ")} \u2014 reserve explicit dimensions`);if(a.length>0){t.push("");t.push(`**Insights:** ${a.join(" \xB7 ")}`)}}const o=e.captureStatus;if(o.rnDecoratorTooLate){t.push("");t.push('\u26A0 RN <Image> capture missed the module-load race \u2014 add `import "@buoy-gg/images/register"` as the FIRST import of the app entry file.')}if(e.globalModes.network!=="normal"||e.globalModes.blank){t.push("");t.push(`\u26A0 Simulation active: network=${e.globalModes.network}${e.globalModes.blank?", blank images ON":""} (set_image_simulation to reset).`)}t.push("");t.push("| id | image | lib | status | cache | ms | decoded px | shown dp | verdict | sim |");t.push("|---|---|---|---|---|---|---|---|---|---|");for(const a of e.records){const s=a.intrinsic?`${a.intrinsic.width}\xD7${a.intrinsic.height}`:"\u2013";const c=a.layout?`${a.layout.width}\xD7${a.layout.height}`:"\u2013";const l=a.status==="error"?`**error**${a.errorCode?` (${a.errorCode})`:""}`:a.status;t.push(`| ${a.id} | ${bc(a.uri)} | ${a.lib} | ${l}${a.mounted?"":" (unmounted)"} | ${vc(a)} | ${a.ms??"\u2013"} | ${s} | ${c} | ${Em(a)} | ${a.overrideLabel??"\u2013"} |`)}t.push("");t.push("_Act on a row with `image_action` using its `id` (hard_reload / retry / force_error / force_loading / force_blank / swap_url / flash / restore / evict_disk)._");return t.join("\n")}var Om={hard_reload:{action:"hardReload"},retry:{action:"retry"},evict_disk:{action:"evictDisk"},flash:{action:"flash"},restore:{action:"clearOverride"},force_error:{action:"setOverride",params:()=>({kind:"error"})},force_loading:{action:"setOverride",params:()=>({kind:"hang"})},force_blank:{action:"setOverride",params:()=>({kind:"blank"})},swap_url:{action:"setOverride",params:e=>({kind:"url",uri:e})}};function Rm(e,n){e.registerTool("get_images",{description:"Inspect image loads reported through the Buoy image integration, including available cache origin, load timing, size estimates, errors and simulation overrides. Coverage depends on instrumented components and platform support. Requires @buoy-gg/images on the device.",inputSchema:{deviceId:L,limit:qe.number().optional().describe("Max records to return (newest first). Default 30, max 200."),status:qe.enum(["pending","loading","loaded","error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")}},async({deviceId:t,limit:i,status:r})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(i??30,1),200);const s=await n.callAction(it,"list",{limit:a,status:r},o);if(!s)return x("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");const c=Cm(s,a);const l={stats:s.stats,insights:s.insights,captureStatus:s.captureStatus,globalModes:s.globalModes,records:s.records.map(d=>({id:d.id,uri:d.uri,lib:d.lib,status:d.status,mounted:d.mounted,cache:d.cache,ms:d.ms,intrinsic:d.intrinsic,layout:d.layout,oversizeFactor:d.oversizeFactor,wastedKB:d.wastedKB,error:d.error,errorCode:d.errorCode,overrideLabel:d.overrideLabel,loadCount:d.loadCount,hasAltText:d.hasAltText,layoutShifts:d.layoutShifts}))};return M(`${c}
822
+ \`\`\``)}catch(r){return x(j(r))}});e.registerTool("clear_js_thread_stats",{description:"Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",inputSchema:{deviceId:L}},async({deviceId:t})=>{try{const i=n.requireProDevice(t);await n.callAction(Qr,"clear",{},i);return M("Cleared js-top stats.")}catch(i){return x(j(i))}})}import{z as ze}from"zod";var ot="images";function Mc(e){if(!e)return"(no source)";const n=e.match(/[?&]unstable_path=([^&?]+)/);const t=n?decodeURIComponent(n[1]):e;const i=t.split("?")[0]??t;const r=i.split("/").filter(Boolean);return r[r.length-1]??t}function Ym(e){if(e.oversizeFactor==null)return"\u2013";if(e.oversizeFactor<.9)return`${e.oversizeFactor.toFixed(1)}\xD7 upscaled`;if(e.oversizeFactor<=1.1)return"right-sized";return`${e.oversizeFactor.toFixed(1)}\xD7 oversized (${e.wastedKB}KB wasted)`}function Ac(e){if(e.cache)return e.cache;if(e.progressSeen)return"network";return"?"}function Cc(e){return`${(e/(1024*1024)).toFixed(1)}MB`}function Xm(e,n){const t=[];const i=e.stats;t.push(`## Images registry (${e.total} records, showing ${Math.min(n,e.records.length)})`);t.push("");t.push(`${i.loaded} loaded \xB7 ${i.errors} failed \xB7 ${i.loading} in flight \xB7 \u2248${Cc(i.estDecodedBytes)} decoded`+(i.estWastedBytes>0?` \xB7 **${Cc(i.estWastedBytes)} wasted on oversized sources**`:""));const r=e.insights;if(r){const a=[];if(r.duplicates.length>0)a.push(`${r.duplicates.length} duplicate URL(s): ${r.duplicates.slice(0,3).map(s=>`${Mc(s.uri)}\xD7${s.count}`).join(", ")}`);if(r.retryStorms.length>0)a.push(`${r.retryStorms.length} retry storm(s): ${r.retryStorms.slice(0,3).map(s=>`#${s.id}(${s.loadCount} loads)`).join(", ")}`);if(r.queueSaturated)a.push("iOS image queue saturated (>4 concurrent loads)");if(r.missingAlt>0)a.push(`${r.missingAlt} image(s) missing alt/accessibilityLabel`);if(r.layoutShifters.length>0)a.push(`${r.layoutShifters.length} layout-shifting image(s): ${r.layoutShifters.slice(0,3).map(s=>`#${s.id}`).join(", ")} \u2014 reserve explicit dimensions`);if(a.length>0){t.push("");t.push(`**Insights:** ${a.join(" \xB7 ")}`)}}const o=e.captureStatus;if(o.rnDecoratorTooLate){t.push("");t.push('\u26A0 RN <Image> capture missed the module-load race \u2014 add `import "@buoy-gg/images/register"` as the FIRST import of the app entry file.')}if(e.globalModes.network!=="normal"||e.globalModes.blank){t.push("");t.push(`\u26A0 Simulation active: network=${e.globalModes.network}${e.globalModes.blank?", blank images ON":""} (set_image_simulation to reset).`)}t.push("");t.push("| id | image | lib | status | cache | ms | decoded px | shown dp | verdict | sim |");t.push("|---|---|---|---|---|---|---|---|---|---|");for(const a of e.records){const s=a.intrinsic?`${a.intrinsic.width}\xD7${a.intrinsic.height}`:"\u2013";const c=a.layout?`${a.layout.width}\xD7${a.layout.height}`:"\u2013";const l=a.status==="error"?`**error**${a.errorCode?` (${a.errorCode})`:""}`:a.status;t.push(`| ${a.id} | ${Mc(a.uri)} | ${a.lib} | ${l}${a.mounted?"":" (unmounted)"} | ${Ac(a)} | ${a.ms??"\u2013"} | ${s} | ${c} | ${Ym(a)} | ${a.overrideLabel??"\u2013"} |`)}t.push("");t.push("_Act on a row with `image_action` using its `id` (hard_reload / retry / force_error / force_loading / force_blank / swap_url / flash / restore / evict_disk)._");return t.join("\n")}var Zm={hard_reload:{action:"hardReload"},retry:{action:"retry"},evict_disk:{action:"evictDisk"},flash:{action:"flash"},restore:{action:"clearOverride"},force_error:{action:"setOverride",params:()=>({kind:"error"})},force_loading:{action:"setOverride",params:()=>({kind:"hang"})},force_blank:{action:"setOverride",params:()=>({kind:"blank"})},swap_url:{action:"setOverride",params:e=>({kind:"url",uri:e})}};function Qm(e,n){e.registerTool("get_images",{description:"Inspect image loads reported through the Buoy image integration, including available cache origin, load timing, size estimates, errors and simulation overrides. Coverage depends on instrumented components and platform support. Requires @buoy-gg/images on the device.",inputSchema:{deviceId:L,limit:ze.number().optional().describe("Max records to return (newest first). Default 30, max 200."),status:ze.enum(["pending","loading","loaded","error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")}},async({deviceId:t,limit:i,status:r})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(i??30,1),200);const s=await n.callAction(ot,"list",{limit:a,status:r},o);if(!s)return x("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");const c=Xm(s,a);const l={stats:s.stats,insights:s.insights,captureStatus:s.captureStatus,globalModes:s.globalModes,records:s.records.map(d=>({id:d.id,uri:d.uri,lib:d.lib,status:d.status,mounted:d.mounted,cache:d.cache,ms:d.ms,intrinsic:d.intrinsic,layout:d.layout,oversizeFactor:d.oversizeFactor,wastedKB:d.wastedKB,error:d.error,errorCode:d.errorCode,overrideLabel:d.overrideLabel,loadCount:d.loadCount,hasAltText:d.hasAltText,layoutShifts:d.layoutShifts}))};return M(`${c}
823
823
 
824
824
  \`\`\`json
825
825
  ${JSON.stringify(l)}
826
- \`\`\``)}catch(o){return x(j(o))}});e.registerTool("image_action",{description:"Request an action for a registered image ID: hard_reload, retry, evict_disk, flash, prove_savings or a supported simulation override. swap_url requires url; restore removes the override. Support depends on the component and platform. prove_savings attempts a WebP conversion at displayed size and reports available byte comparisons and a device-local URI; inspect visual quality and originalBytesSource before using the result. Other actions return an acknowledgement and, when available, a later record snapshot; that snapshot may precede load completion. A device-local URI is not necessarily accessible from the editor host.",inputSchema:{deviceId:L,id:qe.number().describe("Record id from get_images."),action:qe.enum(["hard_reload","retry","evict_disk","flash","prove_savings","force_error","force_loading","force_blank","swap_url","restore"]).describe("What to do."),url:qe.string().optional().describe("Replacement URL (swap_url only).")}},async({deviceId:t,id:i,action:r,url:o})=>{try{const a=n.requireProDevice(t);if(r==="prove_savings"){const h=await n.callAction(it,"proveSavings",{id:i},a);if(!h)return x("Device returned nothing for proveSavings.");if(!h.ok)return x(h.message);const b=[`**prove_savings** on #${i}: ${h.message}`];if(h.originalBytes!=null)b.push(`Original: ${Math.round(h.originalBytes/1024)}KB (${h.originalBytesSource})`);if(h.optimizedBytes!=null)b.push(`Optimized: ${Math.round(h.optimizedBytes/1024)}KB \u2014 ${h.optimizedDims?.width}\xD7${h.optimizedDims?.height} WebP`);if(h.savedBytes!=null)b.push(`**Saved: ${Math.round(h.savedBytes/1024)}KB (${h.savedPct}%)**`);if(h.optimizedUri)b.push(`Preview in place: image_action {id:${i}, action:"swap_url", url:"${h.optimizedUri}"}`);return M(b.join("\n"))}const s=Om[r];if(!s)return x(`Unknown action: ${r}`);if(r==="swap_url"&&!o)return x("swap_url requires the `url` param.");const c=await n.callAction(it,s.action,{id:i,...s.params?.(o)??{}},a);const l=r==="flash"?0:1500;if(l>0)await new Promise(h=>setTimeout(h,l));const d=await n.callAction(it,"getDetail",{id:i},a).catch(()=>null);const u=[`**${r}** on #${i}: ${c?.message??(c?.ok===false?"failed":"done")}`];if(d){u.push("");u.push(`Now: status=${d.status}${d.errorCode?` (${d.errorCode})`:""} \xB7 cache=${vc(d)} \xB7 ms=${d.ms??"\u2013"} \xB7 loads=${d.loadCount}${d.overrideLabel?` \xB7 sim="${d.overrideLabel}"`:""}${d.mounted?"":" \xB7 UNMOUNTED"}`);if(d.error)u.push(`Error: ${d.error}`)}return M(u.join("\n"))}catch(a){return x(j(a))}});e.registerTool("set_image_simulation",{description:"Configure supported instrumented image loads: offline simulates network-load failures while bundled assets can still load; cold requests cache bypass; normal resets the network mode. Reload or remount existing images as needed. blankImages removes sources from integrated images; massAction targets registered mounted images. Can also clear the registry or supported Expo caches. These modes do not change device connectivity.",inputSchema:{deviceId:L,network:qe.enum(["normal","offline","cold"]).optional().describe("Network simulation mode."),blankImages:qe.boolean().optional().describe("Render integrated images without a source."),massAction:qe.enum(["error","loading","blank","reload","flash","restore"]).optional().describe("Apply to registered mounted images: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),clearRegistry:qe.boolean().optional().describe("Clear the captured image log."),clearExpoCaches:qe.boolean().optional().describe("Clear expo-image memory + disk caches.")}},async({deviceId:t,network:i,blankImages:r,massAction:o,clearRegistry:a,clearExpoCaches:s})=>{try{const c=n.requireProDevice(t);const l=[];if(o){const d=await n.callAction(it,"massAction",{kind:o},c);l.push(d?.message??`mass ${o} applied`)}if(i){await n.callAction(it,"setNetworkMode",{mode:i},c);l.push(`network=${i}`)}if(r!=null){await n.callAction(it,"setBlankImages",{enabled:r},c);l.push(`blankImages=${r}`)}if(a){await n.callAction(it,"clearRecords",{},c);l.push("registry cleared")}if(s){const d=await n.callAction(it,"clearExpoCaches",{},c);l.push(d?.message??"expo caches cleared")}if(l.length===0)return x("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");return M(`Applied: ${l.join(" \xB7 ")}`)}catch(c){return x(j(c))}})}import{z as Ue}from"zod";function Pm(e,n){for(const[t,i,r]of[["get_three_scene","getScene","Read the scene tree. Each tree has at most 500 nodes. Requires @buoy-gg/three."],["get_three_stats","getStats","Read the last render counts. Unknown fields are null. Counts are not bytes."]]){e.registerTool(t,{description:r,inputSchema:{deviceId:L}},async({deviceId:o})=>{try{const a=n.requireProDevice(o);const s=await n.callAction("three",i,{},a);return s==null?x("No scene data. Check that @buoy-gg/three is set up."):M(JSON.stringify(s))}catch(a){return x(j(a))}})}e.registerTool("three_action",{description:"Pick a scene node or change its values. Turn values use radians. Colors use six digit hex.",inputSchema:{deviceId:L,rendererId:Ue.string().describe("Scene group ID from get_three_scene."),id:Ue.string().optional().describe("Node ID. Needed except for clearSelection."),action:Ue.enum(["select","clearSelection","visible","position","rotation","scale","color"]),value:Ue.union([Ue.boolean(),Ue.tuple([Ue.number().finite(),Ue.number().finite(),Ue.number().finite()]),Ue.string()]).optional()}},async({deviceId:t,...i})=>{try{const r=n.requireProDevice(t);const o=await n.callAction("three","action",i,r);return o==null?x("No reply from the scene."):M(JSON.stringify(o))}catch(r){return x(j(r))}})}import{z as Sn}from"zod";function Nm(e){if(e.graphSupported&&e.graphCount!=null)return null;const n=(e.graphError??"").trim();const t=e.dev===true||!!n;if(t){return n?`\u26A0 Metro bundle graph unavailable (${n}) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.`:"\u26A0 Metro bundle graph unavailable \u2014 showing runtime-registered assets only; never-loaded detection unavailable."}return"\u26A0 No Metro bundle graph (release build, or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable."}var Tt="assets";function dt(e){if(e==null||e<=0)return"\u2013";if(e<1024)return`${e}B`;if(e<1024*1024)return`${(e/1024).toFixed(1)}KB`;return`${(e/(1024*1024)).toFixed(2)}MB`}function $c(e){if(e.sizeBytes==null)return"\u2013";return e.sizeSource==="measured"?dt(e.sizeBytes):`~${dt(e.sizeBytes)}`}function kc(e){if(!e.length)return"\u2013";return e.map(n=>`@${n}x`).join(" ")}function Fm(e,n){const t=[];if(!e.loaded)t.push("UNUSED");if(n.overDecode.includes(e.id))t.push("OVERSIZE");if(n.duplicates.some(i=>i.ids.includes(e.id)))t.push("DUP");if(n.heavyGifs.includes(e.id)||n.heavyFonts.includes(e.id))t.push("HEAVY");if(n.scaleAnomalies.includes(e.id))t.push("BAD-SCALE");else if(n.scaleGaps.includes(e.id))t.push("LOW-RES");if(n.webpCandidates.includes(e.id))t.push("WEBP");return t.length?t.join(" "):"\u2013"}function Bm(e,n){const t=[];const i=e.stats;const r=e.scanStatus;t.push(`## Asset inventory (${e.total} assets, showing ${Math.min(n,e.records.length)}, largest first)`);t.push("");const o=Object.entries(i.byKind).filter(([,d])=>d.count>0).map(([d,u])=>`${u.count} ${d}${u.bytes>0?` (${dt(u.bytes)})`:""}`);t.push(`${i.loaded} loaded \xB7 ${i.neverLoaded} never loaded \xB7 `+(i.totalBytes>0?`${dt(i.totalBytes)} measured (${i.measuredCount}/${i.total})`:"sizes not measured")+` \xB7 ${o.join(" \xB7 ")}`);if(r.fontFamilies.length>0){t.push(`Loaded font families: ${r.fontFamilies.join(", ")}`)}const a=Nm(r);if(a)t.push(a);if(r.measuring)t.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");const s=e.insights;const c=[];if(s.neverLoaded.length)c.push(`${s.neverLoaded.length} never loaded`);if(s.duplicates.length){const d=s.duplicates.reduce((u,h)=>u+(h.wastedBytes??0),0);c.push(`${s.duplicates.length} duplicate group(s)${d>0?` (${dt(d)} wasted)`:""}`)}if(s.overDecode.length)c.push(`${s.overDecode.length} decode larger than 2\xD7 the screen`);if(s.webpCandidates.length)c.push(`${s.webpCandidates.length} WebP candidate(s)`);if(s.heavyGifs.length)c.push(`${s.heavyGifs.length} heavy GIF(s)`);if(s.heavyFonts.length)c.push(`${s.heavyFonts.length} heavy font(s)`);if(s.scaleGaps.length)c.push(`${s.scaleGaps.length} scale gap(s)`);if(s.scaleAnomalies.length)c.push(`${s.scaleAnomalies.length} scale anomaly(ies)`);if(c.length){t.push("");t.push(`**Findings:** ${c.join(" \xB7 ")}`)}const l=e.diff;if(l){const d=l.netBytes==null?"net unknown":Math.abs(l.netBytes)<1024?"\xB10 net":`${l.netBytes>=0?"+":"\u2212"}${dt(Math.abs(l.netBytes))} net`;t.push("");t.push(`**Since baseline** (${new Date(l.baselineSavedAt).toISOString()}): ${l.added.length} added \xB7 ${l.removed.length} removed \xB7 ${l.grown.length} grew \xB7 ${d}`+(l.removed.length?` \u2014 removed: ${l.removed.slice(0,5).map(u=>`${u.name}.${u.type}`).join(", ")}${l.removed.length>5?"\u2026":""}`:""))}t.push("");t.push("| id | asset | kind | dims | scales | size | flags |");t.push("|---|---|---|---|---|---|---|");for(const d of e.records){const u=d.width!=null&&d.height!=null?`${Math.round(d.width)}\xD7${Math.round(d.height)}`:"\u2013";t.push(`| ${d.id} | ${d.name}.${d.type} | ${d.kind} | ${u} | ${kc(d.scales)} | ${$c(d)} | ${Fm(d,s)} |`)}t.push("");t.push("_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._");return t.join("\n")}function jm(e,n){e.registerTool("get_assets",{description:"Inspect assets reported by the configured manifest, runtime registry and available Metro bundle graph. Includes available dimensions, scale coverage, byte sizes, observed load history, audit findings and saved-baseline differences. Size measurement and coverage depend on setup; not observed loading does not mean unused. Use get_images for instrumented runtime image loads. Requires @buoy-gg/assets on the device.",inputSchema:{deviceId:L,limit:Sn.number().optional().describe("Max records to return (largest first). Default 40, max 500."),kind:Sn.enum(["image","font","video","audio","data","other"]).optional().describe("Only assets of this kind."),unusedOnly:Sn.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")}},async({deviceId:t,limit:i,kind:r,unusedOnly:o})=>{try{const a=n.requireProDevice(t);const s=Math.min(Math.max(i??40,1),500);const c=await n.callAction(Tt,"list",{limit:o?500:s,kind:r,loadedOnly:false},a);if(!c)return x("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");if(o){c.records=c.records.filter(u=>!u.loaded).slice(0,s)}const l=Bm(c,s);const d={stats:c.stats,scanStatus:c.scanStatus,insights:c.insights,diff:c.diff,records:c.records.map(u=>({id:u.id,name:u.name,type:u.type,kind:u.kind,hash:u.hash,width:u.width,height:u.height,scales:u.scales,loaded:u.loaded,sizeBytes:u.sizeBytes,sizeSource:u.sizeSource}))};return M(`${l}
826
+ \`\`\``)}catch(o){return x(j(o))}});e.registerTool("image_action",{description:"Request an action for a registered image ID: hard_reload, retry, evict_disk, flash, prove_savings or a supported simulation override. swap_url requires url; restore removes the override. Support depends on the component and platform. prove_savings attempts a WebP conversion at displayed size and reports available byte comparisons and a device-local URI; inspect visual quality and originalBytesSource before using the result. Other actions return an acknowledgement and, when available, a later record snapshot; that snapshot may precede load completion. A device-local URI is not necessarily accessible from the editor host.",inputSchema:{deviceId:L,id:ze.number().describe("Record id from get_images."),action:ze.enum(["hard_reload","retry","evict_disk","flash","prove_savings","force_error","force_loading","force_blank","swap_url","restore"]).describe("What to do."),url:ze.string().optional().describe("Replacement URL (swap_url only).")}},async({deviceId:t,id:i,action:r,url:o})=>{try{const a=n.requireProDevice(t);if(r==="prove_savings"){const h=await n.callAction(ot,"proveSavings",{id:i},a);if(!h)return x("Device returned nothing for proveSavings.");if(!h.ok)return x(h.message);const b=[`**prove_savings** on #${i}: ${h.message}`];if(h.originalBytes!=null)b.push(`Original: ${Math.round(h.originalBytes/1024)}KB (${h.originalBytesSource})`);if(h.optimizedBytes!=null)b.push(`Optimized: ${Math.round(h.optimizedBytes/1024)}KB \u2014 ${h.optimizedDims?.width}\xD7${h.optimizedDims?.height} WebP`);if(h.savedBytes!=null)b.push(`**Saved: ${Math.round(h.savedBytes/1024)}KB (${h.savedPct}%)**`);if(h.optimizedUri)b.push(`Preview in place: image_action {id:${i}, action:"swap_url", url:"${h.optimizedUri}"}`);return M(b.join("\n"))}const s=Zm[r];if(!s)return x(`Unknown action: ${r}`);if(r==="swap_url"&&!o)return x("swap_url requires the `url` param.");const c=await n.callAction(ot,s.action,{id:i,...s.params?.(o)??{}},a);const l=r==="flash"?0:1500;if(l>0)await new Promise(h=>setTimeout(h,l));const d=await n.callAction(ot,"getDetail",{id:i},a).catch(()=>null);const u=[`**${r}** on #${i}: ${c?.message??(c?.ok===false?"failed":"done")}`];if(d){u.push("");u.push(`Now: status=${d.status}${d.errorCode?` (${d.errorCode})`:""} \xB7 cache=${Ac(d)} \xB7 ms=${d.ms??"\u2013"} \xB7 loads=${d.loadCount}${d.overrideLabel?` \xB7 sim="${d.overrideLabel}"`:""}${d.mounted?"":" \xB7 UNMOUNTED"}`);if(d.error)u.push(`Error: ${d.error}`)}return M(u.join("\n"))}catch(a){return x(j(a))}});e.registerTool("set_image_simulation",{description:"Configure supported instrumented image loads: offline simulates network-load failures while bundled assets can still load; cold requests cache bypass; normal resets the network mode. Reload or remount existing images as needed. blankImages removes sources from integrated images; massAction targets registered mounted images. Can also clear the registry or supported Expo caches. These modes do not change device connectivity.",inputSchema:{deviceId:L,network:ze.enum(["normal","offline","cold"]).optional().describe("Network simulation mode."),blankImages:ze.boolean().optional().describe("Render integrated images without a source."),massAction:ze.enum(["error","loading","blank","reload","flash","restore"]).optional().describe("Apply to registered mounted images: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),clearRegistry:ze.boolean().optional().describe("Clear the captured image log."),clearExpoCaches:ze.boolean().optional().describe("Clear expo-image memory + disk caches.")}},async({deviceId:t,network:i,blankImages:r,massAction:o,clearRegistry:a,clearExpoCaches:s})=>{try{const c=n.requireProDevice(t);const l=[];if(o){const d=await n.callAction(ot,"massAction",{kind:o},c);l.push(d?.message??`mass ${o} applied`)}if(i){await n.callAction(ot,"setNetworkMode",{mode:i},c);l.push(`network=${i}`)}if(r!=null){await n.callAction(ot,"setBlankImages",{enabled:r},c);l.push(`blankImages=${r}`)}if(a){await n.callAction(ot,"clearRecords",{},c);l.push("registry cleared")}if(s){const d=await n.callAction(ot,"clearExpoCaches",{},c);l.push(d?.message??"expo caches cleared")}if(l.length===0)return x("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");return M(`Applied: ${l.join(" \xB7 ")}`)}catch(c){return x(j(c))}})}import{z as We}from"zod";function ew(e,n){for(const[t,i,r]of[["get_three_scene","getScene","Read the scene tree. Each tree has at most 500 nodes. Requires @buoy-gg/three."],["get_three_stats","getStats","Read the last render counts. Unknown fields are null. Counts are not bytes."]]){e.registerTool(t,{description:r,inputSchema:{deviceId:L}},async({deviceId:o})=>{try{const a=n.requireProDevice(o);const s=await n.callAction("three",i,{},a);return s==null?x("No scene data. Check that @buoy-gg/three is set up."):M(JSON.stringify(s))}catch(a){return x(j(a))}})}e.registerTool("three_action",{description:"Pick a scene node or change its values. Turn values use radians. Colors use six digit hex.",inputSchema:{deviceId:L,rendererId:We.string().describe("Scene group ID from get_three_scene."),id:We.string().optional().describe("Node ID. Needed except for clearSelection."),action:We.enum(["select","clearSelection","visible","position","rotation","scale","color"]),value:We.union([We.boolean(),We.tuple([We.number().finite(),We.number().finite(),We.number().finite()]),We.string()]).optional()}},async({deviceId:t,...i})=>{try{const r=n.requireProDevice(t);const o=await n.callAction("three","action",i,r);return o==null?x("No reply from the scene."):M(JSON.stringify(o))}catch(r){return x(j(r))}})}import{z as En}from"zod";function tw(e){if(e.graphSupported&&e.graphCount!=null)return null;const n=(e.graphError??"").trim();const t=e.dev===true||!!n;if(t){return n?`\u26A0 Metro bundle graph unavailable (${n}) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.`:"\u26A0 Metro bundle graph unavailable \u2014 showing runtime-registered assets only; never-loaded detection unavailable."}return"\u26A0 No Metro bundle graph (release build, or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable."}var Mt="assets";function ht(e){if(e==null||e<=0)return"\u2013";if(e<1024)return`${e}B`;if(e<1024*1024)return`${(e/1024).toFixed(1)}KB`;return`${(e/(1024*1024)).toFixed(2)}MB`}function Ec(e){if(e.sizeBytes==null)return"\u2013";return e.sizeSource==="measured"?ht(e.sizeBytes):`~${ht(e.sizeBytes)}`}function Oc(e){if(!e.length)return"\u2013";return e.map(n=>`@${n}x`).join(" ")}function nw(e,n){const t=[];if(!e.loaded)t.push("UNUSED");if(n.overDecode.includes(e.id))t.push("OVERSIZE");if(n.duplicates.some(i=>i.ids.includes(e.id)))t.push("DUP");if(n.heavyGifs.includes(e.id)||n.heavyFonts.includes(e.id))t.push("HEAVY");if(n.scaleAnomalies.includes(e.id))t.push("BAD-SCALE");else if(n.scaleGaps.includes(e.id))t.push("LOW-RES");if(n.webpCandidates.includes(e.id))t.push("WEBP");return t.length?t.join(" "):"\u2013"}function iw(e,n){const t=[];const i=e.stats;const r=e.scanStatus;t.push(`## Asset inventory (${e.total} assets, showing ${Math.min(n,e.records.length)}, largest first)`);t.push("");const o=Object.entries(i.byKind).filter(([,d])=>d.count>0).map(([d,u])=>`${u.count} ${d}${u.bytes>0?` (${ht(u.bytes)})`:""}`);t.push(`${i.loaded} loaded \xB7 ${i.neverLoaded} never loaded \xB7 `+(i.totalBytes>0?`${ht(i.totalBytes)} measured (${i.measuredCount}/${i.total})`:"sizes not measured")+` \xB7 ${o.join(" \xB7 ")}`);if(r.fontFamilies.length>0){t.push(`Loaded font families: ${r.fontFamilies.join(", ")}`)}const a=tw(r);if(a)t.push(a);if(r.measuring)t.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");const s=e.insights;const c=[];if(s.neverLoaded.length)c.push(`${s.neverLoaded.length} never loaded`);if(s.duplicates.length){const d=s.duplicates.reduce((u,h)=>u+(h.wastedBytes??0),0);c.push(`${s.duplicates.length} duplicate group(s)${d>0?` (${ht(d)} wasted)`:""}`)}if(s.overDecode.length)c.push(`${s.overDecode.length} decode larger than 2\xD7 the screen`);if(s.webpCandidates.length)c.push(`${s.webpCandidates.length} WebP candidate(s)`);if(s.heavyGifs.length)c.push(`${s.heavyGifs.length} heavy GIF(s)`);if(s.heavyFonts.length)c.push(`${s.heavyFonts.length} heavy font(s)`);if(s.scaleGaps.length)c.push(`${s.scaleGaps.length} scale gap(s)`);if(s.scaleAnomalies.length)c.push(`${s.scaleAnomalies.length} scale anomaly(ies)`);if(c.length){t.push("");t.push(`**Findings:** ${c.join(" \xB7 ")}`)}const l=e.diff;if(l){const d=l.netBytes==null?"net unknown":Math.abs(l.netBytes)<1024?"\xB10 net":`${l.netBytes>=0?"+":"\u2212"}${ht(Math.abs(l.netBytes))} net`;t.push("");t.push(`**Since baseline** (${new Date(l.baselineSavedAt).toISOString()}): ${l.added.length} added \xB7 ${l.removed.length} removed \xB7 ${l.grown.length} grew \xB7 ${d}`+(l.removed.length?` \u2014 removed: ${l.removed.slice(0,5).map(u=>`${u.name}.${u.type}`).join(", ")}${l.removed.length>5?"\u2026":""}`:""))}t.push("");t.push("| id | asset | kind | dims | scales | size | flags |");t.push("|---|---|---|---|---|---|---|");for(const d of e.records){const u=d.width!=null&&d.height!=null?`${Math.round(d.width)}\xD7${Math.round(d.height)}`:"\u2013";t.push(`| ${d.id} | ${d.name}.${d.type} | ${d.kind} | ${u} | ${Oc(d.scales)} | ${Ec(d)} | ${nw(d,s)} |`)}t.push("");t.push("_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._");return t.join("\n")}function rw(e,n){e.registerTool("get_assets",{description:"Inspect assets reported by the configured manifest, runtime registry and available Metro bundle graph. Includes available dimensions, scale coverage, byte sizes, observed load history, audit findings and saved-baseline differences. Size measurement and coverage depend on setup; not observed loading does not mean unused. Use get_images for instrumented runtime image loads. Requires @buoy-gg/assets on the device.",inputSchema:{deviceId:L,limit:En.number().optional().describe("Max records to return (largest first). Default 40, max 500."),kind:En.enum(["image","font","video","audio","data","other"]).optional().describe("Only assets of this kind."),unusedOnly:En.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")}},async({deviceId:t,limit:i,kind:r,unusedOnly:o})=>{try{const a=n.requireProDevice(t);const s=Math.min(Math.max(i??40,1),500);const c=await n.callAction(Mt,"list",{limit:o?500:s,kind:r,loadedOnly:false},a);if(!c)return x("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");if(o){c.records=c.records.filter(u=>!u.loaded).slice(0,s)}const l=iw(c,s);const d={stats:c.stats,scanStatus:c.scanStatus,insights:c.insights,diff:c.diff,records:c.records.map(u=>({id:u.id,name:u.name,type:u.type,kind:u.kind,hash:u.hash,width:u.width,height:u.height,scales:u.scales,loaded:u.loaded,sizeBytes:u.sizeBytes,sizeSource:u.sizeSource}))};return M(`${l}
827
827
 
828
828
  \`\`\`json
829
829
  ${JSON.stringify(d)}
830
- \`\`\``)}catch(a){return x(j(a))}});e.registerTool("asset_action",{description:"Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",inputSchema:{deviceId:L,action:Sn.enum(["rescan","measure_sizes","detail","save_baseline","clear_baseline","clear_records"]).describe("What to do."),id:Sn.number().optional().describe("Asset id from get_assets (detail only).")}},async({deviceId:t,action:i,id:r})=>{try{const o=n.requireProDevice(t);switch(i){case"rescan":{const a=await n.callAction(Tt,"rescan",{},o);const s=a?.scanStatus;if(!s)return x("Device returned nothing for rescan.");return M(`Rescan done: ${s.registryCount} registered at runtime`+(s.graphCount!=null?` \xB7 ${s.graphCount} in the bundle graph`:" \xB7 no bundle graph")+(s.graphError?` (graph: ${s.graphError})`:"")+` \xB7 ${s.fontFamilies.length} font families`+(s.measuring?" \xB7 size measurement running \u2014 get_assets in a moment":""))}case"measure_sizes":{const a=await n.callAction(Tt,"measureSizes",{},o);return M(`Measured ${a?.measured??0} of ${a?.total??0} assets.`)}case"detail":{if(r==null)return x("detail requires the `id` param (from get_assets).");const a=await n.callAction(Tt,"getDetail",{id:r},o);if(!a)return x(`Asset ${r} not found \u2014 re-run get_assets for current ids.`);const s=[`## ${a.name}.${a.type} (#${a.id})`,"",`- Kind: ${a.kind} \xB7 ${a.loaded?"loaded at runtime":"in bundle, NEVER loaded"} (${a.origin})`];if(a.width!=null)s.push(`- Dimensions: ${a.width}\xD7${a.height} logical \xB7 scales ${kc(a.scales)}`);if(a.sizesByScale){for(const[c,l]of Object.entries(a.sizesByScale)){s.push(`- Size @${c}x: ${dt(l)}`)}}if(a.sizeBytes!=null)s.push(`- Total: ${$c(a)}`);if(a.estDecodedBytes)s.push(`- Decoded memory: ~${dt(a.estDecodedBytes)} (RGBA)`);if(a.hash)s.push(`- Hash: ${a.hash}`);if(a.fileSystemLocation)s.push(`- Source dir: ${a.fileSystemLocation}`);if(a.files?.length)s.push(`- Files: ${a.files.join(", ")}`);if(a.uri)s.push(`- Resolved URI: ${a.uri}`);return M(s.join("\n"))}case"save_baseline":{const a=await n.callAction(Tt,"saveBaseline",{},o);return M(`Baseline saved (${a?.assets??0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`)}case"clear_baseline":{await n.callAction(Tt,"clearBaseline",{},o);return M("Baseline cleared.")}case"clear_records":{const a=await n.callAction(Tt,"clearRecords",{},o);return M(a?.message??"Inventory cleared.")}}}catch(o){return x(j(o))}})}import{z as _i}from"zod";import{WebSocket as Lm}from"ws";var qm=3e3;function Um(e){return`${e.replace(/^http/,"ws").replace(/\/+$/,"")}/message`}function _c(e){return new Promise(n=>{let t=false;const i=s=>{if(t)return;t=true;n(s)};let r;try{r=new Lm(Um(e))}catch(s){i({ok:false,error:s.message});return}const o=setTimeout(()=>{try{r.close()}catch{}i({ok:false,error:`dev server at ${e} did not accept a connection`})},qm);let a=false;r.on("open",()=>{try{r.send(JSON.stringify({version:2,method:"reload"}));a=true;clearTimeout(o);setTimeout(()=>{try{r.close()}catch{}i({ok:true})},250)}catch(s){clearTimeout(o);i({ok:false,error:s.message})}});r.on("error",s=>{clearTimeout(o);if(a)return;i({ok:false,error:s.message})})})}var Hr="app";var Sc="reloadApp";var xc=3e4;var zm=1200;var Tc=1500;var Wm=400;var Dc=e=>new Promise(n=>setTimeout(n,e));async function Ic(e,n,t){try{const i=await e.callAction(Hr,"ping",{},n,{timeoutMs:t,silent:true});e.noteDevServerOrigin(n,i?.devServerOrigin);return typeof i?.startedAt==="number"?i.startedAt:null}catch{return null}}function Hm(e,n){if(n===$e)return;const t=e.toolActions(Hr).find(i=>i.deviceId===n);if(!t?.actions.includes(Sc)){throw new Error(`This app doesn't expose remote reload \u2014 its @buoy-gg/core is too old. Upgrade @buoy-gg/core, then reload the app once by hand.`)}}async function Jr(e,n,t,i){const r=Date.now();await Dc(zm);while(Date.now()-r<t){const o=await Ic(e,n,Tc);if(o!==null&&o!==i)return Date.now()-r;await Dc(Wm)}return null}function Jm(e){if(!/is not connected right now/.test(e))return null;return/"([^"]+)"/.exec(e)?.[1]??null}async function Gm(e,n,t,i){const r=Jm(j(n));if(!r||!e.deviceWasAllowed(r))throw n;const o=e.devServerOrigin(r);let a="";if(o){const d=await _c(o);if(d.ok){if(!i)return M(`"${r}" was not connected, so it was reloaded through its dev server (${o}). It is restarting.`);const u=await Jr(e,r,Math.min(t,12e3),null);if(u!=null){return M(`"${r}" was not connected, so it was reloaded through its dev server. Back after ${(u/1e3).toFixed(1)}s. All in-memory state was reset.`)}a=` Its dev server (${o}) took the reload, but the app did not come back.`}else{a=` Its dev server at ${o} could not reload it: ${d.error}.`}}const s=e.listDevices().find(d=>d.deviceId===r)?.platform;const c=Ln(r);const l=c?await oa(r,c).catch(()=>void 0):await rn(r,s).catch(()=>void 0);if(l){try{await ar(l).catch(()=>void 0);await Ot(l);if(!i)return M(`"${r}" was not connected, so it was relaunched on its ${l.platform==="ios"?"simulator":"device"}.`);const d=await Jr(e,r,t,null);if(d!=null){return M(`"${r}" was not connected, so it was relaunched on its ${l.platform==="ios"?"simulator":"device"} (a cold start). Back after ${(d/1e3).toFixed(1)}s. All in-memory state was reset.`)}}catch{}}return x(`"${r}" is not connected and could not be brought back.${a} Open the app by hand, then run list_devices.`)}function Km(e,n){e.registerTool("reload_app",{description:"Request a JS reload for a supported app using DevSettings in development or installed expo-updates where available. For an unresponsive app, can try its known dev server when reachable. Recovery is not guaranteed. Reload can discard in-memory state and evidence; collect needed observations first when possible. By default waits for reconnection up to timeoutMs. Needs configured @buoy-gg/core support. Works on the free tier.",inputSchema:{strategy:_i.enum(["auto","dev-settings","expo-updates"]).optional().describe("Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."),wait:_i.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),timeoutMs:_i.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),deviceId:_i.string().optional().describe('The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out. Or "All" to reload every connected app.')}},async t=>{try{let i;try{i=n.requireProDevice(t.deviceId)}catch(d){return await Gm(n,d,t.timeoutMs??xc,t.wait!==false)}Hm(n,i);const r=t.wait===false?null:await Ic(n,i,Tc);let o=null;let a=false;const s=async d=>{const u=n.devServerOrigin(i);if(!u){if(d)throw d;throw new Error("The app is not answering and no dev-server origin is known (it was never learned from a successful ping). Reload the app by hand.")}const h=await _c(u);if(!h.ok){throw new Error(`The app is not answering (it looks crashed) and the dev server at ${u} could not be reached either: ${h.error}. Reload the app by hand.`)}a=true};if(n.isUnresponsive(i)&&n.devServerOrigin(i)){await s()}else{try{o=await n.callAction(Hr,Sc,{strategy:t.strategy??"auto"},i)}catch(d){await s(d)}}const c=a?` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)`:o?.strategy?` via ${o.strategy}`:"";if(t.wait===false){return M(`Reload requested${c}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`)}const l=await Jr(n,i,t.timeoutMs??xc,r);if(l==null){return x(`Reload was accepted${c}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`)}return M(`Reloaded the app${c} \u2014 back and connected after ${(l/1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`)}catch(i){return x(j(i))}})}import{z as Vm}from"zod";var Ym=e=>({content:[{type:"text",text:e}]});var Xm=e=>({isError:true,content:[{type:"text",text:e}]});var Mc=e=>e instanceof Error?e.message:String(e);var Zm=5e3;var Qm=20;var ew=400;var tw=1600;var nw=160;var Si=(e,n=nw)=>e.length>n?`${e.slice(0,n)}\u2026`:e;function iw(e){if(!e)return"--:--:--";const n=new Date(e);const t=i=>String(i).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}async function rw(e,n,t){const i=[];try{const r=await e.callAction("storage","mmkv.snapshot",{},n,t);for(const o of r??[]){const a=o.entries?.length??0;if(a>0)i.push(`MMKV "${o.id}": ${a} key${a===1?"":"s"}`)}}catch{}try{const r=await e.callAction("storage","secure.keys",{},n,t);if(r?.length)i.push(`SecureStore: ${r.length} key${r.length===1?"":"s"}`)}catch{}return i.length?`
831
- ${i.join(" \xB7 ")} \u2014 read with get_storage`:""}async function xi(e,n){try{const t=await n();return t?`${e}
832
- ${t}`:null}catch(t){const i=Mc(t);if(/not responding|timed out|got no answer/i.test(i)){return`${e}
830
+ \`\`\``)}catch(a){return x(j(a))}});e.registerTool("asset_action",{description:"Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",inputSchema:{deviceId:L,action:En.enum(["rescan","measure_sizes","detail","save_baseline","clear_baseline","clear_records"]).describe("What to do."),id:En.number().optional().describe("Asset id from get_assets (detail only).")}},async({deviceId:t,action:i,id:r})=>{try{const o=n.requireProDevice(t);switch(i){case"rescan":{const a=await n.callAction(Mt,"rescan",{},o);const s=a?.scanStatus;if(!s)return x("Device returned nothing for rescan.");return M(`Rescan done: ${s.registryCount} registered at runtime`+(s.graphCount!=null?` \xB7 ${s.graphCount} in the bundle graph`:" \xB7 no bundle graph")+(s.graphError?` (graph: ${s.graphError})`:"")+` \xB7 ${s.fontFamilies.length} font families`+(s.measuring?" \xB7 size measurement running \u2014 get_assets in a moment":""))}case"measure_sizes":{const a=await n.callAction(Mt,"measureSizes",{},o);return M(`Measured ${a?.measured??0} of ${a?.total??0} assets.`)}case"detail":{if(r==null)return x("detail requires the `id` param (from get_assets).");const a=await n.callAction(Mt,"getDetail",{id:r},o);if(!a)return x(`Asset ${r} not found \u2014 re-run get_assets for current ids.`);const s=[`## ${a.name}.${a.type} (#${a.id})`,"",`- Kind: ${a.kind} \xB7 ${a.loaded?"loaded at runtime":"in bundle, NEVER loaded"} (${a.origin})`];if(a.width!=null)s.push(`- Dimensions: ${a.width}\xD7${a.height} logical \xB7 scales ${Oc(a.scales)}`);if(a.sizesByScale){for(const[c,l]of Object.entries(a.sizesByScale)){s.push(`- Size @${c}x: ${ht(l)}`)}}if(a.sizeBytes!=null)s.push(`- Total: ${Ec(a)}`);if(a.estDecodedBytes)s.push(`- Decoded memory: ~${ht(a.estDecodedBytes)} (RGBA)`);if(a.hash)s.push(`- Hash: ${a.hash}`);if(a.fileSystemLocation)s.push(`- Source dir: ${a.fileSystemLocation}`);if(a.files?.length)s.push(`- Files: ${a.files.join(", ")}`);if(a.uri)s.push(`- Resolved URI: ${a.uri}`);return M(s.join("\n"))}case"save_baseline":{const a=await n.callAction(Mt,"saveBaseline",{},o);return M(`Baseline saved (${a?.assets??0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`)}case"clear_baseline":{await n.callAction(Mt,"clearBaseline",{},o);return M("Baseline cleared.")}case"clear_records":{const a=await n.callAction(Mt,"clearRecords",{},o);return M(a?.message??"Inventory cleared.")}}}catch(o){return x(j(o))}})}import{z as Oi}from"zod";import{WebSocket as ow}from"ws";var aw=3e3;function sw(e){return`${e.replace(/^http/,"ws").replace(/\/+$/,"")}/message`}function Rc(e){return new Promise(n=>{let t=false;const i=s=>{if(t)return;t=true;n(s)};let r;try{r=new ow(sw(e))}catch(s){i({ok:false,error:s.message});return}const o=setTimeout(()=>{try{r.close()}catch{}i({ok:false,error:`dev server at ${e} did not accept a connection`})},aw);let a=false;r.on("open",()=>{try{r.send(JSON.stringify({version:2,method:"reload"}));a=true;clearTimeout(o);setTimeout(()=>{try{r.close()}catch{}i({ok:true})},250)}catch(s){clearTimeout(o);i({ok:false,error:s.message})}});r.on("error",s=>{clearTimeout(o);if(a)return;i({ok:false,error:s.message})})})}var eo="app";var Pc="reloadApp";var Nc=3e4;var cw=1200;var Fc=1500;var lw=400;var Bc=e=>new Promise(n=>setTimeout(n,e));async function jc(e,n,t){try{const i=await e.callAction(eo,"ping",{},n,{timeoutMs:t,silent:true});e.noteDevServerOrigin(n,i?.devServerOrigin);return typeof i?.startedAt==="number"?i.startedAt:null}catch{return null}}function dw(e,n){if(n===ke)return;const t=e.toolActions(eo).find(i=>i.deviceId===n);if(!t?.actions.includes(Pc)){throw new Error(`This app doesn't expose remote reload \u2014 its @buoy-gg/core is too old. Upgrade @buoy-gg/core, then reload the app once by hand.`)}}async function to(e,n,t,i){const r=Date.now();await Bc(cw);while(Date.now()-r<t){const o=await jc(e,n,Fc);if(o!==null&&o!==i)return Date.now()-r;await Bc(lw)}return null}function uw(e){if(!/is not connected right now/.test(e))return null;return/"([^"]+)"/.exec(e)?.[1]??null}async function hw(e,n,t,i){const r=uw(j(n));if(!r||!e.deviceWasAllowed(r))throw n;const o=e.devServerOrigin(r);let a="";if(o){const d=await Rc(o);if(d.ok){if(!i)return M(`"${r}" was not connected, so it was reloaded through its dev server (${o}). It is restarting.`);const u=await to(e,r,Math.min(t,12e3),null);if(u!=null){return M(`"${r}" was not connected, so it was reloaded through its dev server. Back after ${(u/1e3).toFixed(1)}s. All in-memory state was reset.`)}a=` Its dev server (${o}) took the reload, but the app did not come back.`}else{a=` Its dev server at ${o} could not reload it: ${d.error}.`}}const s=e.listDevices().find(d=>d.deviceId===r)?.platform;const c=Yn(r);const l=c?await fa(r,c).catch(()=>void 0):await dn(r,s).catch(()=>void 0);if(l){try{await fr(l).catch(()=>void 0);await Ft(l);if(!i)return M(`"${r}" was not connected, so it was relaunched on its ${l.platform==="ios"?"simulator":"device"}.`);const d=await to(e,r,t,null);if(d!=null){return M(`"${r}" was not connected, so it was relaunched on its ${l.platform==="ios"?"simulator":"device"} (a cold start). Back after ${(d/1e3).toFixed(1)}s. All in-memory state was reset.`)}}catch{}}return x(`"${r}" is not connected and could not be brought back.${a} Open the app by hand, then run list_devices.`)}function pw(e,n){e.registerTool("reload_app",{description:"Request a JS reload for a supported app using DevSettings in development or installed expo-updates where available. For an unresponsive app, can try its known dev server when reachable. Recovery is not guaranteed. Reload can discard in-memory state and evidence; collect needed observations first when possible. By default waits for reconnection up to timeoutMs. Needs configured @buoy-gg/core support. Works on the free tier.",inputSchema:{strategy:Oi.enum(["auto","dev-settings","expo-updates"]).optional().describe("Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."),wait:Oi.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),timeoutMs:Oi.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),deviceId:Oi.string().optional().describe('The device to act on: its id from list_devices or from your instructions. Pass it whenever more than one app may be connected. Once a call names a device, later calls in this session use it when deviceId is left out. Or "All" to reload every connected app.')}},async t=>{try{let i;try{i=n.requireProDevice(t.deviceId)}catch(d){return await hw(n,d,t.timeoutMs??Nc,t.wait!==false)}dw(n,i);const r=t.wait===false?null:await jc(n,i,Fc);let o=null;let a=false;const s=async d=>{const u=n.devServerOrigin(i);if(!u){if(d)throw d;throw new Error("The app is not answering and no dev-server origin is known (it was never learned from a successful ping). Reload the app by hand.")}const h=await Rc(u);if(!h.ok){throw new Error(`The app is not answering (it looks crashed) and the dev server at ${u} could not be reached either: ${h.error}. Reload the app by hand.`)}a=true};if(n.isUnresponsive(i)&&n.devServerOrigin(i)){await s()}else{try{o=await n.callAction(eo,Pc,{strategy:t.strategy??"auto"},i)}catch(d){await s(d)}}const c=a?` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)`:o?.strategy?` via ${o.strategy}`:"";if(t.wait===false){return M(`Reload requested${c}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`)}const l=await to(n,i,t.timeoutMs??Nc,r);if(l==null){return x(`Reload was accepted${c}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`)}return M(`Reloaded the app${c} \u2014 back and connected after ${(l/1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`)}catch(i){return x(j(i))}})}import{z as fw}from"zod";var mw=e=>({content:[{type:"text",text:e}]});var ww=e=>({isError:true,content:[{type:"text",text:e}]});var Lc=e=>e instanceof Error?e.message:String(e);var gw=5e3;var bw=20;var vw=400;var yw=1600;var $w=160;var Ri=(e,n=$w)=>e.length>n?`${e.slice(0,n)}\u2026`:e;function kw(e){if(!e)return"--:--:--";const n=new Date(e);const t=i=>String(i).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}async function _w(e,n,t){const i=[];try{const r=await e.callAction("storage","mmkv.snapshot",{},n,t);for(const o of r??[]){const a=o.entries?.length??0;if(a>0)i.push(`MMKV "${o.id}": ${a} key${a===1?"":"s"}`)}}catch{}try{const r=await e.callAction("storage","secure.keys",{},n,t);if(r?.length)i.push(`SecureStore: ${r.length} key${r.length===1?"":"s"}`)}catch{}return i.length?`
831
+ ${i.join(" \xB7 ")} \u2014 read with get_storage`:""}async function Pi(e,n){try{const t=await n();return t?`${e}
832
+ ${t}`:null}catch(t){const i=Lc(t);if(/not responding|timed out|got no answer/i.test(i)){return`${e}
833
833
  (no answer \u2014 see the note above)`}return`${e}
834
- (unavailable: ${Si(i,120)})`}}function ow(e,n){e.registerTool("get_triage",{description:"Summarize available runtime observations for a connected app: captured console errors, recent network requests, configured storage and screen information. Use the returned sections and errors to choose follow-up tools. Missing data or an unresponsive app does not prove a crash; crash output may be unavailable. Free apps get storage key names only; values need Buoy Pro.",inputSchema:{deviceId:Vm.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=n.connectedDevices().find(w=>w.deviceId===i);const o=new Set((r?.tools??[]).map(w=>w.toolId));const a=`**${r?.deviceName??i}** (${r?.platform??"?"}, id \`${i}\`) \u2014 tools: ${[...o].join(", ")||"none"}`;const s={timeoutMs:Zm};const[c,l,d,u]=await Promise.all([xi("**Console \u2014 recent errors/warnings:**",async()=>{if(!o.has("console"))return null;const{payload:w}=await n.getSnapshot("console",i,s);const g=w;const m=Array.isArray(g)?g:g?.entries??[];const p=m.filter(v=>v.level==="error"||v.level==="warning");const y=p.slice(-8);if(y.length===0){return`(none \u2014 ${m.length} entries captured)`}return y.map(v=>`${iw(v.timestamp)} [${v.level==="error"?"ERR":"warn"}] ${Si((v.message??"").split("\n").map($=>$.trim()).filter(Boolean).slice(0,3).join(" \u23CE "),220)}`).join("\n")}),xi("**Network \u2014 recent requests:**",async()=>{if(!o.has("network"))return null;const{payload:w}=await n.getSnapshot("network",i,s);const g=Array.isArray(w)?w:w?.events??[];if(g.length===0)return"(no requests captured yet)";const m=g.slice(-8);return m.map(p=>{const y=p.error||(p.status??0)>=400?"\u274C":"\u2705";const v=p.path??p.url??"?";const $=p.error??p.status??"pending";return`${y} ${p.method??"GET"} ${Si(String(v),80)} \u2192 ${$}${p.duration?` (${Math.round(p.duration)}ms)`:""}`}).join("\n")}),xi("**Storage \u2014 persisted values:**",async()=>{if(!o.has("storage"))return null;const w=await n.callAction("storage","async.getAllKeys",{},i,s);if(!Array.isArray(w)||w.length===0){return"(no app keys)"}const g=w.slice(0,Qm);if(!fe(r)){const k=w.length-g.length;return`${g.map(D=>`\`${D}\``).join(", ")}${k>0?` (+${k} more)`:""}
834
+ (unavailable: ${Ri(i,120)})`}}function Sw(e,n){e.registerTool("get_triage",{description:"Summarize available runtime observations for a connected app: captured console errors, recent network requests, configured storage and screen information. Use the returned sections and errors to choose follow-up tools. Missing data or an unresponsive app does not prove a crash; crash output may be unavailable. Free apps get storage key names only; values need Buoy Pro.",inputSchema:{deviceId:fw.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=n.connectedDevices().find(w=>w.deviceId===i);const o=new Set((r?.tools??[]).map(w=>w.toolId));const a=`**${r?.deviceName??i}** (${r?.platform??"?"}, id \`${i}\`) \u2014 tools: ${[...o].join(", ")||"none"}`;const s={timeoutMs:gw};const[c,l,d,u]=await Promise.all([Pi("**Console \u2014 recent errors/warnings:**",async()=>{if(!o.has("console"))return null;const{payload:w}=await n.getSnapshot("console",i,s);const g=w;const m=Array.isArray(g)?g:g?.entries??[];const p=m.filter(v=>v.level==="error"||v.level==="warning");const y=p.slice(-8);if(y.length===0){return`(none \u2014 ${m.length} entries captured)`}return y.map(v=>`${kw(v.timestamp)} [${v.level==="error"?"ERR":"warn"}] ${Ri((v.message??"").split("\n").map($=>$.trim()).filter(Boolean).slice(0,3).join(" \u23CE "),220)}`).join("\n")}),Pi("**Network \u2014 recent requests:**",async()=>{if(!o.has("network"))return null;const{payload:w}=await n.getSnapshot("network",i,s);const g=Array.isArray(w)?w:w?.events??[];if(g.length===0)return"(no requests captured yet)";const m=g.slice(-8);return m.map(p=>{const y=p.error||(p.status??0)>=400?"\u274C":"\u2705";const v=p.path??p.url??"?";const $=p.error??p.status??"pending";return`${y} ${p.method??"GET"} ${Ri(String(v),80)} \u2192 ${$}${p.duration?` (${Math.round(p.duration)}ms)`:""}`}).join("\n")}),Pi("**Storage \u2014 persisted values:**",async()=>{if(!o.has("storage"))return null;const w=await n.callAction("storage","async.getAllKeys",{},i,s);if(!Array.isArray(w)||w.length===0){return"(no app keys)"}const g=w.slice(0,bw);if(!fe(r)){const k=w.length-g.length;return`${g.map(D=>`\`${D}\``).join(", ")}${k>0?` (+${k} more)`:""}
835
835
  _Values need Buoy Pro (get_storage includeValues:true)._`}let m=[];try{m=await n.callAction("storage","async.multiGet",{keys:g},i,s)??[]}catch{return`${g.map(k=>`\`${k}\``).join(", ")}
836
- _Values: get_storage includeValues:true_`}const p=new Map(m);let y=tw;const v=g.map(k=>{const D=p.get(k);if(D==null)return`- \`${k}\` = (null)`;if(D.length>ew){return`- \`${k}\` = (${(D.length/1024).toFixed(1)} KB \u2014 read with get_storage)`}if(y<=0)return`- \`${k}\` = (not shown \u2014 triage budget spent; read with get_storage)`;y-=D.length;return`- \`${k}\` = ${D}`});const $=w.length-g.length;const T=await rw(n,i,s);return`${v.join("\n")}${$>0?`
837
- \u2026+${$} more keys`:""}${T}`}),xi("**Screen \u2014 what's visible now:**",async()=>{if(!o.has("highlight-updates"))return null;const w=await n.callAction("highlight-updates","describeScreen",{},i,s);const g=(w?.elements??[]).filter(m=>m.interactive);if(g.length===0)return"(nothing interactive on screen)";return g.slice(0,20).map(m=>`\u2022 ${m.name}${m.text||m.label?` "${Si(m.text??m.label??"",60)}"`:""}${m.testID?` [testID=${m.testID}]`:""}`).join("\n")})]);const h=n.isUnresponsive(i);const b=h?`
836
+ _Values: get_storage includeValues:true_`}const p=new Map(m);let y=yw;const v=g.map(k=>{const D=p.get(k);if(D==null)return`- \`${k}\` = (null)`;if(D.length>vw){return`- \`${k}\` = (${(D.length/1024).toFixed(1)} KB \u2014 read with get_storage)`}if(y<=0)return`- \`${k}\` = (not shown \u2014 triage budget spent; read with get_storage)`;y-=D.length;return`- \`${k}\` = ${D}`});const $=w.length-g.length;const T=await _w(n,i,s);return`${v.join("\n")}${$>0?`
837
+ \u2026+${$} more keys`:""}${T}`}),Pi("**Screen \u2014 what's visible now:**",async()=>{if(!o.has("highlight-updates"))return null;const w=await n.callAction("highlight-updates","describeScreen",{},i,s);const g=(w?.elements??[]).filter(m=>m.interactive);if(g.length===0)return"(nothing interactive on screen)";return g.slice(0,20).map(m=>`\u2022 ${m.name}${m.text||m.label?` "${Ri(m.text??m.label??"",60)}"`:""}${m.testID?` [testID=${m.testID}]`:""}`).join("\n")})]);const h=n.isUnresponsive(i);const b=h?`
838
838
 
839
- \u{1F6D1} ${n.unresponsiveNote(i)}`:"";const f=[c,l,d,u].filter(Boolean).join("\n\n");return Ym(`${a}${b}
839
+ \u{1F6D1} ${n.unresponsiveNote(i)}`:"";const f=[c,l,d,u].filter(Boolean).join("\n\n");return mw(`${a}${b}
840
840
 
841
841
  ${f}
842
842
 
843
- _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`)}catch(i){return Xm(Mc(i))}})}import{z as Pe}from"zod";var xn="highlight-updates";var aw=e=>({content:[{type:"text",text:e}]});var Tn=e=>({isError:true,content:[{type:"text",text:e}]});var sw=e=>e instanceof Error?e.message:String(e);var Ti=e=>new Promise(n=>setTimeout(n,e));var Ac=3e3;var Ec=400;var Cc=12;var Oc=new Map;var cw=Pe.object({tap:Pe.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),type:Pe.string().optional().describe("Text to type (needs `into`)."),into:Pe.string().optional().describe("Target text input for `type`: testID or fuzzy text."),waitMs:Pe.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),repeat:Pe.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")}).describe("One interaction to measure. Keep it to the thing you're asking about.");function Rc(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function Pc(e){if(!e)return"unknown";let n="unknown";let t=-1;for(const[i,r]of Object.entries(e)){if((r??0)>t){t=r??0;n=i}}return n}var Nc={propsUnstable:"props changed by IDENTITY only (a fresh function/object from the parent) \u2014 fix upstream, not in the child",parent:"parent re-rendered and nothing of its own changed \u2014 a pure cascade",props:"props genuinely changed value",hooks:"its own state/hooks changed",mount:"first render"};function lw(e,n){const t=e.summary;const i=t.wastedRenders??0;const r=t.totalRenders>0?Math.round(i/t.totalRenders*100):0;const o=[];const a=t.totalRenderMs??0;o.push(`**${t.totalRenders} component renders across ${t.totalCommits} commits in ${(e.windowMs/1e3).toFixed(1)}s**`+(a>0?` \xB7 ${a.toFixed(1)}ms of render work`:"")+` \xB7 ${i} wasted (${r}%)`);const s=Object.entries(t.causes??{}).filter(([,u])=>(u??0)>0).sort((u,h)=>(h[1]??0)-(u[1]??0)).map(([u,h])=>`${u} ${h}`).join(" \xB7 ");if(s)o.push(`causes: ${s}`);o.push("");o.push("| component | self ms | renders | cause | dragged along by | changed props |");o.push("|---|---:|---:|---|---|---|");const c=[...t.topComponents].sort((u,h)=>(h.totalMs??0)-(u.totalMs??0)||h.renders-u.renders).slice(0,n);for(const u of c){o.push(`| ${u.name} | ${(u.totalMs??0).toFixed(1)} | ${u.renders} | ${Pc(u.causes)} | ${u.parentName??"\u2014"} | ${(u.changedProps??[]).join(", ")||"\u2014"} |`)}if(t.topComponents.length>n){o.push(`_\u2026+${t.topComponents.length-n} more components_`)}const l=new Set(c.map(u=>Pc(u.causes)));const d=[...l].filter(u=>Nc[u]).map(u=>`\`${u}\` = ${Nc[u]}`);if(d.length){o.push("");o.push(d.join(" \xB7 "))}o.push("");o.push("`wasted` classifies parent cascades and identity-only prop changes. Investigate whether these renders are needed before changing them. Rows are sorted by **self ms**. Compare timing and render counts for the measured workload, and check behavior before deciding whether memoization helps.");return o.join("\n")}function dw(e,n){const t=(d,u)=>{const h=u-d;return`${d} \u2192 ${u} (${h>0?"+":""}${h})`};const i=e.summary;const r=n.summary;const o=[`**vs "${e.label}"**`];o.push(`- total renders: ${t(i.totalRenders,r.totalRenders)}`);o.push(`- wasted: ${t(i.wastedRenders??0,r.wastedRenders??0)}`);const a=new Map(i.topComponents.map(d=>[d.name,d.renders]));const s=[];for(const d of r.topComponents.slice(0,20)){const u=a.get(d.name)??0;if(u!==d.renders)s.push(`${d.name} ${t(u,d.renders)}`);a.delete(d.name)}for(const[d,u]of a){if(u>0)s.push(`${d} ${t(u,0)}`)}if(s.length)o.push(`- per component: ${s.slice(0,10).join(" \xB7 ")}`);const c=r.totalRenders<i.totalRenders;const l=r.totalRenders>i.totalRenders;o.push(l?"\n\u26A0\uFE0F MORE renders than before \u2014 this change made this interaction worse. Re-render fixes routinely move cost instead of removing it; check the other interactions too before moving on.":c?"\n\u2705 Fewer renders than before on this interaction. Measure the OTHER interactions too \u2014 a fix that helps typing can cost taps.":"\n\u2796 No change on this interaction.");return o.join("\n")}function uw(e,n){e.registerTool("measure_renders",{description:"Measure captured component renders during supplied interaction steps or an idle window. Returns available timings, render counts, reported reasons and changed props. The wasted count is a diagnostic classification, not proof a render can be removed. Check interaction warnings before comparing runs. Use the same steps and comparable conditions with compareToPrevious:true after editing; the comparison alone does not prove correctness or overall performance. Requires Buoy Pro and a highlight-updates integration exposing beginMeasurement. Use run_benchmark_batch to compare route-based variants.",inputSchema:{steps:Pe.array(cw).max(20).optional().describe("The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."),idleMs:Pe.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${Ac}.`),label:Pe.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),compareToPrevious:Pe.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),top:Pe.number().min(1).max(40).optional().describe(`Components to list. Default ${Cc}.`),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=n.connectedDevices().find(g=>g.deviceId===i);const o=(r?.tools??[]).find(g=>g.toolId===xn);if(!o){return Tn("This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates.")}if(!(o.actions??[]).includes("beginMeasurement")){return Tn("This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route.")}const a=await n.callAction(xn,"beginMeasurement",{},i);if(a&&a.ok===false){return Tn(`Can't measure renders: ${a.reason??"capture unavailable"}`)}await Ti(Ec);const s=Date.now();const c=[];if(t.steps?.length){for(const g of t.steps){const m=g.repeat??1;for(let p=0;p<m;p++){if(g.type!==void 0){const y=g.into??g.tap;if(!y){c.push("a `type` step needs `into`");continue}const v=await n.callAction(xn,"tapElement",{...Rc(y),text:g.type},i);if(!v?.tapped)c.push(v?.reason??`could not type into "${y}"`);else if(v.matched?.via&&v.matched.via!=="onChangeText"){c.push(`"${y}" is not a text input \u2014 it ran ${v.matched.via}, nothing was typed`)}}else if(g.tap){const y=await n.callAction(xn,"tapElement",Rc(g.tap),i);if(!y?.tapped)c.push(y?.reason??`no element matched "${g.tap}"`)}if(g.waitMs)await Ti(g.waitMs)}}await Ti(Ec)}else{await Ti(t.idleMs??Ac)}const l=Date.now()-s;const d=await n.callAction(xn,"endMeasurement",{},i);const u=d?.summary;if(!u){return Tn("The capture window closed without returning anything. If the app reloaded mid-measurement, try again.")}const h={label:t.label??(t.steps?.length?"interaction":"idle"),windowMs:l,summary:u};const b=Oc.get(i);Oc.set(i,h);const f=t.steps?.length?t.steps.map(g=>(g.type!==void 0?`type "${g.type}"`:`tap "${g.tap}"`)+(g.repeat?` \xD7${g.repeat}`:"")).join(", "):`idle ${(l/1e3).toFixed(1)}s`;const w=[`**measure_renders \u2014 ${f}**`,"",lw(h,t.top??Cc)];if(t.compareToPrevious){w.push("",b?dw(b,h):"_No previous measurement on this device to compare against._")}if(c.length)w.push("",`\u26A0\uFE0F ${c.join("; ")}`);if(u.totalRenders===0){w.push("","_Nothing re-rendered in this window. If you expected renders, check the app is on the screen you meant and that the steps actually hit something._")}else if(!t.compareToPrevious){w.push("","_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._")}return aw(w.join("\n"))}catch(i){return Tn(sw(i))}})}import hw from"fs";import Fc from"path";var pw=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".json"]);var fw=new Set(["node_modules",".git",".expo","ios","android","dist","lib","build",".next","coverage","__snapshots__"]);var mw=400;function ww(e){const n=(e??"").trim().toLowerCase();if(n==="always"||n==="never"||n==="auto")return n;return"auto"}var gw=class{constructor(){this.watcher=null;this.changed=new Set;this.lastEditAt=0;this.nudgedForCurrentBatch=false;this.mode="auto";this.started=false}start(e,n){this.mode=n;if(n==="never"||this.started)return;this.started=true;try{this.watcher=hw.watch(e,{recursive:true},(t,i)=>{if(!i)return;const r=String(i);if(r.split(Fc.sep).some(o=>fw.has(o)))return;if(!pw.has(Fc.extname(r)))return;this.noteEdit(r)});this.watcher.on("error",()=>this.stop())}catch{this.stop()}}noteEdit(e){const n=Date.now();if(n-this.lastEditAt>mw)this.nudgedForCurrentBatch=false;this.lastEditAt=n;if(this.changed.size<50)this.changed.add(e)}stop(){try{this.watcher?.close()}catch{}this.watcher=null}markVerified(){this.changed.clear();this.nudgedForCurrentBatch=false}hasUnverifiedEdits(){return this.changed.size>0}nudge(){if(this.mode==="never"||!this.hasUnverifiedEdits())return null;if(this.mode==="auto"){if(this.nudgedForCurrentBatch)return null;this.nudgedForCurrentBatch=true}const e=[...this.changed];const n=e.slice(0,3).join(", ");const t=e.length>3?` +${e.length-3} more`:"";return`
843
+ _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`)}catch(i){return ww(Lc(i))}})}import{z as Fe}from"zod";var On="highlight-updates";var xw=e=>({content:[{type:"text",text:e}]});var Rn=e=>({isError:true,content:[{type:"text",text:e}]});var Tw=e=>e instanceof Error?e.message:String(e);var Ni=e=>new Promise(n=>setTimeout(n,e));var qc=3e3;var Uc=400;var zc=12;var Wc=new Map;var Dw=Fe.object({tap:Fe.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),type:Fe.string().optional().describe("Text to type (needs `into`)."),into:Fe.string().optional().describe("Target text input for `type`: testID or fuzzy text."),waitMs:Fe.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),repeat:Fe.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")}).describe("One interaction to measure. Keep it to the thing you're asking about.");function Hc(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function Jc(e){if(!e)return"unknown";let n="unknown";let t=-1;for(const[i,r]of Object.entries(e)){if((r??0)>t){t=r??0;n=i}}return n}var Gc={propsUnstable:"props changed by IDENTITY only (a fresh function/object from the parent) \u2014 fix upstream, not in the child",parent:"parent re-rendered and nothing of its own changed \u2014 a pure cascade",props:"props genuinely changed value",hooks:"its own state/hooks changed",mount:"first render"};function Iw(e,n){const t=e.summary;const i=t.wastedRenders??0;const r=t.totalRenders>0?Math.round(i/t.totalRenders*100):0;const o=[];const a=t.totalRenderMs??0;o.push(`**${t.totalRenders} component renders across ${t.totalCommits} commits in ${(e.windowMs/1e3).toFixed(1)}s**`+(a>0?` \xB7 ${a.toFixed(1)}ms of render work`:"")+` \xB7 ${i} wasted (${r}%)`);const s=Object.entries(t.causes??{}).filter(([,u])=>(u??0)>0).sort((u,h)=>(h[1]??0)-(u[1]??0)).map(([u,h])=>`${u} ${h}`).join(" \xB7 ");if(s)o.push(`causes: ${s}`);o.push("");o.push("| component | self ms | renders | cause | dragged along by | changed props |");o.push("|---|---:|---:|---|---|---|");const c=[...t.topComponents].sort((u,h)=>(h.totalMs??0)-(u.totalMs??0)||h.renders-u.renders).slice(0,n);for(const u of c){o.push(`| ${u.name} | ${(u.totalMs??0).toFixed(1)} | ${u.renders} | ${Jc(u.causes)} | ${u.parentName??"\u2014"} | ${(u.changedProps??[]).join(", ")||"\u2014"} |`)}if(t.topComponents.length>n){o.push(`_\u2026+${t.topComponents.length-n} more components_`)}const l=new Set(c.map(u=>Jc(u.causes)));const d=[...l].filter(u=>Gc[u]).map(u=>`\`${u}\` = ${Gc[u]}`);if(d.length){o.push("");o.push(d.join(" \xB7 "))}o.push("");o.push("`wasted` classifies parent cascades and identity-only prop changes. Investigate whether these renders are needed before changing them. Rows are sorted by **self ms**. Compare timing and render counts for the measured workload, and check behavior before deciding whether memoization helps.");return o.join("\n")}function Mw(e,n){const t=(d,u)=>{const h=u-d;return`${d} \u2192 ${u} (${h>0?"+":""}${h})`};const i=e.summary;const r=n.summary;const o=[`**vs "${e.label}"**`];o.push(`- total renders: ${t(i.totalRenders,r.totalRenders)}`);o.push(`- wasted: ${t(i.wastedRenders??0,r.wastedRenders??0)}`);const a=new Map(i.topComponents.map(d=>[d.name,d.renders]));const s=[];for(const d of r.topComponents.slice(0,20)){const u=a.get(d.name)??0;if(u!==d.renders)s.push(`${d.name} ${t(u,d.renders)}`);a.delete(d.name)}for(const[d,u]of a){if(u>0)s.push(`${d} ${t(u,0)}`)}if(s.length)o.push(`- per component: ${s.slice(0,10).join(" \xB7 ")}`);const c=r.totalRenders<i.totalRenders;const l=r.totalRenders>i.totalRenders;o.push(l?"\n\u26A0\uFE0F MORE renders than before \u2014 this change made this interaction worse. Re-render fixes routinely move cost instead of removing it; check the other interactions too before moving on.":c?"\n\u2705 Fewer renders than before on this interaction. Measure the OTHER interactions too \u2014 a fix that helps typing can cost taps.":"\n\u2796 No change on this interaction.");return o.join("\n")}function Aw(e,n){e.registerTool("measure_renders",{description:"Measure captured component renders during supplied interaction steps or an idle window. Returns available timings, render counts, reported reasons and changed props. The wasted count is a diagnostic classification, not proof a render can be removed. Check interaction warnings before comparing runs. Use the same steps and comparable conditions with compareToPrevious:true after editing; the comparison alone does not prove correctness or overall performance. Requires Buoy Pro and a highlight-updates integration exposing beginMeasurement. Use run_benchmark_batch to compare route-based variants.",inputSchema:{steps:Fe.array(Dw).max(20).optional().describe("The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."),idleMs:Fe.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${qc}.`),label:Fe.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),compareToPrevious:Fe.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),top:Fe.number().min(1).max(40).optional().describe(`Components to list. Default ${zc}.`),deviceId:L}},async t=>{try{const i=n.requireProDevice(t.deviceId);const r=n.connectedDevices().find(g=>g.deviceId===i);const o=(r?.tools??[]).find(g=>g.toolId===On);if(!o){return Rn("This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates.")}if(!(o.actions??[]).includes("beginMeasurement")){return Rn("This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route.")}const a=await n.callAction(On,"beginMeasurement",{},i);if(a&&a.ok===false){return Rn(`Can't measure renders: ${a.reason??"capture unavailable"}`)}await Ni(Uc);const s=Date.now();const c=[];if(t.steps?.length){for(const g of t.steps){const m=g.repeat??1;for(let p=0;p<m;p++){if(g.type!==void 0){const y=g.into??g.tap;if(!y){c.push("a `type` step needs `into`");continue}const v=await n.callAction(On,"tapElement",{...Hc(y),text:g.type},i);if(!v?.tapped)c.push(v?.reason??`could not type into "${y}"`);else if(v.matched?.via&&v.matched.via!=="onChangeText"){c.push(`"${y}" is not a text input \u2014 it ran ${v.matched.via}, nothing was typed`)}}else if(g.tap){const y=await n.callAction(On,"tapElement",Hc(g.tap),i);if(!y?.tapped)c.push(y?.reason??`no element matched "${g.tap}"`)}if(g.waitMs)await Ni(g.waitMs)}}await Ni(Uc)}else{await Ni(t.idleMs??qc)}const l=Date.now()-s;const d=await n.callAction(On,"endMeasurement",{},i);const u=d?.summary;if(!u){return Rn("The capture window closed without returning anything. If the app reloaded mid-measurement, try again.")}const h={label:t.label??(t.steps?.length?"interaction":"idle"),windowMs:l,summary:u};const b=Wc.get(i);Wc.set(i,h);const f=t.steps?.length?t.steps.map(g=>(g.type!==void 0?`type "${g.type}"`:`tap "${g.tap}"`)+(g.repeat?` \xD7${g.repeat}`:"")).join(", "):`idle ${(l/1e3).toFixed(1)}s`;const w=[`**measure_renders \u2014 ${f}**`,"",Iw(h,t.top??zc)];if(t.compareToPrevious){w.push("",b?Mw(b,h):"_No previous measurement on this device to compare against._")}if(c.length)w.push("",`\u26A0\uFE0F ${c.join("; ")}`);if(u.totalRenders===0){w.push("","_Nothing re-rendered in this window. If you expected renders, check the app is on the screen you meant and that the steps actually hit something._")}else if(!t.compareToPrevious){w.push("","_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._")}return xw(w.join("\n"))}catch(i){return Rn(Tw(i))}})}import Cw from"fs";import Kc from"path";var Ew=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".json"]);var Ow=new Set(["node_modules",".git",".expo","ios","android","dist","lib","build",".next","coverage","__snapshots__"]);var Rw=400;function Pw(e){const n=(e??"").trim().toLowerCase();if(n==="always"||n==="never"||n==="auto")return n;return"auto"}var Nw=class{constructor(){this.watcher=null;this.changed=new Set;this.lastEditAt=0;this.nudgedForCurrentBatch=false;this.mode="auto";this.started=false}start(e,n){this.mode=n;if(n==="never"||this.started)return;this.started=true;try{this.watcher=Cw.watch(e,{recursive:true},(t,i)=>{if(!i)return;const r=String(i);if(r.split(Kc.sep).some(o=>Ow.has(o)))return;if(!Ew.has(Kc.extname(r)))return;this.noteEdit(r)});this.watcher.on("error",()=>this.stop())}catch{this.stop()}}noteEdit(e){const n=Date.now();if(n-this.lastEditAt>Rw)this.nudgedForCurrentBatch=false;this.lastEditAt=n;if(this.changed.size<50)this.changed.add(e)}stop(){try{this.watcher?.close()}catch{}this.watcher=null}markVerified(){this.changed.clear();this.nudgedForCurrentBatch=false}hasUnverifiedEdits(){return this.changed.size>0}nudge(){if(this.mode==="never"||!this.hasUnverifiedEdits())return null;if(this.mode==="auto"){if(this.nudgedForCurrentBatch)return null;this.nudgedForCurrentBatch=true}const e=[...this.changed];const n=e.slice(0,3).join(", ");const t=e.length>3?` +${e.length-3} more`:"";return`
844
844
 
845
845
  ---
846
846
  \u26A0\uFE0F **${e.length} source file${e.length===1?"":"s"} changed since anything was last checked on the device** (${n}${t}).
847
- For a behavior change, repeat the affected interaction and check the expected result. Use \`measure_renders\` with the same steps and \`compareToPrevious: true\` for render comparisons, or \`run_flow\` for supported behavior checks. Report what was checked and any remaining limits. This reminder tracks edits and tool calls; clearing it does not prove the change is correct.`}};var Gr=new gw;var Bc=6e3;function Kr(e){return e.listDevices().some(n=>fe(n))||Mn()}var bw=`Buoy MCP connects your editor to supported Buoy tools through the configured broker.
847
+ For a behavior change, repeat the affected interaction and check the expected result. Use \`measure_renders\` with the same steps and \`compareToPrevious: true\` for render comparisons, or \`run_flow\` for supported behavior checks. Report what was checked and any remaining limits. This reminder tracks edits and tool calls; clearing it does not prove the change is correct.`}};var no=new Nw;var Vc=6e3;function io(e){return e.listDevices().some(n=>fe(n))||jn()}var Fw=`Buoy MCP connects your editor to supported Buoy tools through the configured broker.
848
848
 
849
849
  Start with account and connection setup. Use \`list_devices\` to select an app and inspect
850
850
  its available tools. An app on the free tier can use the screen, touch, flow, screenshot,
@@ -952,4 +952,4 @@ After a behavior change, repeat the affected interaction and check the expected
952
952
  Include restart or persistence checks when relevant to the change. Use appropriate static
953
953
  checks for documentation-only work. Report actual checks, observations and remaining limits.
954
954
  BUOY_VERIFY reminders track selected edits and tool calls; clearing a reminder is not a
955
- test result. Stay within the user's authorized task and preserve existing project work.`;var vw=new Set(["measure_renders","scenario_action","run_flow","run_benchmark_batch","run_web_benchmark","find_web_ceiling","screenshot_web_variants","get_triage","tap_element","describe_screen","get_console","get_network_requests","get_events","get_snapshot"]);function yw(e){const n=new af({name:"buoy",version:In},{capabilities:{tools:{}},instructions:bw});Gr.start(process.cwd(),ww(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((i,r,o)=>t(i,r,(async(...a)=>eo(Yc(i,r?.inputSchema?a[0]:{}),async()=>{const s=a[0];if(i==="list_devices"){}else if(s?.deviceId)await e.awaitBroker();else await e.settleDeviceChoice();const c=ut();if(c.level==="pro"&&!Kr(e)){const m=Date.now()+Bc;while(!Kr(e)&&Date.now()<m&&e.connectedDevices().some(p=>e.accessState(p.deviceId,"pro")==="pending")){await new Promise(p=>setTimeout(p,250))}if(!Kr(e)){const p=e.connectedDevices().find($=>e.accessState($.deviceId,"pro")==="pending");if(p){return{isError:true,content:[{type:"text",text:`${pt} account access for "${p.deviceId}" is still being confirmed (the app just connected). Try again in a moment.`}]}}if(!e.isAdmitted()){return{isError:true,content:[{type:"text",text:"A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP."}]}}const y=e.connectedDevices();const v=y.length?` The connected app${y.length>1?"s are":" is"} on ${[...new Set(y.map($=>$.tier))].join(" / ")}.`:" No app on Buoy Pro has connected in this session.";return{isError:true,content:[{type:"text",text:Xt(c,v)}]}}}Op(e,i,s);const l=async()=>{const m=await o(...a);for(const p of m?.content??[]){if(p.type==="text"&&typeof p.text==="string")p.text=xe(p.text)}return m};let d=await l();const u=m=>{const p=m;return p?.isError===true&&(p.content?.[0]?.text??"").startsWith(pt)};if(u(d)){const m=d.content[0].text;const p=/"([^"]+)"/.exec(m)?.[1];const y=Date.now()+Bc;while(Date.now()<y){await new Promise(v=>setTimeout(v,250));if(p?e.accessState(p)==="ready":e.connectedDevices().some(v=>An(v,ut().level)))break}d=await l()}const h=d?.isError===true;let b=null;if(vw.has(i)&&!h)Gr.markVerified();else b=Gr.nudge();const f=dc();const w=[b,f].filter(m=>!!m);if(!w.length)return d;const g=d;if(Array.isArray(g?.content)){return{...g,content:[...g.content,...w.map(m=>({type:"text",text:m}))]}}return d}))));ow(n,e);Dp(n,e);uw(n,e);da(n,e);va(n,e);ya(n,e);wf(n,e);$a(n,e);_a(n,e);Sa(n,e);Da(n,e);Ia(n,e);Is(n,e);Es(n,e);vf(n,e);yf(n,e);kf(n,e);Af(n,e);Ff(n,e);Uf(n,e);Hf(n,e);ja(n,e);Jf(n);qc(n,e);im(n,e);nf(n,e);Kp(n,e);jp(n,e);gm(n,e);Sm(n,e);Am(n,e);Rm(n,e);jm(n,e);Pm(n,e);Km(n,e);return n}var Kb={deviceId:sf.string().optional().describe('Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.')};export{Ei as BROKER_PORT,go as BROKER_URL,bo as DEFAULT_ACTION_TIMEOUT_MS,yo as DEFAULT_SNAPSHOT_TIMEOUT_MS,Oi as DASHBOARD_NAME,$e as ALL_DEVICES,hd as SyncClient,Eo as getUpdateNotice,da as registerGenericTools,qn as buildAutomationConfig,lr as estimateBatchMs,Un as rankBatchFromIndex,an as formatRankingMarkdown,dr as compareReportStats,va as registerPerfMonitorTools,ya as registerEventsTools,$a as registerReactQueryTools,_a as registerZustandTools,Sa as registerJotaiTools,Da as registerReduxTools,Ia as registerStorageTools,wr as stopAllHidStreamsNow,ja as registerScreenshotTools,Jh as stopAllRecordingsNow,Is as registerRoutesTools,Es as registerConsoleTools,yw as createMcpServer};
955
+ test result. Stay within the user's authorized task and preserve existing project work.`;var Bw=new Set(["measure_renders","scenario_action","run_flow","run_benchmark_batch","run_web_benchmark","find_web_ceiling","screenshot_web_variants","get_triage","tap_element","describe_screen","get_console","get_network_requests","get_events","get_snapshot"]);function jw(e){const n=new _f({name:"buoy",version:Nn},{capabilities:{tools:{}},instructions:Fw});no.start(process.cwd(),Pw(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((i,r,o)=>t(i,r,(async(...a)=>lo(pl(i,r?.inputSchema?a[0]:{}),async()=>{const s=a[0];if(i==="list_devices"){}else if(s?.deviceId)await e.awaitBroker();else await e.settleDeviceChoice();const c=ft();if(c.level==="pro"&&!io(e)){const m=Date.now()+Vc;while(!io(e)&&Date.now()<m&&e.connectedDevices().some(p=>e.accessState(p.deviceId,"pro")==="pending")){await new Promise(p=>setTimeout(p,250))}if(!io(e)){const p=e.connectedDevices().find($=>e.accessState($.deviceId,"pro")==="pending");if(p){return{isError:true,content:[{type:"text",text:`${wt} account access for "${p.deviceId}" is still being confirmed (the app just connected). Try again in a moment.`}]}}if(!e.isAdmitted()){return{isError:true,content:[{type:"text",text:"A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP."}]}}const y=e.connectedDevices();const v=y.length?` The connected app${y.length>1?"s are":" is"} on ${[...new Set(y.map($=>$.tier))].join(" / ")}.`:" No app on Buoy Pro has connected in this session.";return{isError:true,content:[{type:"text",text:rn(c,v)}]}}}Yp(e,i,s);const l=async()=>{const m=await o(...a);for(const p of m?.content??[]){if(p.type==="text"&&typeof p.text==="string")p.text=Te(p.text)}return m};let d=await l();const u=m=>{const p=m;return p?.isError===true&&(p.content?.[0]?.text??"").startsWith(wt)};if(u(d)){const m=d.content[0].text;const p=/"([^"]+)"/.exec(m)?.[1];const y=Date.now()+Vc;while(Date.now()<y){await new Promise(v=>setTimeout(v,250));if(p?e.accessState(p)==="ready":e.connectedDevices().some(v=>Ln(v,ft().level)))break}d=await l()}const h=d?.isError===true;let b=null;if(Bw.has(i)&&!h)no.markVerified();else b=no.nudge();const f=$c();const w=[b,f].filter(m=>!!m);if(!w.length)return d;const g=d;if(Array.isArray(g?.content)){return{...g,content:[...g.content,...w.map(m=>({type:"text",text:m}))]}}return d}))));Sw(n,e);Wp(n,e);Aw(n,e);va(n,e);Ia(n,e);Ma(n,e);Of(n,e);Aa(n,e);Ea(n,e);Oa(n,e);Na(n,e);Fa(n,e);Fs(n,e);Ls(n,e);Nf(n,e);Ff(n,e);jf(n,e);Gf(n,e);em(n,e);om(n,e);cm(n,e);Ka(n,e);lm(n);Zc(n,e);um(n,e);km(n,e);yf(n,e);hf(n,e);nf(n,e);Nm(n,e);zm(n,e);Vm(n,e);Qm(n,e);rw(n,e);ew(n,e);pw(n,e);return n}var fv={deviceId:Sf.string().optional().describe('Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.')};export{ji as BROKER_PORT,To as BROKER_URL,Do as DEFAULT_ACTION_TIMEOUT_MS,Mo as DEFAULT_SNAPSHOT_TIMEOUT_MS,qi as DASHBOARD_NAME,ke as ALL_DEVICES,Md as SyncClient,Lo as getUpdateNotice,va as registerGenericTools,Xn as buildAutomationConfig,gr as estimateBatchMs,Zn as rankBatchFromIndex,hn as formatRankingMarkdown,br as compareReportStats,Ia as registerPerfMonitorTools,Ma as registerEventsTools,Aa as registerReactQueryTools,Ea as registerZustandTools,Oa as registerJotaiTools,Na as registerReduxTools,Fa as registerStorageTools,Sr as stopAllHidStreamsNow,Ka as registerScreenshotTools,lp as stopAllRecordingsNow,Fs as registerRoutesTools,Ls as registerConsoleTools,jw as createMcpServer};