@buoy-gg/mcp 7.0.49 → 7.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(e,n)=>function t(){return e&&(n=(0,e[__getOwnPropNames(e)[0]])(e=0)),n};var __export=(e,n)=>{for(var t in n)__defProp(e,t,{get:n[t],enumerable:true})};var __copyProps=(e,n,t,r)=>{if(n&&typeof n==="object"||typeof n==="function"){for(let s of __getOwnPropNames(n))if(!__hasOwnProp.call(e,s)&&s!==t)__defProp(e,s,{get:()=>n[s],enumerable:!(r=__getOwnPropDesc(n,s))||r.enumerable})}return e};var __toESM=(e,n,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(n||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));var START_PROBE_JS,STOP_PROBE_JS,MEASURE_REFRESH_JS;var init_inPageProbe=__esm({"src/webBench/inPageProbe.ts"(){"use strict";START_PROBE_JS=`(() => {
2
+ "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(e,n)=>function t(){return e&&(n=(0,e[__getOwnPropNames(e)[0]])(e=0)),n};var __export=(e,n)=>{for(var t in n)__defProp(e,t,{get:n[t],enumerable:true})};var __copyProps=(e,n,t,r)=>{if(n&&typeof n==="object"||typeof n==="function"){for(let s of __getOwnPropNames(n))if(!__hasOwnProp.call(e,s)&&s!==t)__defProp(e,s,{get:()=>n[s],enumerable:!(r=__getOwnPropDesc(n,s))||r.enumerable})}return e};var __toESM=(e,n,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(n||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));var camera_exports={};__export(camera_exports,{cliPath:()=>cliPath,describeDevices:()=>describeDevices,describeDiagnosis:()=>describeDiagnosis,describeInputs:()=>describeInputs,proRequired:()=>proRequired,readEntitlement:()=>readEntitlement,run:()=>run2});function candidates(){const e=process.env.BUOY_SIMCAM_DIR;const n=[...e?[import_node_path4.default.join(e,"buoycam")]:[],"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",import_node_path4.default.join(import_node_os4.default.homedir(),"Applications/Buoy.app/Contents/Resources/simcam/buoycam")];let t=__dirname;for(let r=0;r<8;r++){n.push(import_node_path4.default.join(t,"apps/desktop/resources/simcam/buoycam"));const s=import_node_path4.default.dirname(t);if(s===t)break;t=s}return n}function cliPath(){if(resolved)return resolved;resolved=candidates().find(e=>(0,import_node_fs4.existsSync)(e))??null;return resolved}async function run2(e){const n=cliPath();if(!n){return{ok:false,error:"The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."}}try{const{stdout:t}=await execFileAsync5(n,[...e,"--json"],{timeout:3e4});return{ok:true,data:JSON.parse(t)}}catch(t){const r=t.stdout??"";try{return{ok:false,data:JSON.parse(r)}}catch{return{ok:false,error:t.message||"the simulator camera CLI failed"}}}}function describeDiagnosis(e){const n=e.filter(o=>o.level==="fail");const t=e.filter(o=>o.level==="warn");if(n.length===0&&t.length===0){return"The simulator camera is healthy \u2014 every check passed."}const r=[];for(const o of[...n,...t]){r.push(`- **${o.name}** (${o.level}): ${o.detail}`);if(o.fix)r.push(` ${o.fix}`)}const s=n.length>0?`${n.length} problem${n.length===1?"":"s"} found.`:`No hard failures, but ${t.length} thing${t.length===1?"":"s"} worth knowing.`;return[s,"",...r].join("\n")}function describeDevices(e,n){if(e.length===0){return"No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._"}const t=[e.length===1?"1 booted simulator:":`${e.length} booted simulators:`,...e.map(r=>`- **${r.name}** (${r.runtime}) \u2014 \`${r.udid}\``)];if(e.length>1&&n===null){t.push("","_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._");return t.join("\n")}if(n===null){t.push("","_Could not read the installed apps._");return t.join("\n")}if(n.length===0){t.push("","No apps installed on it.");return t.join("\n")}t.push("",`${n.length} installed app${n.length===1?"":"s"}:`,...n.map(r=>`- ${r.name} \u2014 \`${r.bundleId}\``),"","_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._");return t.join("\n")}function describeInputs(e,n){const t=[];t.push(e.length?'**Mac cameras** (for `kind: "camera"`):':"**Mac cameras**: none found.");if(e.length){t.push(...e.map(r=>`- ${r.name} \u2014 \`${r.uniqueId}\``))}else{t.push("_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._")}t.push("");t.push(n.length?'**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):':"**Capturable windows**: none found.");if(n.length){t.push(...n.map(r=>`- ${r.app}${r.title?` \u2014 ${r.title}`:""} \u2014 \`window:${r.id}\``))}else{t.push("_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._")}return t.join("\n")}function entitlementPaths(){const e=process.env.BUOY_ENTITLEMENT_FILE;if(e)return[e];return[import_node_path4.default.join(import_node_os4.default.homedir(),"Library/Application Support/Buoy/entitlement.json"),import_node_path4.default.join(import_node_os4.default.homedir(),"Library/Application Support/buoy-desktop/entitlement.json")]}function readEntitlement(){for(const e of entitlementPaths()){try{if(!(0,import_node_fs4.existsSync)(e))continue;const n=JSON.parse((0,import_node_fs4.readFileSync)(e,"utf8"));return{tier:n.tier==="pro"||n.tier==="free"?n.tier:"locked",isPro:n.isPro===true&&n.hasDesktopAccess===true&&typeof n.validUntil==="number"&&Number.isFinite(n.validUntil)&&Date.now()<n.validUntil&&typeof n.proValidUntil==="number"&&Number.isFinite(n.proValidUntil)&&Date.now()<n.proValidUntil}}catch{}}return{tier:"locked",isPro:false}}function proRequired(e){return`\`${e}\` is a Buoy Pro feature.
3
+
4
+ The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel with a Free account. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
5
+
6
+ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`}var import_node_child_process5,import_node_util5,import_node_fs4,import_node_os4,import_node_path4,execFileAsync5,resolved;var init_camera=__esm({"src/capture/camera.ts"(){"use strict";import_node_child_process5=require("child_process");import_node_util5=require("util");import_node_fs4=require("fs");import_node_os4=__toESM(require("os"));import_node_path4=__toESM(require("path"));execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process5.execFile);resolved=null}});var START_PROBE_JS,END_PROBE_JS,COLLECT_PROBE_JS,MEASURE_REFRESH_JS;var init_inPageProbe=__esm({"src/webBench/inPageProbe.ts"(){"use strict";START_PROBE_JS=`(() => {
3
7
  const supported = (t) => {
4
8
  try { return (PerformanceObserver.supportedEntryTypes || []).includes(t); } catch { return false; }
5
9
  };
6
- const frames = [], longTasks = [], loaf = [], shifts = [], observers = [];
7
- const watch = (type, cb) => {
10
+ const frames = [], longTasks = [], loaf = [], shifts = [], events = [], observers = [];
11
+ const watch = (type, cb, extra) => {
8
12
  if (!supported(type)) return false;
9
13
  const o = new PerformanceObserver((l) => l.getEntries().forEach(cb));
10
- o.observe({ type, buffered: false });
14
+ o.observe(Object.assign({ type, buffered: false }, extra || {}));
11
15
  observers.push([o, cb]);
12
16
  return true;
13
17
  };
@@ -26,34 +30,108 @@
26
30
  return { duration: s.duration, name, forced: s.forcedStyleAndLayoutDuration || 0 };
27
31
  }),
28
32
  }));
33
+ // 16 ms is Event Timing's smallest threshold; the default (104) hides most taps.
34
+ const hasEvents = watch("event", (e) => {
35
+ if (e.interactionId) events.push({
36
+ name: e.name, startTime: e.startTime, duration: e.duration,
37
+ processingStart: e.processingStart, processingEnd: e.processingEnd, interactionId: e.interactionId,
38
+ });
39
+ }, { durationThreshold: 16 });
40
+ // Click proof: which step selectors each real click landed inside, read
41
+ // at the moment of the click (capture phase, before the page's handlers).
42
+ const clickTargets = Array.isArray(window.__buoyBenchClickTargets) ? window.__buoyBenchClickTargets : [];
43
+ const clicks = [], keys = [];
44
+ // Only real (trusted) input counts as proof; a click the page fakes in
45
+ // script doesn't. Input after the run's end doesn't count either.
46
+ const onClick = (e) => {
47
+ if (!e.isTrusted || ended) return;
48
+ const t = e.target;
49
+ const hit = clickTargets.filter((sel) => { try { return t && t.closest && t.closest(sel); } catch { return false; } });
50
+ clicks.push({ t: performance.now(), hit });
51
+ };
52
+ const onKey = (e) => {
53
+ if (!e.isTrusted || ended) return;
54
+ keys.push({ t: performance.now(), key: e.key, ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey });
55
+ };
56
+ // Expect steps, judged inside end() so no page work can slip in between
57
+ // the end of the run and the check.
58
+ const expects = Array.isArray(window.__buoyBenchExpects) ? window.__buoyBenchExpects : [];
59
+ let expectResults = null;
60
+ document.addEventListener("click", onClick, true);
61
+ document.addEventListener("keydown", onKey, true);
29
62
  const hasShift = watch("layout-shift", (e) => shifts.push({ startTime: e.startTime, value: e.value, hadRecentInput: e.hadRecentInput }));
30
63
  let hidden = document.visibilityState === "hidden";
31
64
  const onVis = () => { if (document.visibilityState === "hidden") hidden = true; };
32
65
  document.addEventListener("visibilitychange", onVis);
66
+ // Latched: a resize or zoom that's undone before stop still changed the work.
67
+ let resized = false;
68
+ const onResize = () => { resized = true; };
69
+ window.addEventListener("resize", onResize);
70
+ // A pixel-ratio change (moving to another screen) may not fire resize.
71
+ let dprQuery = null;
72
+ try {
73
+ dprQuery = matchMedia("(resolution: " + window.devicePixelRatio + "dppx)");
74
+ dprQuery.addEventListener("change", onResize);
75
+ } catch {}
33
76
  let raf = 0;
34
77
  const tick = (t) => { frames.push(t); raf = requestAnimationFrame(tick); };
35
78
  raf = requestAnimationFrame(tick);
36
79
  const start = performance.now();
80
+ // Trace markers: the driver counts Chrome's frames between these two.
81
+ try { performance.mark("__buoy_bench_start"); } catch {}
37
82
  const domStart = document.getElementsByTagName("*").length;
83
+ let ended = 0;
38
84
  window.__buoyBench = {
39
- stop: () => {
40
- const end = performance.now();
85
+ // Two steps so the driver can read Chrome's counters between them:
86
+ // end() freezes the end time at once; collect() then waits up to two
87
+ // frames (capped at 300 ms) so an input's entry, which only arrives after
88
+ // its paint, isn't lost. Entries after the end are filtered out later.
89
+ end: () => {
90
+ if (ended) return ended;
91
+ ended = performance.now();
92
+ try { performance.mark("__buoy_bench_end"); } catch {}
41
93
  cancelAnimationFrame(raf);
42
- document.removeEventListener("visibilitychange", onVis);
43
- for (const [o, cb] of observers) { o.takeRecords().forEach(cb); o.disconnect(); }
44
- return {
45
- start, end, frames,
46
- longTasks: hasLong ? longTasks : null,
47
- loaf: hasLoaf ? loaf : null,
48
- shifts: hasShift ? shifts : null,
49
- domStart,
50
- domEnd: document.getElementsByTagName("*").length,
51
- hidden,
52
- };
94
+ expectResults = expects.map((x) => {
95
+ const n = document.querySelector(x.selector);
96
+ const full = n ? (n.textContent || "") : null;
97
+ // Match against the full text here; only a short snippet goes back.
98
+ return { selector: x.selector, text: x.text == null ? null : x.text, found: !!n, matched: full != null && (x.text == null || full.includes(x.text)), content: full == null ? null : full.slice(0, 200) };
99
+ });
100
+ return ended;
53
101
  },
102
+ collect: () => new Promise((resolve) => {
103
+ const end = window.__buoyBench.end();
104
+ let done = false;
105
+ const finish = () => {
106
+ if (done) return;
107
+ done = true;
108
+ document.removeEventListener("visibilitychange", onVis);
109
+ window.removeEventListener("resize", onResize);
110
+ document.removeEventListener("click", onClick, true);
111
+ document.removeEventListener("keydown", onKey, true);
112
+ try { dprQuery && dprQuery.removeEventListener("change", onResize); } catch {}
113
+ for (const [o, cb] of observers) { o.takeRecords().forEach(cb); o.disconnect(); }
114
+ resolve({
115
+ start, end, frames,
116
+ longTasks: hasLong ? longTasks : null,
117
+ loaf: hasLoaf ? loaf : null,
118
+ shifts: hasShift ? shifts : null,
119
+ events: hasEvents ? events : null,
120
+ clicks,
121
+ keys,
122
+ expectResults,
123
+ domStart,
124
+ domEnd: document.getElementsByTagName("*").length,
125
+ hidden,
126
+ resized,
127
+ });
128
+ };
129
+ requestAnimationFrame(() => requestAnimationFrame(finish));
130
+ setTimeout(finish, 300);
131
+ }),
54
132
  };
55
133
  return true;
56
- })()`;STOP_PROBE_JS=`(() => (window.__buoyBench ? window.__buoyBench.stop() : null))()`;MEASURE_REFRESH_JS=`new Promise((resolve) => {
134
+ })()`;END_PROBE_JS=`(() => (window.__buoyBench ? window.__buoyBench.end() : null))()`;COLLECT_PROBE_JS=`(() => (window.__buoyBench ? window.__buoyBench.collect() : null))()`;MEASURE_REFRESH_JS=`new Promise((resolve) => {
57
135
  let n = 0, t0 = 0;
58
136
  const tick = (t) => {
59
137
  if (!t0) t0 = t;
@@ -62,63 +140,64 @@
62
140
  else requestAnimationFrame(tick);
63
141
  };
64
142
  requestAnimationFrame(tick);
65
- })`}});var webBench_exports={};__export(webBench_exports,{PLAYWRIGHT_MISSING:()=>PLAYWRIGHT_MISSING,aggregateCase:()=>aggregateCase,caseUrl:()=>caseUrl,cdpDeltas:()=>cdpDeltas,formatWebBenchMarkdown:()=>formatWebBenchMarkdown,oneLine:()=>oneLine,percentile:()=>percentile,runWebBench:()=>runWebBench,shuffled:()=>shuffled,snapRefreshRate:()=>snapRefreshRate,summarizeProbe:()=>summarizeProbe,webBenchFlags:()=>webBenchFlags});function percentile(e,n){if(e.length===0)return null;const t=[...e].sort((s,o)=>s-o);const r=Math.ceil(n/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,r))]}function snapRefreshRate(e){for(const n of COMMON_RATES)if(Math.abs(e-n)/n<=.08)return n;return Math.max(1,Math.round(e))}function shuffled(e,n){let t=n>>>0;const r=()=>{t=t+1831565813>>>0;let o=t;o=Math.imul(o^o>>>15,o|1);o^=o+Math.imul(o^o>>>7,o|61);return((o^o>>>14)>>>0)/4294967296};const s=[...e];for(let o=s.length-1;o>0;o--){const i=Math.floor(r()*(o+1));[s[o],s[i]]=[s[i],s[o]]}return s}function caseUrl(e,n,t){const r=new URL(e);const s=n.path??t;if(s)r.pathname=s.startsWith("/")?s:`${r.pathname.replace(/\/$/,"")}/${s}`;for(const[o,i]of Object.entries(n.params??{}))r.searchParams.set(o,i);return r.toString()}function summarizeProbe(e,n){const{start:t,end:r}=e;const s=Math.max(1,r-t);const o=[];for(let v=1;v<e.frames.length;v++)o.push(e.frames[v]-e.frames[v-1]);const i=1e3/n;const a=percentile(o,95);let c=null;if(e.longTasks){let v=0;for(const m of e.longTasks){const g=Math.min(m.end,r)-Math.max(m.start,t);if(g>0)v+=g}c=r1(Math.min(100,v/s*100))}let u=null;let l=null;let d=null;let h=[];if(e.loaf){let v=0;let m=0;let g=0;const p=new Map;for(const f of e.loaf){if(f.startTime<t||f.startTime>r)continue;v+=f.blockingDuration;if(f.styleAndLayoutStart>f.startTime)m+=Math.max(0,f.startTime+f.duration-f.styleAndLayoutStart);for(const w of f.scripts){g+=w.forced;const b=p.get(w.name)??{ms:0,count:0};b.ms+=w.duration;b.count++;p.set(w.name,b)}}u=r1(v);l=r1(m);d=r1(g);h=[...p.entries()].map(([f,w])=>({name:f,ms:r1(w.ms),count:w.count})).sort((f,w)=>w.ms-f.ms).slice(0,5)}let y=null;if(e.shifts){let v=0;for(const m of e.shifts)if(!m.hadRecentInput&&m.startTime>=t&&m.startTime<=r)v+=m.value;y=Math.round(v*1e3)/1e3}return{fps:e.frames.length>1?r1((e.frames.length-1)/s*1e3):null,frameGapP95Ms:a==null?null:r1(a),frameGapMaxMs:o.length?r1(Math.max(...o)):null,longFrames:o.filter(v=>v>i*1.5).length,longTaskPct:c,loafBlockingMs:u,styleLayoutMs:l,forcedLayoutMs:d,topScripts:h,layoutShift:y,domNodesStart:e.domStart,domNodesEnd:e.domEnd}}function cdpDeltas(e,n,t){const r=i=>i in e&&i in n?n[i]-e[i]:null;const s=i=>{const a=r(i);return a==null?null:r1(a*1e3)};const o=s("TaskDuration");return{mainThreadPct:o==null?null:r1(Math.min(100,o/Math.max(1,t)*100)),scriptMs:s("ScriptDuration"),styleMs:s("RecalcStyleDuration"),layoutMs:s("LayoutDuration"),layoutCount:r("LayoutCount"),styleCount:r("RecalcStyleCount"),heapMb:"JSHeapUsedSize"in n?r1(n.JSHeapUsedSize/(1024*1024)):null}}function webBenchFlags(e,n,t){const r=[];if(e.fps!=null&&e.fps<n*.95)r.push("Main thread drops frames");if(e.frameGapMaxMs!=null&&e.frameGapMaxMs>=100)r.push(`Freeze of ${Math.round(e.frameGapMaxMs)} ms`);if(e.mainThreadPct!=null&&e.mainThreadPct>=80)r.push("Main thread near full");if(e.longTaskPct!=null&&e.longTaskPct>=20)r.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50)r.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`);if(e.layoutShift!=null&&e.layoutShift>=.1)r.push(`Layout shifts (${e.layoutShift})`);if(t)r.push(`DOM grows each run (+${t} nodes)`);return r}function aggregateCase(e,n){const t=e.filter(a=>!a.failed&&a.fps!=null);const r=[...t].sort((a,c)=>(a.fps??0)-(c.fps??0));const s=r.length?r[Math.floor((r.length-1)/2)]:null;const o=[...t].sort((a,c)=>a.round-c.round).map(a=>a.domNodesEnd).filter(a=>a!=null);let i=null;if(o.length>=3&&o.every((a,c)=>c===0||a>o[c-1])){const a=o[o.length-1]-o[0];if(a>=Math.max(50,o[0]*.05))i=a}return{caseName:e[0]?.caseName??"",cpu:e[0]?.cpu??1,validRuns:t.length,runs:e,median:s,fpsMin:r.length?r[0].fps:null,fpsMax:r.length?r[r.length-1].fps:null,domGrowth:i,flags:s?webBenchFlags(s,n,i):[]}}async function loadChromium(){const e=(0,import_node_module.createRequire)((0,import_node_path5.join)(process.cwd(),"noop.js"));for(const n of["playwright","playwright-core"]){try{const t=await import((0,import_node_url.pathToFileURL)(e.resolve(n)).href);const r=t.chromium??t.default?.chromium;if(r)return r}catch{}}throw new Error(PLAYWRIGHT_MISSING)}function oneLine(e){return e.replace(/\u001b\[[0-9;]*m/g,"").split("\n")[0].trim().slice(0,160)}async function readMetrics(e){const{metrics:n}=await e.send("Performance.getMetrics");return Object.fromEntries(n.map(t=>[t.name,t.value]))}async function runWebBench(e,n=()=>{}){if(e.cases.length===0)throw new Error("Give at least one case.");const t=Math.max(1,Math.min(10,e.runsPerCase??3));const r=Math.max(500,e.durationMs??5e3);const s=Math.max(0,e.settleMs??1e3);const o=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map(v=>Math.max(1,v));let i=e.refreshHz??60;const a={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const c=[];const u=[];if(t<2)c.push("runsPerCase=1 gives no spread; use 3 or more before choosing.");const l=await loadChromium();const d=await l.launch({headless:!e.headed});const h=[];try{const v=await d.newContext({viewport:{width:a.width,height:a.height},deviceScaleFactor:a.deviceScaleFactor});const m=async(g,p,f,w)=>{const b=await v.newPage();const _=[];b.on("pageerror",$=>_.push(String($?.message??$)));const k={caseName:g.name,cpu:p,round:f,fps:null,frameGapP95Ms:null,frameGapMaxMs:null,longFrames:0,longTaskPct:null,loafBlockingMs:null,styleLayoutMs:null,forcedLayoutMs:null,topScripts:[],layoutShift:null,domNodesStart:null,domNodesEnd:null,mainThreadPct:null,scriptMs:null,styleMs:null,layoutMs:null,layoutCount:null,styleCount:null,heapMb:null,pageErrors:_};try{const $=await v.newCDPSession(b);await $.send("Performance.enable",{timeDomain:"timeTicks"});if(p>1)await $.send("Emulation.setCPUThrottlingRate",{rate:p});const A=await b.goto(caseUrl(e.url,g,e.path),{waitUntil:"load",timeout:3e4});if(A&&A.status()>=400)return{...k,failed:`HTTP ${A.status()}`};if(e.readySelector)await b.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep2(s);if(w){await $.send("Tracing.start",{transferMode:"ReturnAsStream",categories:["devtools.timeline","disabled-by-default-devtools.timeline","disabled-by-default-devtools.timeline.frame","blink.user_timing","loading","latencyInfo","v8.execute"].join(",")})}const R=await readMetrics($);const x=Date.now();await b.evaluate(START_PROBE_JS);await sleep2(r);const S=await b.evaluate(STOP_PROBE_JS);const T=await readMetrics($);const M=Date.now()-x;if(w){const O=new Promise(D=>$.on("Tracing.tracingComplete",C=>D(C.stream)));await $.send("Tracing.end");const z=await O;let I="";for(;;){const D=await $.send("IO.read",{handle:z});I+=D.base64Encoded?Buffer.from(D.data,"base64").toString("utf8"):D.data;if(D.eof)break}await $.send("IO.close",{handle:z});(0,import_node_fs5.writeFileSync)(w,I);u.push(w)}if(!S)return{...k,failed:"probe returned nothing (page navigated away?)"};if(S.hidden)return{...k,failed:"page was hidden during the run"};return{...k,...summarizeProbe(S,i),...cdpDeltas(R,T,M)}}catch($){return{...k,failed:oneLine($?.message??String($))}}finally{await b.close().catch(()=>{})}};if(e.refreshHz==null){const g=await v.newPage();try{await g.goto("about:blank");const p=await g.evaluate(MEASURE_REFRESH_JS);i=snapRefreshRate(p);n(`idle frame rate ${p.toFixed(1)} \u2192 ${i} Hz`)}catch{c.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await g.close().catch(()=>{})}}n("warmup load");await m(e.cases[0],1,-1);for(const g of o){for(let p=0;p<t;p++){const f=shuffled(e.cases,2654435769^p*7919+g);for(const w of f){n(`cpu ${g}x \xB7 round ${p+1}/${t} \xB7 ${w.name}`);h.push(await m(w,g,p))}}}if(e.traceDir){(0,import_node_fs5.mkdirSync)(e.traceDir,{recursive:true});for(const g of e.cases){const p=(0,import_node_path5.join)(e.traceDir,`${g.name.replace(/[^\w.-]+/g,"_")}.json`);n(`trace \xB7 ${g.name}`);await m(g,o[0],-1,p)}}await v.close()}finally{await d.close().catch(()=>{})}const y=[];for(const v of o){for(const m of e.cases){y.push(aggregateCase(h.filter(g=>g.cpu===v&&g.caseName===m.name),i))}}if(h.some(v=>v.pageErrors.length))c.push("Some runs threw page errors; check them before trusting those cases.");return{url:e.url,browser:`Chromium ${d.version()}${e.headed?"":" (headless)"}`,viewport:a,refreshHz:i,durationMs:r,runsPerCase:t,results:y,traces:u,warnings:c}}function formatWebBenchMarkdown(e){const n=(s,o=1)=>s==null?"\u2014":s.toFixed(o);const t=[];t.push(`### Web benchmark \xB7 ${e.url}`);t.push(`${e.browser} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 ${e.runsPerCase} runs \xD7 ${e.durationMs/1e3}s per case, fresh page each run, case order shuffled each round.`);t.push("");t.push(`**No single winner \u2014 weigh the trade-offs.** FPS and frame gaps come from the page's main thread. Main thread % is Chrome's own task time for the run. Desktop numbers, even with CPU slowdown, don't stand in for a real phone's GPU, memory or heat.`);const r=Array.from(new Set(e.results.map(s=>s.cpu)));for(const s of r){t.push("");t.push(s===1?`#### No CPU slowdown`:`#### CPU slowed ${s}x`);t.push("");t.push(`| Case | FPS (range) | p95 gap ms | Worst gap ms | Long frames | Main thread % | Script ms | Style ms | Layout ms | Layouts | DOM nodes | Heap MB | Flags |`);t.push(`|------|------------:|-----------:|-------------:|------------:|--------------:|----------:|---------:|----------:|--------:|----------:|--------:|-------|`);for(const a of e.results.filter(c=>c.cpu===s)){const c=a.median;if(!c){const d=a.runs.find(h=>h.failed)?.failed??"no valid runs";t.push(`| ${a.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${d}) |`);continue}const u=a.fpsMin!=null&&a.fpsMax!=null&&a.validRuns>1?` (${n(a.fpsMin)}\u2013${n(a.fpsMax)})`:"";const l=a.flags.length?`\u26A0\uFE0F ${a.flags.join("; ")}`:"\u2713 clean";t.push(`| ${a.caseName} | ${n(c.fps)}${u} | ${n(c.frameGapP95Ms)} | ${n(c.frameGapMaxMs)} | ${c.longFrames} | ${n(c.mainThreadPct)} | ${n(c.scriptMs,0)} | ${n(c.styleMs,0)} | ${n(c.layoutMs,0)} | ${n(c.layoutCount,0)} | ${n(c.domNodesEnd,0)} | ${n(c.heapMb)} | ${l} |`)}const o=e.results.filter(a=>a.cpu===s&&a.median?.fps!=null).sort((a,c)=>c.median.fps-a.median.fps);if(o.length>=2&&o[0].validRuns>1&&o[1].validRuns>1&&(o[1].fpsMax??0)>=(o[0].fpsMin??0)){t.push("");t.push(`**Too close to call:** ${o[0].caseName} and ${o[1].caseName} overlap across runs. Use more runs before picking.`)}const i=e.results.filter(a=>a.cpu===s&&a.median&&a.median.topScripts.length).map(a=>`- **${a.caseName}**: ${a.median.topScripts.slice(0,3).map(c=>`${c.name} ${c.ms} ms`).join(", ")}`);if(i.length){t.push("");t.push(`Scripts in slow frames (Long Animation Frames):`);t.push(...i)}}if(e.traces.length){t.push("");t.push(`Chrome traces (open in DevTools \u2192 Performance \u2192 Load profile): ${e.traces.join(", ")}`)}if(e.warnings.length){t.push("");t.push(...e.warnings.map(s=>`\u26A0\uFE0F ${s}`))}return t.join("\n")}var import_node_module,import_node_path5,import_node_fs5,import_node_url,r1,COMMON_RATES,PLAYWRIGHT_MISSING,sleep2;var init_webBench=__esm({"src/webBench/webBench.ts"(){"use strict";import_node_module=require("module");import_node_path5=require("path");import_node_fs5=require("fs");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;COMMON_RATES=[30,60,75,90,120,144,165,240];PLAYWRIGHT_MISSING="Playwright isn't installed in this project. Install it, then run again:\n npm i -D playwright && npx playwright install chromium";sleep2=e=>new Promise(n=>setTimeout(n,e))}});var import_stdio=require("@modelcontextprotocol/sdk/server/stdio.js");var import_node_fs=require("fs");var import_node_path=require("path");var import_node_os=require("os");var import_node_crypto=require("crypto");var import_headless=require("@buoy-gg/license/headless");var KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];function readProjectKey(e,n=process.env){for(const t of KEY_NAMES)if(n[t]!==void 0)return(0,import_headless.normalizeLicenseKey)(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let r;try{r=(0,import_node_fs.readFileSync)((0,import_node_path.join)(e,t),"utf8")}catch{continue}const s=new Map;for(const o of r.split(/\r?\n/)){const i=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!i)continue;const a=i[2].trim();const c=a.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);s.set(i[1],c?c[2]:a.split("#")[0].trim())}for(const o of KEY_NAMES)if(s.has(o))return(0,import_headless.normalizeLicenseKey)(s.get(o))||null}return null}var NodeAccount=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=(0,import_node_path.resolve)(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??(0,import_node_path.join)((0,import_node_os.homedir)(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:(0,import_headless.createNodeSession)({audience:import_headless.CLI_AUDIENCE,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return(0,import_headless.resolveAccountAccess)({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((0,import_node_fs.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;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=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const n=this.generation;try{if((0,import_headless.isDevToken)(e)){const a=(0,import_headless.checkDevToken)(e);if(!a.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),a.claims.iat*1e3);const u=a.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${a.claims.sid}`,valid:true,tier:u,isPro:u==="pro",expiresAt:new Date(a.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}if((0,import_headless.isServiceKey)(e)){const a=await(0,import_headless.exchangeServiceKey)(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;if(!a.ok){if(a.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL}else{this.nextCheck=Date.now()+(import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]??import_headless.LICENSE_REVALIDATION_INTERVAL)}return}const c=Date.now();const u=a.claims.exp*1e3;this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`service:${a.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(u).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,u-60*60*1e3);return}if((0,import_headless.isBuoyToken)(e)){const a=(0,import_headless.verifyFreeToken)(e);if(!a){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${a.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;const r=t.data?.relationships?.product?.data?.id;const s=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(r&&r!==import_headless.KEYGEN_CONFIG.productId||!s&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const o=s?"free":(0,import_headless.resolveTier)(t.data);const i=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:s?null:t.data.attributes.expiry,cachedAt:i,lastValidatedAt:i});this.persistCache();this.retry=0;this.nextCheck=i+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(n!==this.generation)return;const r=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(r??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs.mkdirSync)((0,import_node_path.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs.unlinkSync)(this.cacheFile)}catch{}}};var import_node_http=__toESM(require("http"));var import_node_crypto2=require("crypto");var import_socket=require("socket.io-client");var import_sync_broker2=require("@buoy-gg/sync-broker");var import_sync_broker=require("@buoy-gg/sync-broker");var BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;var BROKER_URL=`http://localhost:${BROKER_PORT}`;var DEFAULT_ACTION_TIMEOUT_MS=15e3;var UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;var UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;var RECOVERY_PROBE_MS=2e4;var UNRESPONSIVE_AFTER_MS=3e4;var DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;var DEVICE_RETENTION_MS=15*6e4;var DASHBOARD_NAME="Dashboard";var ALL_DEVICES="All";function isRecoveryAction(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.deviceConnections=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
66
- `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);try{(0,import_sync_broker2.writeBrokerToken)(this.broker.dashboardToken,(0,import_sync_broker2.brokerTokenPath)(this.port))}catch(t){this.log(`could not write the broker token file: ${t.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 new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:a=>{const c=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));a(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const s=()=>{if(r)return;r=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const c=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(c);if(c){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const i=a=>a.message===import_sync_broker2.DASHBOARD_TOKEN_REQUIRED;t.once("connect_error",a=>{if(r)return;r=true;clearTimeout(o);n(new Error(i(a)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${(0,import_sync_broker2.brokerTokenPath)(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}: ${a.message}`))});t.on("connect_error",a=>{if(!i(a)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let c=false;for(const l of a){if(l.deviceId&&l.tier==="locked")this.lockDevice(String(l.deviceId));else if(l.deviceId&&(l.tier==="free"||l.tier==="pro")){c=this.lockedDevices.delete(String(l.deviceId))||c}}this.devices=a;const u=Date.now();for(const l of a)if(l.deviceId)this.lastSeen.set(l.deviceId,u);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(a)});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const c=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(c,a.payload);this.snapshotAt.set(c,Date.now());const u=this.snapshotWaiters.get(c);if(u&&u.length){this.snapshotWaiters.set(c,[]);for(const l of u)l(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const c=this.pendingActions.get(a.requestId);if(!c)return;this.pendingActions.delete(a.requestId);clearTimeout(c.timer);c.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const s of r)s(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===ALL_DEVICES){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(r)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(r))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=this.connectedDevices();const t=n.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(n.length>0){throw new Error(`Devices are connected (${n.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const r=t.filter(o=>o.tools.length>0);if(r.length===1)return r[0].deviceId;if(r.length===0){const o=t.map(i=>i.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=r.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const n=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${n(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const r=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===r);if(!s?.isPro)throw new Error(t(s));return r}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(o)r.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=diagnoseCrashed({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const n=this.snapshots.get(snapKey(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const s=t[r];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(snapKey(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){this.devServerOrigins.set(e,n)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=snapKey(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 r=`${n.id}|${n.connectionNonce??""}`;const s=this.deviceConnections.get(t);this.deviceConnections.set(t,r);if(s===void 0||s===r)continue;for(const{toolId:o,targetDeviceId:i}of this.watching.values()){if(i===t)this.sendWatch(o,i)}}}unwatch(e,n){const t=snapKey(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const s=snapKey(r,e);this.ensureWatching(e,r);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const u=this.healthOf(r);if(Date.now()-u.lastFullBudgetAt>RECOVERY_PROBE_MS){u.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const i=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const a=await i;this.requireAdmission();this.requireDeviceAccount(r);if(a!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:a}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const i=(this.snapshotWaiters.get(e)??[]).filter(a=>a!==o);this.snapshotWaiters.set(e,i);t(void 0)},n);const o=i=>{clearTimeout(s);t(i)};r.push(o);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,s={}){this.requireAdmission();const o=r===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const i=(0,import_node_crypto2.randomUUID)();const a=s.timeoutMs??this.timeoutFor(o,e,n);const c=await new Promise((u,l)=>{const d=setTimeout(()=>{this.pendingActions.delete(i);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);l(new Error(`Action "${e}.${n}" got no answer in ${a}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},a);this.pendingActions.set(i,{resolve:u,reject:l,timer:d,target:o});this.socket.emit("devtool-action",{requestId:i,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};var snapKey=(e,n)=>`${e}::${n}`;function probeBroker(e,n=800){return new Promise(t=>{const r=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function waitForBroker(e,n=20){for(let t=0;t<n;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(n=>setTimeout(n,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod31=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.49"?"7.0.49":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,n){const t=o=>o.split("-")[0].split(".").map(i=>Number(i)||0);const r=t(e);const s=t(n);for(let o=0;o<3;o++){const i=r[o]??0;const a=s[o]??0;if(i>a)return true;if(i<a)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function fetchLatest(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),FETCH_TIMEOUT_MS);try{const r=await fetch(REGISTRY_URL,{signal:n.signal});if(!r.ok)return null;const s=await r.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const n=await readCache();if(n&&e-n.checkedAt<CHECK_TTL_MS)return n.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const n=await resolveLatest(e);if(n&&isNewer(n,VERSION)){return`A newer buoy-mcp is available (${n}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,n){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const r=n.listDevices();if(r.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=r.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const i=s.some(c=>c.tier!=="locked");const a=s.filter(c=>c.collision);return json({devices:r,...a.length>0?{warning:`${a.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:i?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const r=n.toolActions(t);if(r.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:r})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:r,timeoutMs:s})=>{try{const o=n.requireProDevice(r);const{payload:i}=await n.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:i})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:r,params:s,deviceId:o,timeoutMs:i})=>{try{const a=n.requireProDevice(o);const c=await n.callAction(t,r,s,a,{timeoutMs:i});return json({toolId:t,action:r,ok:true,result:c??null})}catch(a){return fail(errMsg(a))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,n){return typeof e==="number"&&e>0?e:n}function staleHeader(e){return e?`${e}
143
+ })`}});var webBench_exports={};__export(webBench_exports,{PLAYWRIGHT_MISSING:()=>PLAYWRIGHT_MISSING,aggregateCase:()=>aggregateCase,caseUrl:()=>caseUrl,cdpDeltas:()=>cdpDeltas,checkBudgets:()=>checkBudgets,closeCall:()=>closeCall,expectFailureFrom:()=>expectFailureFrom,formatWebBenchMarkdown:()=>formatWebBenchMarkdown,keyMatches:()=>keyMatches,locateStepTargets:()=>locateStepTargets,mergeInteractions:()=>mergeInteractions,missedInput:()=>missedInput,oneLine:()=>oneLine,percentile:()=>percentile,runWebBench:()=>runWebBench,shuffled:()=>shuffled,snapRefreshRate:()=>snapRefreshRate,summarizeFrameTrace:()=>summarizeFrameTrace,summarizeProbe:()=>summarizeProbe,takeHostLock:()=>takeHostLock,tiedPairs:()=>tiedPairs,validateWebBenchOptions:()=>validateWebBenchOptions,webBenchFlags:()=>webBenchFlags});function checkBudgets(e,n,t=[]){const r=e.median;const s=[];const o=[];const a=(h,d,m)=>{if(d==null)return;if(m==null)o.push(h);else if(m>d)s.push(`${h} ${m} > ${d}`)};a("droppedPct",n.droppedPct,e.droppedMin!=null?r?.droppedPct:null);const i=e.runs.filter(h=>!h.failed&&h.fps!=null);const c=t.some(h=>"click"in h||"press"in h||"type"in h);const u=i.map(h=>h.interactions==null||!h.inputCaptureComplete?null:h.worstInteractionMs??16);a("inputMs",n.inputMs,c&&u.length&&u.every(h=>h!=null)?Math.max(...u):null);const l=i.map(h=>Math.max(h.frameGapMaxMs??0,h.edgeSilenceMs??0));a("freezeMs",n.freezeMs,l.length?Math.max(...l):null);a("mainThreadPct",n.mainThreadPct,r?.mainThreadPct);a("jsHeapMb",n.jsHeapMb,r?.heapMb);return{meets:Boolean(r)&&s.length===0&&o.length===0,over:s,unknown:o}}function percentile(e,n){if(e.length===0)return null;const t=[...e].sort((s,o)=>s-o);const r=Math.ceil(n/100*t.length)-1;return t[Math.min(t.length-1,Math.max(0,r))]}function snapRefreshRate(e){for(const n of COMMON_RATES)if(Math.abs(e-n)/n<=.08)return n;return Math.max(1,Math.round(e))}function shuffled(e,n){let t=n>>>0;const r=()=>{t=t+1831565813>>>0;let o=t;o=Math.imul(o^o>>>15,o|1);o^=o+Math.imul(o^o>>>7,o|61);return((o^o>>>14)>>>0)/4294967296};const s=[...e];for(let o=s.length-1;o>0;o--){const a=Math.floor(r()*(o+1));[s[o],s[a]]=[s[a],s[o]]}return s}function caseUrl(e,n,t){const r=new URL(e);const s=n.path??t;if(s)r.pathname=s.startsWith("/")?s:`${r.pathname.replace(/\/$/,"")}/${s}`;for(const[o,a]of Object.entries(n.params??{}))r.searchParams.set(o,a);return r.toString()}function summarizeFrameTrace(e){const n=m=>e.find(w=>w.name===m&&w.ts!=null);const t=n("__buoy_bench_start");const r=n("__buoy_bench_end");if(!t||!r||r.ts<=t.ts)return null;if(t.pid!==r.pid)return null;const s=t.pid;const o={STATE_PRESENTED_ALL:0,STATE_PRESENTED_PARTIAL:1,STATE_DROPPED:2};const a=new Map;for(const m of e){if(m.name!=="PipelineReporter"||m.ph!=="b")continue;if(s!=null&&m.pid!==s)continue;if(m.ts==null||m.ts<t.ts||m.ts>r.ts)continue;const w=m.args?.frame_reporter;if(!w||w.frame_sequence==null)continue;const g=w.state;if(!FRAME_STATES.includes(g))continue;const b=`${w.layer_tree_host_id??""}:${w.frame_source??""}:${w.frame_sequence}`;const p=Boolean(w.affects_smoothness)&&g!=="STATE_PRESENTED_ALL";const f=a.get(b);if(!f)a.set(b,{state:g,fault:p});else{if(o[g]>o[f.state])f.state=g;f.fault=f.fault||p}}let i=0;let c=0;let u=0;let l=0;for(const m of a.values()){if(m.state==="STATE_PRESENTED_ALL")i++;else if(m.state==="STATE_PRESENTED_PARTIAL")c++;else u++;if(m.fault)l++}const h=i+c+u;if(h===0)return null;const d=(r.ts-t.ts)/1e6;return{full:i,partial:c,dropped:u,faults:l,shownFps:r1((i+c)/d),droppedPct:r1(l/h*100)}}function keyMatches(e,n){const t=e.split("+");const r=t[t.length-1];const s=new Set(t.slice(0,-1).map(i=>i.toLowerCase()));if(n.key!==r)return false;const o=r.toLowerCase();const a={ctrl:s.has("control")||o==="control",alt:s.has("alt")||o==="alt",meta:s.has("meta")||s.has("command")||o==="meta",shift:s.has("shift")||o==="shift"};if(Boolean(n.ctrl)!==a.ctrl||Boolean(n.alt)!==a.alt||Boolean(n.meta)!==a.meta)return false;if(!a.shift&&n.shift&&r.length!==1)return false;if(a.shift&&!n.shift)return false;return true}function mergeInteractions(e){const n=[];for(const t of e){const r=t.startTime+t.duration;const s=n.find(a=>a.interactionId===t.interactionId&&Math.abs(a.startTime+a.duration-r)<=8);if(!s){n.push({...t});continue}const o=s.startTime+s.duration;s.startTime=Math.min(s.startTime,t.startTime);s.processingStart=Math.min(s.processingStart,t.processingStart);s.processingEnd=Math.max(s.processingEnd,t.processingEnd);s.duration=Math.max(o,r)-s.startTime;if(t.processingEnd-t.processingStart>0)s.name=t.name}return n}function summarizeProbe(e,n){const{start:t,end:r}=e;const s=Math.max(1,r-t);const o=[];for(let _=1;_<e.frames.length;_++)o.push(e.frames[_]-e.frames[_-1]);const a=1e3/n;const i=percentile(o,95);let c=null;if(e.longTasks){let _=0;for(const M of e.longTasks){const B=Math.min(M.end,r)-Math.max(M.start,t);if(B>0)_+=B}c=r1(Math.min(100,_/s*100))}let u=null;let l=null;let h=null;let d=[];let m=0;if(e.loaf){let _=0;let M=0;let B=0;const I=new Map;for(const x of e.loaf){if(x.startTime>=t&&x.startTime<=r&&x.startTime+x.duration>r)m++;if(x.startTime<t||x.startTime+x.duration>r)continue;_+=x.blockingDuration;if(x.styleAndLayoutStart>x.startTime)M+=Math.max(0,x.startTime+x.duration-x.styleAndLayoutStart);for(const $ of x.scripts){B+=$.forced;const T=I.get($.name)??{ms:0,count:0};T.ms+=$.duration;T.count++;I.set($.name,T)}}u=r1(_);l=r1(M);h=r1(B);d=[...I.entries()].map(([x,$])=>({name:x,ms:r1($.ms),count:$.count})).sort((x,$)=>$.ms-x.ms).slice(0,5)}let w=null;if(e.shifts){let _=0;for(const M of e.shifts)if(!M.hadRecentInput&&M.startTime>=t&&M.startTime<=r)_+=M.value;w=Math.round(_*1e3)/1e3}let g=null;let b=null;if(e.events){const _=mergeInteractions(e.events.filter(M=>M.startTime>=t&&M.startTime<=r));g=new Set(_.map(M=>M.interactionId)).size;for(const M of _)if(!b||M.duration>b.duration)b=M}const p=b;const f=e.frames;const v=[...(e.clicks??[]).map(_=>_.t),...(e.keys??[]).map(_=>_.t)];const y=v.length?Math.max(...v):null;const k=y==null||f.filter(_=>_>y).length>=2;const S=f.length===0?s:Math.max(0,f[0]-t,r-f[f.length-1]);return{edgeSilenceMs:r1(S),frameGapCount:o.length,inputCaptureComplete:k,resized:Boolean(e.resized),interactions:g,worstInteractionMs:p?r1(p.duration):null,worstInputDelayMs:p?r1(Math.max(0,p.processingStart-p.startTime)):null,worstProcessingMs:p?r1(Math.max(0,p.processingEnd-p.processingStart)):null,worstPresentationMs:p?r1(Math.max(0,p.startTime+p.duration-p.processingEnd)):null,fps:r1(e.frames.length/s*1e3),frameGapP95Ms:i==null?null:r1(i),frameGapMaxMs:o.length?r1(Math.max(...o)):null,longFrames:o.filter(_=>_>a*1.5).length,longTaskPct:c,loafBlockingMs:u,styleLayoutMs:l,forcedLayoutMs:h,topScripts:d,loafCutOff:m,layoutShift:w,domNodesStart:e.domStart,domNodesEnd:e.domEnd}}function cdpDeltas(e,n,t){const r=i=>i in e&&i in n?n[i]-e[i]:null;const s=i=>{const c=r(i);return c==null?null:r1(c*1e3)};const o=s("TaskDuration");const a=s("Timestamp")??t;return{mainThreadPct:o==null?null:r1(o/Math.max(1,a)*100),scriptMs:s("ScriptDuration"),styleMs:s("RecalcStyleDuration"),layoutMs:s("LayoutDuration"),layoutCount:r("LayoutCount"),styleCount:r("RecalcStyleCount"),heapMb:"JSHeapUsedSize"in n?r1(n.JSHeapUsedSize/(1024*1024)):null}}function webBenchFlags(e,n){const t=[];if(e.droppedPct!=null&&e.droppedPct>=5)t.push(`${e.droppedPct}% of frames dropped or partial`);if(e.fps!=null&&e.fps<n*.95)t.push("Low main-thread frame rate");if(e.frameGapCount&&e.longFrames/e.frameGapCount>=.02)t.push("Frequent long frames");const r=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(r>=100)t.push(`Freeze of ${Math.round(r)} ms`);if(e.mainThreadPct!=null&&e.mainThreadPct>=80)t.push("Main thread near full");if(e.longTaskPct!=null&&e.longTaskPct>=20)t.push("Long tasks block the main thread");if(e.forcedLayoutMs!=null&&e.forcedLayoutMs>=50)t.push(`Forced layout in scripts (${Math.round(e.forcedLayoutMs)} ms)`);if(e.layoutShift!=null&&e.layoutShift>=.1)t.push(`Layout shifts (${e.layoutShift})`);if(e.worstInteractionMs!=null&&e.worstInteractionMs>=200){t.push(`Slow response to input (${Math.round(e.worstInteractionMs)} ms)`)}return t}function aggregateCase(e,n){const t=e.filter(c=>!c.failed&&c.fps!=null);const r=[...t].sort((c,u)=>(c.fps??0)-(u.fps??0));const s=t.filter(c=>c.droppedPct!=null).length;const o=t.length>0&&s===t.length;const a=o?[...t].sort((c,u)=>c.droppedPct-u.droppedPct):r;const i=a.length?a[Math.floor((a.length-1)/2)]:null;return{caseName:e[0]?.caseName??"",cpu:e[0]?.cpu??1,validRuns:t.length,plannedRuns:e.length,isBaseline:false,deltaDroppedPct:null,tracedRuns:s,...(()=>{const c=o?t.map(l=>l.droppedPct):[];const u=t.map(l=>l.worstInteractionMs).filter(l=>l!=null);return{droppedMin:c.length?Math.min(...c):null,droppedMax:c.length?Math.max(...c):null,inputMin:u.length?Math.min(...u):null,inputMax:u.length?Math.max(...u):null}})(),runs:e,median:i,fpsMin:r.length?r[0].fps:null,fpsMax:r.length?r[r.length-1].fps:null,flags:i?webBenchFlags(o?i:{...i,droppedPct:null},n):[]}}async function loadChromium(e){const n=(0,import_node_module.createRequire)((0,import_node_path5.join)(e,"noop.js"));for(const t of["playwright","playwright-core"]){let r;try{r=n.resolve(t)}catch{continue}const s=await import((0,import_node_url.pathToFileURL)(r).href);const o=s.chromium??s.default?.chromium;if(o)return o}throw new Error(`${PLAYWRIGHT_MISSING}
144
+ (Searched from ${e}. Pass projectRoot to look elsewhere.)`)}async function withDeadline(e,n,t){let r;const s=new Promise(o=>{r=setTimeout(()=>o("timeout"),n)});try{const o=await Promise.race([e,s]);if(o==="timeout"){e.catch(()=>{});return await t()}return o}finally{clearTimeout(r)}}function gitCommit(e){try{return(0,import_node_child_process6.execFileSync)("git",["rev-parse","HEAD"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:3e3}).toString().trim()}catch{return null}}function gitDirty(e){try{const n=(0,import_node_child_process6.execFileSync)("git",["status","--porcelain"],{cwd:e,stdio:["ignore","pipe","ignore"],timeout:5e3});return n.toString().trim().length>0}catch{return null}}async function closeWithin(e,n){if(!e)return true;return withDeadline(e.close().then(()=>true,()=>false),n,async()=>false)}function validateWebBenchOptions(e){if(!e.cases?.length)throw new Error("Give at least one case.");const n=new Set;for(const a of e.cases){if(!a.name?.trim())throw new Error("Every case needs a name.");if(n.has(a.name))throw new Error(`Two cases are named "${a.name}". Give each case its own name.`);n.add(a.name)}const t=(a,i,c,u)=>{if(a==null)return;if(!Number.isInteger(a)||a<c||a>u)throw new Error(`${i} must be a whole number from ${c} to ${u}.`)};const r=(a,i,c)=>{if(a==null)return;if(!Number.isFinite(a)||a<c)throw new Error(`${i} must be a number of at least ${c}.`)};const s=(a,i,c,u)=>{if(a==null)return;if(!Number.isFinite(a)||a<c||a>u)throw new Error(`${i} must be from ${c} to ${u}.`)};t(e.runsPerCase,"runsPerCase",1,10);s(e.durationMs,"durationMs",500,12e4);s(e.settleMs,"settleMs",0,6e4);s(e.refreshHz,"refreshHz",1,500);const o=e.cpuSlowdown??[];for(const a of o)s(a,"each cpuSlowdown rate",1,20);if(new Set(o).size!==o.length)throw new Error("cpuSlowdown has the same rate twice.");if(e.viewport){t(e.viewport.width,"viewport.width",200,8e3);t(e.viewport.height,"viewport.height",200,8e3);s(e.viewport.deviceScaleFactor,"viewport.deviceScaleFactor",.5,4)}t(e.extraRoundsWhenClose,"extraRoundsWhenClose",0,10);s(e.maxBatchMs,"maxBatchMs",1e4,6*60*6e4);for(const[a,i]of Object.entries(e.budgets??{}))s(i,`budgets.${a}`,0,1e9);for(const a of e.steps??[]){if("wait"in a)s(a.wait,"a wait step",0,6e4);if("scroll"in a)s(a.scroll,"a scroll step",-1e5,1e5)}new URL(e.url)}function oneLine(e){return e.replace(/\u001b\[[0-9;]*m/g,"").split("\n")[0].trim().slice(0,160)}async function locateStepTargets(e,n){const t=new Map;for(const r of n){if("expect"in r)continue;const s="click"in r?r.click:"type"in r?r.type.selector:null;if(!s||t.has(s))continue;const o=await e.$(s);const a=o?await o.boundingBox():null;if(!a)throw new Error(`step target ${s} isn't on the page (or isn't visible)`);t.set(s,{x:a.x+a.width/2,y:a.y+a.height/2})}return t}async function runSteps(e,n,t){for(const r of n){if("click"in r){const s=t.get(r.click);await e.mouse.click(s.x,s.y)}else if("press"in r)await e.keyboard.press(r.press);else if("type"in r){const s=t.get(r.type.selector);await e.mouse.click(s.x,s.y);await e.keyboard.type(r.type.text)}else if("scroll"in r)await e.mouse.wheel(0,r.scroll);else if("wait"in r)await sleep2(r.wait)}}function missedInput(e,n,t=[]){const r=[...n].sort((l,h)=>l.t-h.t);const s=[...t].sort((l,h)=>l.t-h.t);let o=0;let a=0;let i=-Infinity;const c=l=>{while(o<r.length){const h=r[o++];if(h.t>=i&&h.hit.includes(l)){i=h.t;return true}}return false};const u=l=>{while(a<s.length){const h=s[a++];if(h.t>=i&&keyMatches(l,h)){i=h.t;return true}}return false};for(const[l,h]of e.entries()){if("click"in h&&!c(h.click)){return`step ${l+1} (click ${h.click}) has no real click on its target, in order (moved, covered, or ignored)`}if("type"in h){if(!c(h.type.selector))return`step ${l+1} (type into ${h.type.selector}) has no real click on its target`;for(const d of h.type.text){if(!u(d))return`step ${l+1} (type "${h.type.text}") didn't deliver the key "${d}"`}}if("press"in h&&!u(h.press))return`step ${l+1} (press ${h.press}) didn't deliver that key`}return null}async function expectsAlreadyTrue(e,n){for(const t of n){if(!("expect"in t))continue;const{selector:r,text:s}=t.expect;const o=await e.evaluate(`(() => { const n = document.querySelector(${JSON.stringify(r)}); if (!n) return false; ${s==null?"return true;":`return (n.textContent || "").includes(${JSON.stringify(s)});`} })()`);if(o)return s==null?r:`${r} contains "${s}"`}return null}async function checkTargetsStill(e,n){for(const[t,r]of n){const s=await e.$(t);const o=s?await s.boundingBox():null;if(!o)return`step target ${t} was gone after the run`;const a=o.x+o.width/2;const i=o.y+o.height/2;if(Math.abs(a-r.x)>4||Math.abs(i-r.y)>4){return`step target ${t} moved during the run, so a click may have missed it`}}return null}function expectFailureFrom(e){for(const n of e){if(!n.found)return`expected ${n.selector} at the end of the run, but it isn't there`;const t=n.matched??(n.text==null||(n.content??"").includes(n.text));if(!t){return`expected ${n.selector} to contain "${n.text}" at the end of the run, got "${(n.content??"").slice(0,60)}"`}}return null}async function readMetrics(e){const{metrics:n}=await e.send("Performance.getMetrics");return Object.fromEntries(n.map(t=>[t.name,t.value]))}async function runWebBench(e,n=()=>{}){validateWebBenchOptions(e);const t=takeHostLock();const r={browserLeftRunning:false};try{return await runWebBenchLocked(e,n,r)}finally{t(r.browserLeftRunning)}}function takeHostLock(){if(lockedHere){throw new Error("A web benchmark is already running in this process (or left a stuck browser). Wait, or restart the MCP server.")}const e=`${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;const n=JSON.stringify({pid:process.pid,token:e,startedAt:new Date().toISOString()});const t=()=>{const o=`${LOCK_FILE}.${e.replace(/[^\w]/g,"")}.tmp`;try{(0,import_node_fs5.writeFileSync)(o,n);(0,import_node_fs5.linkSync)(o,LOCK_FILE);return true}catch{return false}finally{try{(0,import_node_fs5.unlinkSync)(o)}catch{}}};const r=()=>{try{return JSON.parse((0,import_node_fs5.readFileSync)(LOCK_FILE,"utf8"))}catch{return null}};const s=o=>{if(!o)return false;try{process.kill(o,0);return true}catch{return false}};if(!t()){const o=r();if(!o)throw new Error("Another web benchmark is starting. Wait for it to finish.");if(s(o.pid)){throw new Error(`Another web benchmark is running (pid ${o.pid}, since ${o.startedAt}). Two at once would skew both; wait for it to finish.`)}const a=`${LOCK_FILE}.cleanup`;let i=null;try{if(Date.now()-(0,import_node_fs5.statSync)(a).mtimeMs>3e4){const c=`${a}.${e.replace(/[^\w]/g,"")}.old`;(0,import_node_fs5.renameSync)(a,c);if(Date.now()-(0,import_node_fs5.statSync)(c).mtimeMs>3e4)(0,import_node_fs5.unlinkSync)(c);else{try{(0,import_node_fs5.linkSync)(c,a)}catch{}(0,import_node_fs5.unlinkSync)(c)}}}catch{}try{i=(0,import_node_fs5.openSync)(a,"wx")}catch{throw new Error("Another web benchmark is clearing an old lock. Try again in a moment.")}try{const c=r();if(c?.token===o.token)(0,import_node_fs5.unlinkSync)(LOCK_FILE);if(!t())throw new Error("Another web benchmark just started. Wait for it to finish.")}finally{(0,import_node_fs5.closeSync)(i);try{(0,import_node_fs5.unlinkSync)(a)}catch{}}}lockedHere=true;return(o=false)=>{if(o)return;lockedHere=false;if(r()?.token===e){try{(0,import_node_fs5.unlinkSync)(LOCK_FILE)}catch{}}}}async function runWebBenchLocked(e,n,t){const r=e.runsPerCase??3;const s=Math.max(500,e.durationMs??5e3);const o=Math.max(0,e.settleMs??1e3);const a=e.frameTrace!==false;const i=(e.cpuSlowdown?.length?e.cpuSlowdown:[1]).map($=>Math.max(1,$));let c=e.refreshHz??60;const u={width:e.viewport?.width??1280,height:e.viewport?.height??800,deviceScaleFactor:e.viewport?.deviceScaleFactor??1};const l=[];const h=[];if(r<2)l.push("runsPerCase=1 gives no spread; use 3 or more before choosing.");const d=await loadChromium(e.projectRoot??process.cwd());const m=await d.launch({headless:!e.headed});const w=[];const g=[];const b=Date.now();const p=new Date(b).toISOString();const f=e.maxBatchMs??30*6e4;let v=false;let y=false;const k={commit:gitCommit(e.projectRoot??process.cwd()),dirty:gitDirty(e.projectRoot??process.cwd())};try{const $={viewport:{width:u.width,height:u.height},deviceScaleFactor:u.deviceScaleFactor};const T=await withDeadline(m.newContext($),3e4,async()=>{t.browserLeftRunning=true;throw new Error("Chromium didn't open a page in 30 s. Restart the MCP server and try again.")});const A=3e4+15e3+o+s+6e4;const U=async(E,R,C,O)=>{const F={};return withDeadline(D(F,E,R,C,O),A,async()=>{F.cancelled=true;if(!F.ctx)v=true;else if(!await closeWithin(F.ctx,1e4))v=true;return{...N(E.name,R,C),failed:`run took over ${Math.round(A/1e3)} s (the page may be stuck in a loop)`}})};const N=(E,R,C)=>({caseName:E,cpu:R,round:C,fps:null,frameGapP95Ms:null,frameGapMaxMs:null,longFrames:0,longTaskPct:null,loafBlockingMs:null,styleLayoutMs:null,forcedLayoutMs:null,topScripts:[],layoutShift:null,domNodesStart:null,domNodesEnd:null,mainThreadPct:null,scriptMs:null,styleMs:null,layoutMs:null,layoutCount:null,styleCount:null,heapMb:null,pageErrors:[]});const D=async(E,R,C,O,F)=>{const H=await m.newContext($);E.ctx=H;if(E.cancelled){await closeWithin(H,1e4);throw new Error("run was cancelled during setup")}const z=await H.newPage();const W=[];z.on("pageerror",P=>W.push(String(P?.message??P)));const q={...N(R.name,C,O),pageErrors:W};try{const P=await H.newCDPSession(z);await P.send("Performance.enable",{timeDomain:"timeTicks"});if(C>1)await P.send("Emulation.setCPUThrottlingRate",{rate:C});const Z=await z.goto(caseUrl(e.url,R,e.path),{waitUntil:"load",timeout:3e4});if(Z&&Z.status()>=400)return{...q,failed:`HTTP ${Z.status()}`};if(e.readySelector)await z.waitForSelector(e.readySelector,{state:"attached",timeout:15e3});await sleep2(o);const se=F||a;if(se){await P.send("Tracing.start",{transferMode:"ReturnAsStream",categories:(F?FULL_TRACE_CATEGORIES:FRAME_TRACE_CATEGORIES).join(",")})}const ee=e.steps?.length?await locateStepTargets(z,e.steps):new Map;if(e.steps?.length){const K=await expectsAlreadyTrue(z,e.steps);if(K)return{...q,failed:`expect ${K} already held before the steps, so it can't prove they worked`};const Y=[...ee.keys()];const G=e.steps.flatMap(Q=>"expect"in Q?[Q.expect]:[]);await z.evaluate(`window.__buoyBenchClickTargets = ${JSON.stringify(Y)}; window.__buoyBenchExpects = ${JSON.stringify(G)}; true`)}const te=await readMetrics(P);await z.evaluate(START_PROBE_JS);const oe=Date.now();if(e.steps?.length)await runSteps(z,e.steps,ee);const ae=s-(Date.now()-oe);if(ae>0)await sleep2(ae);await z.evaluate(END_PROBE_JS);const ne=await readMetrics(P);const ie=Date.now()-oe;const J=await z.evaluate(COLLECT_PROBE_JS);let j=null;if(se){const K=new Promise(L=>P.on("Tracing.tracingComplete",X=>L(X.stream)));await P.send("Tracing.end");const Y=await K;const G=F?(0,import_node_fs5.openSync)(F,"w"):null;const Q=[];let re=0;try{for(;;){const L=await P.send("IO.read",{handle:Y});const X=L.base64Encoded?Buffer.from(L.data,"base64").toString("utf8"):L.data;re+=X.length;if(G!=null)(0,import_node_fs5.writeSync)(G,X);if(re<=MAX_TRACE_CHARS)Q.push(X);else Q.length=0;if(L.eof)break}}finally{await P.send("IO.close",{handle:Y}).catch(()=>{});if(G!=null)(0,import_node_fs5.closeSync)(G)}if(F)h.push(F);if(re>MAX_TRACE_CHARS){l.push(`Trace for ${R.name} was over ${Math.round(MAX_TRACE_CHARS/1e6)} MB, so its frames weren't counted. A page that busy shows its problem in the table already.`)}else{try{const L=JSON.parse(Q.join(""));j=summarizeFrameTrace(Array.isArray(L)?L:L.traceEvents??[])}catch{j=null}}}if(!J)return{...q,failed:"probe returned nothing (page navigated away?)"};if(J.hidden)return{...q,failed:"page was hidden during the run"};if(J.resized)return{...q,failed:"the window size or zoom changed during the run"};if(W.length&&!e.allowPageErrors){return{...q,failed:`page error: ${oneLine(W[0])}`}}if(e.steps?.length){const K=missedInput(e.steps,J.clicks??[],J.keys??[]);if(K)return{...q,failed:K};const Y=expectFailureFrom(J.expectResults??[]);if(Y)return{...q,failed:Y};const G=await checkTargetsStill(z,ee);if(G)return{...q,failed:G}}const ce="Timestamp"in te&&"Timestamp"in ne?r1((ne.Timestamp-te.Timestamp)*1e3):null;return{...q,...summarizeProbe(J,c),...cdpDeltas(te,ne,ie),probeSpanMs:r1(J.end-J.start),cdpSpanMs:ce,shownFps:j?.shownFps??null,droppedFrames:j?.faults??null,frameCounts:j?{full:j.full,partial:j.partial,dropped:j.dropped,faults:j.faults}:null,droppedPct:j?.droppedPct??null}}catch(P){return{...q,failed:oneLine(P?.message??String(P))}}finally{await closeWithin(z,5e3);if(!await closeWithin(H,1e4))v=true}};if(e.refreshHz==null){const E=await withDeadline(T.newPage(),15e3,async()=>{throw new Error("Chromium didn't open the calibration page in 15 s.")});try{await withDeadline(E.goto("about:blank"),15e3,async()=>null);const R=await withDeadline(E.evaluate(MEASURE_REFRESH_JS),5e3,async()=>{throw new Error("idle rate check timed out")});c=snapRefreshRate(R);n(`idle frame rate ${R.toFixed(1)} \u2192 ${c} Hz`)}catch{l.push("Couldn't measure the idle frame rate; frame budgets assume 60 Hz.")}finally{await closeWithin(E,5e3)}}n("warmup load");await U(e.cases[0],1,-1);const V=async(E,R,C,O)=>{if(v){w.push({...N(E.name,R,C),failed:"an earlier run left the browser stuck; batch stopped"});return}if(Date.now()-b>f){w.push({...N(E.name,R,C),failed:"batch time limit reached; run skipped"});return}n(O);g.push({cpu:R,round:C,caseName:E.name});w.push(await U(E,R,C))};for(const E of i){for(let R=0;R<r;R++){const C=shuffled(e.cases,2654435769^R*7919+E);for(const O of C)await V(O,E,R,`cpu ${E}x \xB7 round ${R+1}/${r} \xB7 ${O.name}`)}for(let R=0;R<(e.extraRoundsWhenClose??0);R++){const C=e.cases.map(z=>aggregateCase(w.filter(W=>W.cpu===E&&W.caseName===z.name),c));for(const z of C)z.control=e.cases.find(W=>W.name===z.caseName)?.control===true;const O=new Set([...tiedPairs(C).flat(),...C.filter(z=>!z.control&&z.validRuns<2&&z.plannedRuns>0).map(z=>z.caseName)]);if(O.size===0)break;const F=r+R;const H=shuffled(e.cases.filter(z=>O.has(z.name)),20973^F*7919+E);for(const z of H)await V(z,E,F,`cpu ${E}x \xB7 extra round ${R+1} (close call) \xB7 ${z.name}`)}}if(e.traceDir){(0,import_node_fs5.mkdirSync)(e.traceDir,{recursive:true});for(const[E,R]of e.cases.entries()){const C=(0,import_node_path5.join)(e.traceDir,`${String(E+1).padStart(2,"0")}-${R.name.replace(/[^\w.-]+/g,"_")}.json`);if(v||Date.now()-b>f){l.push(`Trace run for ${R.name} skipped (batch time limit or a stuck browser).`);continue}n(`trace \xB7 ${R.name}`);const O=await U(R,i[0],-1,C);if(O.failed)l.push(`Trace run for ${R.name} failed: ${O.failed}`)}}await closeWithin(T,1e4)}finally{const $=await closeWithin(m,15e3);t.browserLeftRunning=!$;if(!$){y=true;l.push("Chromium didn't close in 15 s and may still be running. The benchmark lock stays held; restart the MCP server before the next batch.")}}const S=[];for(const $ of i){for(const T of e.cases){S.push(aggregateCase(w.filter(A=>A.cpu===$&&A.caseName===T.name),c))}}for(const $ of S)$.control=e.cases.find(T=>T.name===$.caseName)?.control===true;for(const $ of i){const T=S.filter(D=>D.cpu===$);const A=T.find(D=>!D.control);if(!A)continue;A.isBaseline=true;const U=D=>D.droppedMin!=null?D.median?.droppedPct??null:null;const N=U(A);for(const D of T.filter(V=>V!==A&&!V.control)){const V=U(D);D.deltaDroppedPct=N!=null&&V!=null?r1(V-N):null}}if(e.allowPageErrors&&w.some($=>$.pageErrors.length)){l.push("Some runs threw page errors (kept because allowPageErrors is on).")}if(v)l.push("A browser context wouldn't close, so the batch stopped early. Restart and run again.");if(e.budgets)for(const $ of S)$.budget=checkBudgets($,e.budgets,e.steps??[]);const _=S.filter($=>$.median).length;const M=S.every($=>$.validRuns===$.plannedRuns);const B=_===0?"failed":_<S.length||!M?"partial":"ok";const I=S.filter($=>$.median&&$.validRuns>0&&$.validRuns<$.plannedRuns);for(const $ of I){const T=Array.from(new Set($.runs.filter(A=>A.failed).map(A=>A.failed)));l.push(`${$.caseName} (CPU ${$.cpu}x): ${$.plannedRuns-$.validRuns} of ${$.plannedRuns} runs failed: ${T.join("; ")}`)}for(const $ of S){const T=$.median;if(T?.probeSpanMs&&T.cdpSpanMs&&Math.abs(T.cdpSpanMs-T.probeSpanMs)/T.probeSpanMs>.15){l.push(`${$.caseName} (CPU ${$.cpu}x): Chrome's counters cover ${Math.round(T.cdpSpanMs)} ms but frames cover ${Math.round(T.probeSpanMs)} ms, so its cost columns include extra time.`)}if(T?.probeSpanMs!=null&&T.probeSpanMs<s*.8){l.push(`${$.caseName} (CPU ${$.cpu}x) recorded ${Math.round(T.probeSpanMs)} ms, not the ${s} ms asked for.`)}}const x={status:B,hasSteps:Boolean(e.steps?.length),browserLeftRunning:y,replay:{startedAt:p,...k,runOrder:g},url:e.url,browser:`Chromium ${m.version()}${e.headed?"":" (headless)"}`,viewport:u,refreshHz:c,durationMs:s,runsPerCase:r,results:S,traces:h,warnings:l};if(e.outFile){try{(0,import_node_fs5.writeFileSync)(e.outFile,JSON.stringify({options:e,result:x},null,2))}catch($){x.warnings.push(`Couldn't write outFile ${e.outFile}: ${oneLine($?.message??String($))}`)}}return x}function tiedPairs(e){const n=e.filter(o=>!o.control&&o.median&&o.validRuns>1);const t=n.filter(o=>o.droppedMin!=null);const r=[];if(t.length>=2&&t.length===n.length){const o=[...t].sort((a,i)=>a.median.droppedPct-i.median.droppedPct);for(let a=1;a<o.length;a++){const[i,c]=[o[a-1],o[a]];const u=c.median.droppedPct-i.median.droppedPct<=MIN_DROPPED_GAP_PP;if(c.droppedMin<=i.droppedMax||u)r.push([i.caseName,c.caseName])}return r}if(t.length>0)return r;const s=n.filter(o=>o.median.fps!=null).sort((o,a)=>a.median.fps-o.median.fps);for(let o=1;o<s.length;o++){const[a,i]=[s[o-1],s[o]];const c=a.median.fps-i.median.fps<=MIN_FPS_GAP;if((i.fpsMax??0)>=(a.fpsMin??0)||c)r.push([a.caseName,i.caseName])}return r}function closeCall(e){const n=e.filter(i=>!i.control&&i.median);const t=[];const r=n.filter(i=>i.validRuns<2);if(r.length)t.push(`**Too few runs to rank:** ${r.map(i=>i.caseName).join(", ")} (need 2 or more valid runs).`);const s=n.filter(i=>i.validRuns>1);const o=s.filter(i=>i.droppedMin!=null);if(o.length>0&&o.length<s.length){t.push(`**Not enough traced runs to compare smoothness:** ${s.filter(i=>!o.includes(i)).map(i=>`${i.caseName} (${i.tracedRuns}/${i.validRuns} traced)`).join(", ")}.`);return t.join("\n")}const a=new Map(s.map(i=>[i.caseName,i]));for(const[i,c]of tiedPairs(e)){const u=a.get(i);const l=a.get(c);t.push(o.length?`**Too close to call:** ${i} (${u.droppedMin}\u2013${u.droppedMax}% dropped) and ${c} (${l.droppedMin}\u2013${l.droppedMax}%) overlap or sit within ${MIN_DROPPED_GAP_PP} pp. No clear winner between them; more runs may separate them.`:`**Too close to call:** ${i} and ${c} overlap or sit within ${MIN_FPS_GAP} FPS. No clear winner between them; more runs may separate them.`)}return t.length?t.join("\n"):null}function formatWebBenchMarkdown(e){const n=(i,c=1)=>i==null?"\u2014":i.toFixed(c);const t=[];t.push(`### Web benchmark \xB7 ${e.url}`);t.push(`${e.browser} \xB7 ${e.viewport.width}\xD7${e.viewport.height} @${e.viewport.deviceScaleFactor}x \xB7 ${e.runsPerCase} runs${e.results.some(i=>i.plannedRuns>e.runsPerCase)?" (more for close calls)":""} \xD7 ${e.durationMs/1e3}s per case, fresh browser context each run, case order shuffled each round.`);const r=e.results.map(i=>i.median?.probeSpanMs).filter(i=>i!=null);const s=e.results.map(i=>i.median?.cdpSpanMs).filter(i=>i!=null);const o=i=>`${(Math.min(...i)/1e3).toFixed(2)}\u2013${(Math.max(...i)/1e3).toFixed(2)} s`;if(r.length){t.push(`Recorded: frames over ${o(r)}${s.length?`; cost columns (Chrome's counters) over ${o(s)}`:""}, not the requested time.`)}if(e.replay?.dirty)t.push(`Note: the project had uncommitted changes, so commit ${e.replay.commit?.slice(0,10)??"?"} alone doesn't describe the code that ran.`);t.push("");t.push(`**How to read this.** Dropped % is the smoothness number: frames Chrome dropped or only partly produced that it says hurt smoothness, as a share of the frames it wanted, from its own verdicts in a trace. Rank on it first, and only across variants doing the same work. Shown FPS counts frames Chrome produced (headless Chrome displays nothing), so an animation that changes less often needs fewer; don't compare it across variants that update at different rates. Main-thread FPS and frame gaps come from requestAnimationFrame. Main thread % is Chrome's task time for the run. Desktop numbers, even with CPU slowdown, don't stand in for a real phone's GPU, memory or heat.`);const a=Array.from(new Set(e.results.map(i=>i.cpu)));for(const i of a){t.push("");t.push(i===1?`#### No CPU slowdown`:`#### CPU slowed ${i}x`);t.push("");t.push(`| Case | Runs | Shown FPS | Dropped % | Main-thread FPS (range) | p95 gap ms | Worst gap ms | Main thread % | Script ms | Style ms | Layout ms | Layouts | DOM nodes | JS heap MB | Flags |`);t.push(`|------|-----:|----------:|----------:|------------------------:|-----------:|-------------:|--------------:|----------:|---------:|----------:|--------:|----------:|--------:|-------|`);for(const d of e.results.filter(m=>m.cpu===i)){const m=d.median;if(!m){const b=d.runs.find(p=>p.failed)?.failed??"no valid runs";t.push(`| ${d.caseName} | 0/${d.plannedRuns} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${b}) |`);continue}const w=d.fpsMin!=null&&d.fpsMax!=null&&d.validRuns>1?` (${n(d.fpsMin)}\u2013${n(d.fpsMax)})`:"";const g=d.flags.length?`\u26A0\uFE0F ${d.flags.join("; ")}`:"no flags";t.push(`| ${d.isBaseline?`${d.caseName} (baseline)`:d.control?`${d.caseName} (control)`:d.caseName} | ${d.validRuns}/${d.plannedRuns} | ${n(m.shownFps)} | ${d.droppedMin==null&&d.tracedRuns>0?`\u2014 (${d.tracedRuns}/${d.validRuns} traced)`:n(d.droppedMin==null?null:m.droppedPct)}${d.droppedMin!=null&&d.validRuns>1?` [${d.droppedMin}\u2013${d.droppedMax}]`:""}${d.deltaDroppedPct!=null?` (${d.deltaDroppedPct>0?"+":""}${d.deltaDroppedPct} pp)`:""} | ${n(m.fps)}${w} | ${n(m.frameGapP95Ms)} | ${n(m.frameGapMaxMs)} | ${n(m.mainThreadPct)} | ${n(m.scriptMs,0)} | ${n(m.styleMs,0)} | ${n(m.layoutMs,0)} | ${n(m.layoutCount,0)} | ${n(m.domNodesEnd,0)} | ${n(m.heapMb)} | ${g} |`)}const c=closeCall(e.results.filter(d=>d.cpu===i));if(c){t.push("");t.push(c)}const u=e.results.filter(d=>d.cpu===i&&d.budget);if(u.length){t.push("");t.push(`Budget:`);for(const d of u){const m=d.budget;const w=[...m.over,...m.unknown.map(b=>`${b} not measured`)].join("; ");const g=d.control?`${d.caseName} (control)`:d.caseName;t.push(`- **${g}**: ${m.meets?"meets every limit":`misses (${w||"no result"})`}`)}}const l=e.results.filter(d=>d.cpu===i&&d.median?.interactions!=null&&(e.hasSteps||d.median.interactions>0)).map(d=>{const m=d.median;const w=d.inputMin!=null&&d.inputMax!=null?` \xB7 slowest across all runs ${d.inputMax} ms (range ${d.inputMin}\u2013${d.inputMax})`:"";const g=d.runs.filter(p=>!p.failed&&p.fps!=null&&p.inputCaptureComplete===false).length;const b=g?` \xB7 \u26A0\uFE0F timing incomplete in ${g} run(s): the last input's paint came after the run ended, so its time is unknown (leave more time after the last step)`:"";if(!m.interactions)return`- **${d.caseName}**: middle run had no input entry (inputs under 16 ms aren't reported)${w}${b}`;return`- **${d.caseName}**: middle run: ${m.interactions} reported (16 ms or more), slowest ${m.worstInteractionMs} ms (waiting ${m.worstInputDelayMs} \xB7 handlers ${m.worstProcessingMs} \xB7 to paint ${m.worstPresentationMs})${w}${b}`});if(l.length){t.push("");t.push(`Input response (scripted steps, Event Timing). Inputs under 16 ms aren't reported. Under 200 ms is good.`);t.push(...l)}const h=e.results.filter(d=>d.cpu===i&&d.median&&d.median.topScripts.length).map(d=>`- **${d.caseName}**: ${d.median.topScripts.slice(0,3).map(m=>`${m.name} ${m.ms} ms`).join(", ")}`);if(h.length){t.push("");t.push(`Scripts in slow frames (Long Animation Frames; frames that ran past the end of a run are left out):`);t.push(...h)}}if(e.traces.length){t.push("");t.push(`Chrome traces (open in DevTools \u2192 Performance \u2192 Load profile): ${e.traces.join(", ")}`)}if(e.warnings.length){t.push("");t.push(...e.warnings.map(i=>`\u26A0\uFE0F ${i}`))}return t.join("\n")}var import_node_module,import_node_child_process6,import_node_path5,import_node_fs5,import_node_os5,import_node_url,r1,COMMON_RATES,FRAME_STATES,PLAYWRIGHT_MISSING,FRAME_TRACE_CATEGORIES,FULL_TRACE_CATEGORIES,MAX_TRACE_CHARS,sleep2,LOCK_FILE,lockedHere,MIN_DROPPED_GAP_PP,MIN_FPS_GAP;var init_webBench=__esm({"src/webBench/webBench.ts"(){"use strict";import_node_module=require("module");import_node_child_process6=require("child_process");import_node_path5=require("path");import_node_fs5=require("fs");import_node_os5=require("os");import_node_url=require("url");init_inPageProbe();r1=e=>Math.round(e*10)/10;COMMON_RATES=[30,60,75,90,120,144,165,240];FRAME_STATES=["STATE_PRESENTED_ALL","STATE_PRESENTED_PARTIAL","STATE_DROPPED"];PLAYWRIGHT_MISSING="Playwright isn't installed in this project. Install it, then run again:\n npm i -D playwright && npx playwright install chromium";FRAME_TRACE_CATEGORIES=["disabled-by-default-devtools.timeline.frame","blink.user_timing"];FULL_TRACE_CATEGORIES=["devtools.timeline","disabled-by-default-devtools.timeline","disabled-by-default-devtools.timeline.frame","blink.user_timing","loading","latencyInfo","v8.execute"];MAX_TRACE_CHARS=15e7;sleep2=e=>new Promise(n=>setTimeout(n,e));LOCK_FILE=(0,import_node_path5.join)((0,import_node_os5.tmpdir)(),"buoy-web-bench.lock");lockedHere=false;MIN_DROPPED_GAP_PP=1.5;MIN_FPS_GAP=2}});var import_stdio=require("@modelcontextprotocol/sdk/server/stdio.js");var import_node_fs=require("fs");var import_node_path=require("path");var import_node_os=require("os");var import_node_crypto=require("crypto");var import_headless=require("@buoy-gg/license/headless");var KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];function readProjectKey(e,n=process.env){for(const t of KEY_NAMES)if(n[t]!==void 0)return(0,import_headless.normalizeLicenseKey)(n[t])||null;for(const t of[".env.development.local",".env.local",".env"]){let r;try{r=(0,import_node_fs.readFileSync)((0,import_node_path.join)(e,t),"utf8")}catch{continue}const s=new Map;for(const o of r.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const i=a[2].trim();const c=i.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);s.set(a[1],c?c[2]:i.split("#")[0].trim())}for(const o of KEY_NAMES)if(s.has(o))return(0,import_headless.normalizeLicenseKey)(s.get(o))||null}return null}var NodeAccount=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=(0,import_node_path.resolve)(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??(0,import_node_path.join)((0,import_node_os.homedir)(),".buoy",`account-${t}.json`);this.session=n.session!==void 0?n.session:(0,import_headless.createNodeSession)({audience:import_headless.CLI_AUDIENCE,openBrowser:()=>{throw new Error("The MCP does not open a browser. Run `npx buoy login`.")}})}hasKeyAccess(){return(0,import_headless.resolveAccountAccess)({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((0,import_node_fs.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;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=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasKeyAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const n=this.generation;try{if((0,import_headless.isDevToken)(e)){const i=(0,import_headless.checkDevToken)(e);if(!i.ok){this.cache=null;this.removeCache()}else{const c=Math.min(Date.now(),i.claims.iat*1e3);const u=i.claims.tier==="free"?"free":"pro";this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`dev:${i.claims.sid}`,valid:true,tier:u,isPro:u==="pro",expiresAt:new Date(i.claims.exp*1e3).toISOString(),cachedAt:c,lastValidatedAt:Date.now()});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}if((0,import_headless.isServiceKey)(e)){const i=await(0,import_headless.exchangeServiceKey)(e,{origin:process.env.BUOY_SITE_URL||"https://buoy.gg"});if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;if(!i.ok){if(i.reason==="rejected"){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL}else{this.nextCheck=Date.now()+(import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]??import_headless.LICENSE_REVALIDATION_INTERVAL)}return}const c=Date.now();const u=i.claims.exp*1e3;this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`service:${i.claims.sid}`,valid:true,tier:"pro",isPro:true,expiresAt:new Date(u).toISOString(),cachedAt:c,lastValidatedAt:c});this.persistCache();this.retry=0;this.nextCheck=Math.max(c+6e4,u-60*60*1e3);return}if((0,import_headless.isBuoyToken)(e)){const i=(0,import_headless.verifyFreeToken)(e);if(!i){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${i.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(n!==this.generation||readProjectKey(this.projectDir)!==e)return;const r=t.data?.relationships?.product?.data?.id;const s=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(r&&r!==import_headless.KEYGEN_CONFIG.productId||!s&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const o=s?"free":(0,import_headless.resolveTier)(t.data);const a=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:s?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(n!==this.generation)return;const r=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(r??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs.mkdirSync)((0,import_node_path.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs.unlinkSync)(this.cacheFile)}catch{}}};var import_node_http=__toESM(require("http"));var import_node_crypto2=require("crypto");var import_socket=require("socket.io-client");var import_sync_broker2=require("@buoy-gg/sync-broker");var import_sync_broker=require("@buoy-gg/sync-broker");var BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;var BROKER_URL=`http://localhost:${BROKER_PORT}`;var DEFAULT_ACTION_TIMEOUT_MS=15e3;var UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;var UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;var RECOVERY_PROBE_MS=2e4;var UNRESPONSIVE_AFTER_MS=3e4;var DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;var DEVICE_RETENTION_MS=15*6e4;var DASHBOARD_NAME="Dashboard";var ALL_DEVICES="All";function isRecoveryAction(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.deviceConnections=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
145
+ `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);try{(0,import_sync_broker2.writeBrokerToken)(this.broker.dashboardToken,(0,import_sync_broker2.brokerTokenPath)(this.port))}catch(t){this.log(`could not write the broker token file: ${t.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 new Promise((e,n)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},auth:i=>{const c=this.broker?.dashboardToken??(0,import_sync_broker2.readBrokerToken)((0,import_sync_broker2.brokerTokenPath)(this.port));i(c?{dashboardToken:c}:{})},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const s=()=>{if(r)return;r=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",i=>{const c=!!i&&typeof i==="object"&&i.protocol===1&&i.ready===true;this.setAdmission(c);if(c){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));const a=i=>i.message===import_sync_broker2.DASHBOARD_TOKEN_REQUIRED;t.once("connect_error",i=>{if(r)return;r=true;clearTimeout(o);n(new Error(a(i)?`The broker at ${this.url} refused this MCP as a dashboard: the token in ${(0,import_sync_broker2.brokerTokenPath)(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}: ${i.message}`))});t.on("connect_error",i=>{if(!a(i)||t.active)return;setTimeout(()=>{if(this.socket===t&&!t.connected)t.connect()},1e3).unref?.()});t.on("devtool-capabilities",i=>{if(!this.admitted||this.lockedDevices.has(i.persistentDeviceId))return;this.capabilities.set(i.persistentDeviceId,i);void this.learnDevServerOrigin(i.persistentDeviceId)});t.on("all-devices-update",i=>{if(!this.admitted)return;if(!Array.isArray(i))return;let c=false;for(const l of i){if(l.deviceId&&l.tier==="locked")this.lockDevice(String(l.deviceId));else if(l.deviceId&&(l.tier==="free"||l.tier==="pro")){c=this.lockedDevices.delete(String(l.deviceId))||c}}this.devices=i;const u=Date.now();for(const l of i)if(l.deviceId)this.lastSeen.set(l.deviceId,u);this.pruneStaleDevices();if(c)this.rearmWatches();else this.rearmReconnectedDevices(i)});t.on("devtool-sync",i=>{if(!this.admitted||this.lockedDevices.has(i.persistentDeviceId))return;const c=snapKey(i.persistentDeviceId,i.toolId);this.snapshots.set(c,i.payload);this.snapshotAt.set(c,Date.now());const u=this.snapshotWaiters.get(c);if(u&&u.length){this.snapshotWaiters.set(c,[]);for(const l of u)l(i.payload)}});t.on("devtool-action-result",i=>{if(!this.admitted)return;const c=this.pendingActions.get(i.requestId);if(!c)return;this.pendingActions.delete(i.requestId);clearTimeout(c.timer);c.resolve(i)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const s of r)s(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===ALL_DEVICES){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(r)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(r))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=this.connectedDevices();const t=n.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(n.length>0){throw new Error(`Devices are connected (${n.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const r=t.filter(o=>o.tools.length>0);if(r.length===1)return r[0].deviceId;if(r.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=r.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const n=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${n(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const r=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===r);if(!s?.isPro)throw new Error(t(s));return r}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:n,action:t});if(o)r.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=diagnoseCrashed({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const n=this.snapshots.get(snapKey(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const s=t[r];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(snapKey(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){this.devServerOrigins.set(e,n)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=snapKey(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 r=`${n.id}|${n.connectionNonce??""}`;const s=this.deviceConnections.get(t);this.deviceConnections.set(t,r);if(s===void 0||s===r)continue;for(const{toolId:o,targetDeviceId:a}of this.watching.values()){if(a===t)this.sendWatch(o,a)}}}unwatch(e,n){const t=snapKey(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(snapKey(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const s=snapKey(r,e);this.ensureWatching(e,r);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const u=this.healthOf(r);if(Date.now()-u.lastFullBudgetAt>RECOVERY_PROBE_MS){u.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const i=await a;this.requireAdmission();this.requireDeviceAccount(r);if(i!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:i}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},n);const o=a=>{clearTimeout(s);t(a)};r.push(o);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,s={}){this.requireAdmission();const o=r===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto2.randomUUID)();const i=s.timeoutMs??this.timeoutFor(o,e,n);const c=await new Promise((u,l)=>{const h=setTimeout(()=>{this.pendingActions.delete(a);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);l(new Error(`Action "${e}.${n}" got no answer in ${i}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:u,reject:l,timer:h,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};var snapKey=(e,n)=>`${e}::${n}`;function probeBroker(e,n=800){return new Promise(t=>{const r=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function waitForBroker(e,n=20){for(let t=0;t<n;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(n=>setTimeout(n,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod31=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.50"?"7.0.50":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,n){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const r=t(e);const s=t(n);for(let o=0;o<3;o++){const a=r[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function fetchLatest(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),FETCH_TIMEOUT_MS);try{const r=await fetch(REGISTRY_URL,{signal:n.signal});if(!r.ok)return null;const s=await r.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const n=await readCache();if(n&&e-n.checkedAt<CHECK_TTL_MS)return n.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const n=await resolveLatest(e);if(n&&isNewer(n,VERSION)){return`A newer buoy-mcp is available (${n}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,n){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const r=n.listDevices();if(r.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=r.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return json({devices:r,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const r=n.toolActions(t);if(r.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:r})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:r,timeoutMs:s})=>{try{const o=n.requireProDevice(r);const{payload:a}=await n.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:a})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:r,params:s,deviceId:o,timeoutMs:a})=>{try{const i=n.requireProDevice(o);const c=await n.callAction(t,r,s,i,{timeoutMs:a});return json({toolId:t,action:r,ok:true,result:c??null})}catch(i){return fail(errMsg(i))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,n){return typeof e==="number"&&e>0?e:n}function staleHeader(e){return e?`${e}
67
146
 
68
- `:""}function inactiveToolMessage(e){return`No ${e.toolId} result from the device. The ${e.toolId} tool may be inactive, or the app needs a newer Buoy (${e.mcpTool} requires the ${e.action} action).`}function withDevice(e,n){return async t=>{try{const r=e.requireProDevice(t.deviceId);return await n(t,r)}catch(r){return fail2(errMsg2(r))}}}var PERF_TOOL_ID="perf-monitor";var clamp=(e,n,t)=>Math.max(n,Math.min(t,e));var WARMUP_CASE_NAME="__batch_warmup__";var THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function pick(e,n,t){return e??n??t}function buildAutomationConfig(e,n={}){const t=[];const r=(e.targetRoute||"").trim();if(!r)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=r==="/"?"/_buoy_bounce":"/";if(s===r){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((d,h)=>{const y=(d.name||"").trim()||`case ${h+1}`;return{id:`case-${h}`,name:y,params:d.params??{},route:d.route}});const i=clamp(Math.round(pick(e.runsPerCase,n.runsPerCase,FALLBACK.runsPerCase)),1,10);const a=clamp(Math.round(pick(e.coolDownMs,n.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const c=clamp(Math.round(pick(e.discardWarmupRuns,n.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,i-1);const u=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&u===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(i===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const l={targetRoute:r,bounceRoute:s,perCaseDurationMs:clamp(Math.round(pick(e.perCaseDurationMs,n.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp(Math.round(pick(e.settleMs,n.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp(Math.round(pick(e.navTimeoutMs,n.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:i,coolDownMs:a,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:u,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:l,warnings:t}}function describeBenchmarkSettings(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const r=estimateBatchMs(e);const s=r/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(r/1e3)}s`;const i=[`**${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 a=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${i.join(", ")}. That's ~${n(a)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function estimateBatchMs(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let r=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)r+=e.cases.length*3e3;return r}function computeFlags(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 computeWebFlags(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.jsFps!=null&&t&&e.jsFps<t*.95){n.push("Main thread drops frames")}else if(e.longFrames!=null&&e.frames>0&&e.longFrames/e.frames>=.02){n.push("Frequent long frames")}if(e.frameGapMaxMs!=null&&e.frameGapMaxMs>=100){n.push(`Freeze of ${Math.round(e.frameGapMaxMs)} 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 grows each run (+${e.domGrowth} nodes)`);return n}function domGrowthAcrossRuns(e){const n=e.filter(r=>!r.batchFailureReason).sort((r,s)=>(r.runIndex??0)-(s.runIndex??0)).map(r=>r.webRun?.domNodesStart).filter(r=>typeof r==="number");if(n.length<3)return null;for(let r=1;r<n.length;r++)if(n[r]<=n[r-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function pickBaseline(e,n){const t=e.find(s=>s.isBaseline);if(t)return t;const r=n?.[0]?.name;if(r){const s=e.filter(o=>o.caseName===r);if(s.length===1)return s[0]}return e[0]}function rankBatchFromIndex(e,n,t,r=[]){const s=e.filter(p=>p.batchId===n);const o=new Map(r.map(p=>[p.index,p.reason]));const i=Array.from(new Set(s.map(p=>p.batchIndex??0))).sort((p,f)=>p-f);const a=p=>{const f=s.filter(w=>(w.batchIndex??0)===p);if(f.length===0)return void 0;return f.find(w=>w.isMedianRun===true)??f.find(w=>w.runIndex===0||w.runIndex===void 0)??f[0]};const c=p=>typeof p==="number"?p:null;const u=i.map(p=>{const f=a(p);const w=s.filter(I=>(I.batchIndex??0)===p);const b=w.length;const _=w.filter(I=>!I.batchFailureReason);const k=I=>I.webRun?I.jsFpsAvg:I.uiFpsAvg;const $=_.map(k).filter(I=>typeof I==="number");const A=f?.name??t?.[p]?.name??`case ${p+1}`;const R=s.some(I=>(I.batchIndex??0)===p&&I.isBaseline===true);const x=o.get(p)??f?.batchFailureReason;const S=Boolean(x)||!f;const T={batchIndex:p,caseName:A,params:f?.params??{},runs:b,failed:S,...R?{isBaseline:true}:{},failureReason:S?x??"no run recorded":void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:_.length,fpsMin:$.length?round(Math.min(...$),1):null,fpsMax:$.length?round(Math.max(...$),1):null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};const M=f?.webRun;if(!M)return{...T,flags:S?[]:computeFlags(T)};const O=[...M.longTasksSupported?[]:["long tasks"],...M.heapSupported?[]:["JS heap"],...M.loaf===null?["long animation frames"]:[]];const z={...T,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:M.heapSupported?T.memMb:null,deviceMaxRefreshRate:Math.max(T.deviceMaxRefreshRate??0,M.refreshHz??0)||null,longTaskPct:M.longTaskPct,frameGapP95Ms:M.frameGapP95Ms,frameGapMaxMs:M.frameGapMaxMs,longFrames:M.longFrames,...O.length?{missingMetrics:O}:{},loafBlockingMs:M.loaf?.blockingMs??null,styleLayoutMs:M.loaf?.styleLayoutMs??null,forcedLayoutMs:M.loaf?.forcedLayoutMs??null,topScripts:M.loaf?.topScripts??[],worstInteractionMs:M.interactions?.worstMs??null,layoutShift:M.layoutShift??null,domNodes:M.domNodesEnd??null,domGrowth:domGrowthAcrossRuns(w),pageLoad:M.pageLoad??null};return{...z,flags:S?[]:computeWebFlags({...z,frames:M.frames})}}).filter(p=>p.caseName!==WARMUP_CASE_NAME).map((p,f)=>({...p,batchIndex:f}));const l=pickBaseline(u,t);if(l){l.isBaseline=true;for(const p of u){if(p===l)continue;if(p.uiFps!=null&&l.uiFps!=null)p.deltaUiFps=round(p.uiFps-l.uiFps,1);if(p.jsFps!=null&&l.jsFps!=null)p.deltaJsFps=round(p.jsFps-l.jsFps,1)}}const d=u.filter(p=>!p.failed);const h=(p,f)=>{let w;let b=NaN;for(const _ of d){const k=p(_);if(k==null)continue;if(w===void 0||f(k,b)){w=_;b=k}}return w?.caseName};const y=p=>p.platform==="web"?p.longFrames??null:p.jsJank==null&&p.uiJank==null?null:(p.jsJank??0)+(p.uiJank??0);const v={uiFps:h(p=>p.uiFps,(p,f)=>p>f),jsFps:h(p=>p.jsFps,(p,f)=>p>f),lowestCpu:h(p=>p.cpu,(p,f)=>p<f),lowestMem:h(p=>p.memMb,(p,f)=>p<f),fewestJank:h(y,(p,f)=>p<f),fewestLongTasks:h(p=>p.longTaskPct??null,(p,f)=>p<f)};const m=d.filter(p=>p.flags.length>0).map(p=>({caseName:p.caseName,flags:p.flags}));const g=d.filter(p=>p.flags.length===0).sort((p,f)=>(f.uiFps??f.jsFps??0)-(p.uiFps??p.jsFps??0)||(p.cpu??p.longTaskPct??Infinity)-(f.cpu??f.longTaskPct??Infinity)||(p.memMb??Infinity)-(f.memMb??Infinity)).map(p=>p.caseName);return{batchId:n,baselineName:l?.caseName??null,deviceMaxRefreshRate:d.find(p=>p.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:u,leaders:v,flagged:m,balanced:g}}var FRESH_LOAD_MS=2e4;function pageLoadLines(e){const n=e.filter(r=>!r.failed);if(n.length===0||!n.every(r=>r.pageLoad&&r.pageLoad.pageAgeMs<FRESH_LOAD_MS)){return[]}const t=r=>r==null?"\u2014":String(Math.round(r));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(r=>{const s=r.pageLoad;return`| ${r.caseName} | ${t(s.ttfbMs)} | ${t(s.fcpMs)} | ${t(s.lcpMs)} | ${t(s.domContentLoadedMs)} | ${t(s.scriptKb)} | ${t(s.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function closeCallNote(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,i)=>(n(i)??0)-(n(o)??0));if(t.length<2)return null;const[r,s]=t;if((r.validRuns??0)<2||(s.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${r.caseName} and ${s.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((s.fpsMax??0)>=(r.fpsMin??0)){return`**Too close to call:** ${r.caseName} (${r.fpsMin}\u2013${r.fpsMax} FPS) and ${s.caseName} (${s.fpsMin}\u2013${s.fpsMax} FPS) overlap across runs. Run more repeats before picking between them.`}return null}function formatRankingMarkdown(e){const n=(h,y=1)=>h==null?"\u2014":h.toFixed(y);const t=h=>h==null?"":h>0?` (+${h.toFixed(1)})`:` (${h.toFixed(1)})`;const r=[];r.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(h=>h.platform==="web")){formatWebTable(e,r);return r.join("\n")}r.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);r.push("");const s=e.rows.some(h=>h.thermalState);const o=s?" Thermal |":"";const i=s?"-------|":"";r.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);r.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${i}-------|`);for(const h of e.rows){const y=s?` ${THERMAL_MARK[h.thermalState??"unknown"]??"\u2014"} |`:"";if(h.failed){r.push(`| ${h.batchIndex} | ${h.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${y} \u274C FAILED (${h.failureReason}) |`);continue}const v=h.flags.length?`\u26A0\uFE0F ${h.flags.join("; ")}`:"\u2713 clean";r.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)} |${y} ${v} |`)}const a=closeCallNote(e.rows);if(a){r.push("");r.push(a)}const c=e.rows.filter(h=>h.thermalState==="serious"||h.thermalState==="critical");if(c.length){r.push("");r.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.`)}r.push("");const u=e.leaders;const l=[u.uiFps&&`UI FPS \u2192 **${u.uiFps}**`,u.jsFps&&`JS FPS \u2192 **${u.jsFps}**`,u.lowestCpu&&`lowest CPU \u2192 **${u.lowestCpu}**`,u.lowestMem&&`lowest mem \u2192 **${u.lowestMem}**`,u.fewestJank&&`fewest jank \u2192 **${u.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(l)r.push(`**Per-metric best:** ${l}`);r.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){r.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(h=>`${h.caseName} (${h.flags.join("; ")})`).join(" \xB7 ")}`)}const d=e.rows.filter(h=>!h.failed&&(h.renderCommits!=null||h.topRenderers.length>0)).map(h=>{const y=h.topRenderers.map(g=>`${g.name} \xD7${g.renders} (${g.totalMs}ms${g.cause?`, ${g.cause}`:""})`).join(", ");const v=h.renderCommits!=null?`${h.renderCommits} commits`:"";const m=h.renderWasted!=null?`${h.renderWasted} wasted`:"";return`- **${h.caseName}** \u2014 ${[v,m,y].filter(Boolean).join(": ")}`});if(d.length){r.push("");r.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");r.push(...d)}r.push("");r.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 r.join("\n")}function compareReportStats(e,n){const t=(r,s)=>({a:{avg:round(r.avg,1),p95:round(r.p95,1)},b:{avg:round(s.avg,1),p95:round(s.p95,1)},deltaAvg:round(s.avg-r.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,n.renders)}}function compareRenders(e,n){if(!e&&!n)return null;const t=r=>r??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function round(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function formatWebTable(e,n){const t=(l,d=1)=>l==null?"\u2014":l.toFixed(d);const r=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.toFixed(1)})`;n.push(`**No single winner \u2014 weigh the trade-offs.** 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.`);n.push("");n.push(`| # | Case | FPS | p95 frame gap ms | Worst gap ms | Long frames | Long tasks % | Heap MB | Flags |`);n.push(`|---|------|----:|-----------------:|-------------:|------------:|-------------:|--------:|-------|`);for(const l of e.rows){const d=l.isBaseline?`${l.caseName} (baseline)`:l.caseName;if(l.failed){n.push(`| ${l.batchIndex} | ${d} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${l.failureReason}) |`);continue}const h=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"\u2713 clean";n.push(`| ${l.batchIndex} | ${d} | ${t(l.jsFps)}${r(l.deltaJsFps)} | ${t(l.frameGapP95Ms)} | ${t(l.frameGapMaxMs)} | ${t(l.longFrames,0)} | ${t(l.longTaskPct)} | ${t(l.memMb)} | ${h} |`)}const s=e.rows.filter(l=>!l.failed).map(l=>{const d=[l.loafBlockingMs!=null&&l.loafBlockingMs>0&&`slow frames blocked input ${l.loafBlockingMs} ms`,l.styleLayoutMs!=null&&l.styleLayoutMs>0&&`style + layout ${l.styleLayoutMs} ms`,l.forcedLayoutMs!=null&&l.forcedLayoutMs>0&&`forced layout ${l.forcedLayoutMs} ms`,l.worstInteractionMs!=null&&`slowest input ${l.worstInteractionMs} ms`,l.layoutShift!=null&&l.layoutShift>0&&`layout shift ${l.layoutShift}`,l.domNodes!=null&&`${l.domNodes} DOM nodes${l.domGrowth?` (+${l.domGrowth} over the runs)`:""}`,l.topScripts&&l.topScripts.length>0&&`top scripts: ${l.topScripts.slice(0,3).map(h=>`${h.name} ${h.ms} ms`).join(", ")}`].filter(Boolean);return d.length?`- **${l.caseName}**: ${d.join(" \xB7 ")}`:null}).filter(Boolean);if(s.length){n.push("");n.push(`**Why frames were slow** (Long Animation Frames, Event Timing, DOM size):`);n.push(...s)}const o=pageLoadLines(e.rows);if(o.length){n.push("");n.push(...o)}const i=closeCallNote(e.rows);if(i){n.push("");n.push(i)}const a=Array.from(new Set(e.rows.flatMap(l=>l.missingMetrics??[])));if(a.length){n.push("");n.push(`**Not measured in this browser:** ${a.join(", ")}. These show as \u2014 and can't win or lose.`)}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 u=[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(u)n.push(`**Per-metric leaders:** ${u}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.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._`)}var POLL_INTERVAL_MS=1500;var delay2=e=>new Promise(n=>setTimeout(n,e));var json2=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var caseSchema=import_zod3.z.object({name:import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),params:import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function readDeviceSettings(e,n){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:i,coolDownMs:a,discardWarmupRuns:c,shuffleCases:u,reloadBetweenCases:l,captureRenders:d}=t;return{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:i,coolDownMs:a,discardWarmupRuns:c,shuffleCases:u,reloadBetweenCases:l,captureRenders:d}}catch{return{}}}async function readPerfSnapshot(e,n){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,n,{timeoutMs:4e3});return t}function batchIdsIn(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function runCountFor(e,n){return(e??[]).filter(t=>t.batchId===n).length}function newestBatchId(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 failuresFromIndex(e,n){const t=new Map;for(const r of e??[]){if(r.batchId===n&&r.batchFailureReason&&!t.has(r.batchIndex??0)){t.set(r.batchIndex??0,r.batchFailureReason)}}return[...t.entries()].map(([r,s])=>({index:r,reason:s}))}function expectedRunsFor(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}var SETTLE_WAIT_MS=1e4;function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,n,t,r=SETTLE_WAIT_MS){const s=Date.now();while(Date.now()-s<r){const o=await readPerfSnapshot(e,n).catch(()=>null);if(o&&!isRunnerBusy(o.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(r/1e3)}s`);return false}async function waitForBatchByIndex(e,n,t){const{preBatchIds:r,expectedRuns:s,maxWaitMs:o,log:i}=t;const a=Date.now();let c="";let u=false;let l=0;while(Date.now()-a<o){let y;try{y=await readPerfSnapshot(e,n)}catch{await delay2(POLL_INTERVAL_MS);continue}const v=y.index??[];const m=y.live?.automation?.phase??"idle";const g=y.live?.automationCompleted;if(g?.phase==="cancelled"&&!r.has(g.batchId)){return{batchId:g.batchId,failures:[],cancelled:true}}if(g?.phase==="done"&&!r.has(g.batchId)){return{batchId:g.batchId,failures:g.failures??failuresFromIndex(v,g.batchId)}}if(m!=="idle")u=true;if(m!==c){c=m;i(`automation phase: ${m}`)}const p=[...batchIdsIn(v)].find(f=>!r.has(f));if(p){const f=runCountFor(v,p);if(s>0&&f>=s){await waitForRunnerSettled(e,n,i);i(`batch ${p} complete (${f}/${s} runs saved)`);return{batchId:p,failures:failuresFromIndex(v,p)}}if(m==="idle"&&u&&f>0){if(++l>=3){i(`batch ${p} settled (${f} runs, automation idle)`);return{batchId:p,failures:failuresFromIndex(v,p)}}}else{l=0}}await delay2(POLL_INTERVAL_MS)}const d=await readPerfSnapshot(e,n).catch(()=>null);const h=d?[...batchIdsIn(d.index)].find(y=>!r.has(y)):void 0;if(h&&d)return{batchId:h,failures:failuresFromIndex(d.index,h),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}function registerPerfMonitorTools(e,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:import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),maxWaitMs:import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const r=s=>process.stderr.write(`[buoy-mcp] ${s}
69
- `);try{const s=n.requireProDevice(t.deviceId);const o=await readDeviceSettings(n,s);const{config:i,warnings:a}=buildAutomationConfig(t,o);const c=estimateBatchMs(i);const u=t.maxWaitMs??c*3+6e4;const l=expectedRunsFor(i);if(!await waitForRunnerSettled(n,s,r)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const d=await readPerfSnapshot(n,s);const h=batchIdsIn(d.index);r(`starting batch: ${i.cases.length} cases \xD7 ${i.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${s}`);await n.callAction(PERF_TOOL_ID,"startAutomation",{config:i},s);const y=await waitForBatchByIndex(n,s,{preBatchIds:h,expectedRuns:l,maxWaitMs:u,log:r});if(y.cancelled){await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});return fail2(`Batch ${y.batchId} was cancelled before completing.`)}await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const v=await readPerfSnapshot(n,s);const m=rankBatchFromIndex(v.index??[],y.batchId,i.cases,y.failures);await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},s).catch(()=>{});if(m.rows.length===0){const f=n.connectedDevices().some(b=>b.deviceId===s);const w=!f?`The device disconnected during the batch (a browser tab that closes, reloads or sleeps drops off). Reconnect it and run again.`:y.timedOut?`No runs were saved before the wait cap (${Math.round(u/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 fail2(`Benchmark batch ${y.batchId||"(no id)"} saved no runs. ${w}`)}const g=formatRankingMarkdown(m);const p={batchId:y.batchId,leaders:m.leaders,balanced:m.balanced,flagged:m.flagged,rows:m.rows,failures:y.failures,warnings:a,estimateMs:c,...y.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return text(`${g}
147
+ `:""}function inactiveToolMessage(e){return`No ${e.toolId} result from the device. The ${e.toolId} tool may be inactive, or the app needs a newer Buoy (${e.mcpTool} requires the ${e.action} action).`}function withDevice(e,n){return async t=>{try{const r=e.requireProDevice(t.deviceId);return await n(t,r)}catch(r){return fail2(errMsg2(r))}}}var PERF_TOOL_ID="perf-monitor";var clamp=(e,n,t)=>Math.max(n,Math.min(t,e));var WARMUP_CASE_NAME="__batch_warmup__";var THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function pick(e,n,t){return e??n??t}function buildAutomationConfig(e,n={}){const t=[];const r=(e.targetRoute||"").trim();if(!r)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=r==="/"?"/_buoy_bounce":"/";if(s===r){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((h,d)=>{const m=(h.name||"").trim()||`case ${d+1}`;return{id:`case-${d}`,name:m,params:h.params??{},route:h.route}});const a=clamp(Math.round(pick(e.runsPerCase,n.runsPerCase,FALLBACK.runsPerCase)),1,10);const i=clamp(Math.round(pick(e.coolDownMs,n.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const c=clamp(Math.round(pick(e.discardWarmupRuns,n.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,a-1);const u=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&u===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 l={targetRoute:r,bounceRoute:s,perCaseDurationMs:clamp(Math.round(pick(e.perCaseDurationMs,n.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp(Math.round(pick(e.settleMs,n.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp(Math.round(pick(e.navTimeoutMs,n.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:u,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:l,warnings:t}}function describeBenchmarkSettings(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const r=estimateBatchMs(e);const s=r/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(r/1e3)}s`;const a=[`**${n(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const i=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${n(i)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function estimateBatchMs(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let r=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)r+=e.cases.length*3e3;return r}function computeFlags(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 computeWebFlags(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 r=Math.max(e.frameGapMaxMs??0,e.edgeSilenceMs??0);if(r>=100)n.push(`Freeze of ${Math.round(r)} 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 domGrowthAcrossRuns(e){const n=e.filter(r=>!r.batchFailureReason&&!r.discardedWarmup).sort((r,s)=>(r.runIndex??0)-(s.runIndex??0)).map(r=>r.webRun?.domNodesStart).filter(r=>typeof r==="number");if(n.length<3)return null;for(let r=1;r<n.length;r++)if(n[r]<=n[r-1])return null;const t=n[n.length-1]-n[0];return t>=Math.max(50,n[0]*.05)?t:null}function pickBaseline(e,n){const t=e.find(s=>s.isBaseline);if(t)return t;const r=n?.[0]?.name;if(r){const s=e.filter(o=>o.caseName===r);if(s.length===1)return s[0]}return e[0]}function rankBatchFromIndex(e,n,t,r=[]){const s=e.filter(p=>p.batchId===n);const o=new Map(r.map(p=>[p.index,p.reason]));const a=Array.from(new Set(s.map(p=>p.batchIndex??0))).sort((p,f)=>p-f);const i=p=>{const f=s.filter(k=>(k.batchIndex??0)===p);if(f.length===0)return void 0;const v=f.filter(k=>!k.batchFailureReason&&!k.discardedWarmup);const y=v.filter(k=>k.webRun?.fps!=null);if(y.length>0&&y.length===v.length){const k=[...y].sort((S,_)=>S.webRun.fps-_.webRun.fps);return k[Math.floor((k.length-1)/2)]}if(v.length>0){return v.find(k=>k.isMedianRun===true)??v.find(k=>k.runIndex===0||k.runIndex===void 0)??v[0]}return f.find(k=>k.batchFailureReason)??f[0]};const c=p=>typeof p==="number"?p:null;const u=a.map(p=>{const f=i(p);const v=s.filter(D=>(D.batchIndex??0)===p);const y=v.length;const k=v.filter(D=>!D.batchFailureReason&&!D.discardedWarmup);const S=D=>D.webRun?D.webRun.fps??D.jsFpsAvg:D.uiFpsAvg;const _=k.map(S).filter(D=>typeof D==="number");const M=f?.name??t?.[p]?.name??`case ${p+1}`;const B=s.some(D=>(D.batchIndex??0)===p&&D.isBaseline===true);const I=o.get(p)??f?.batchFailureReason;const x=v.every(D=>D.batchFailureReason||D.discardedWarmup);const $=Boolean(I)||!f||x;const T={batchIndex:p,caseName:M,params:f?.params??{},runs:y,failed:$,...f?.route?{route:f.route}:{},...B?{isBaseline:true}:{},failureReason:$?I??(x&&f?"only warmup runs were saved":"no run recorded"):void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,validRuns:k.length,fpsMin:_.length?round(Math.min(..._),1):null,fpsMax:_.length?round(Math.max(..._),1):null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};const A=f?.webRun;if(!A)return{...T,flags:$?[]:computeFlags(T)};const U=[...A.longTasksSupported?[]:["long tasks"],...A.heapSupported?[]:["JS heap"],...A.loaf===null?["long animation frames"]:[]];const N={...T,platform:"web",uiFps:null,uiJank:null,jsJank:null,cpu:null,memMb:A.heapSupported?T.memMb:null,deviceMaxRefreshRate:Math.max(T.deviceMaxRefreshRate??0,A.refreshHz??0)||null,longTaskPct:A.longTaskPct,frameGapP95Ms:A.frameGapP95Ms,frameGapMaxMs:A.frameGapMaxMs,edgeSilenceMs:A.edgeSilenceMs??null,jsFps:A.fps??T.jsFps,longFrames:A.longFrames,...U.length?{missingMetrics:U}:{},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:domGrowthAcrossRuns(v),pageLoad:A.pageLoad??null,refreshCalibrated:A.refreshCalibrated??null};return{...N,flags:$?[]:computeWebFlags({...N,frames:A.frames})}}).filter(p=>p.caseName!==WARMUP_CASE_NAME).map((p,f)=>({...p,batchIndex:f}));const l=pickBaseline(u,t);if(l){l.isBaseline=true;for(const p of u){if(p===l)continue;if(p.uiFps!=null&&l.uiFps!=null)p.deltaUiFps=round(p.uiFps-l.uiFps,1);if(p.jsFps!=null&&l.jsFps!=null)p.deltaJsFps=round(p.jsFps-l.jsFps,1)}}const h=u.filter(p=>!p.failed);const d=(p,f)=>{let v;let y=NaN;for(const k of h){const S=p(k);if(S==null)continue;if(v===void 0||f(S,y)){v=k;y=S}}return v?.caseName};const m=p=>p.platform==="web"?p.longFrames??null:p.jsJank==null&&p.uiJank==null?null:(p.jsJank??0)+(p.uiJank??0);const w={uiFps:d(p=>p.uiFps,(p,f)=>p>f),jsFps:d(p=>p.jsFps,(p,f)=>p>f),lowestCpu:d(p=>p.cpu,(p,f)=>p<f),lowestMem:d(p=>p.memMb,(p,f)=>p<f),fewestJank:d(m,(p,f)=>p<f),fewestLongTasks:d(p=>p.longTaskPct??null,(p,f)=>p<f)};const g=h.filter(p=>p.flags.length>0).map(p=>({caseName:p.caseName,flags:p.flags}));const b=h.filter(p=>p.flags.length===0).sort((p,f)=>(f.uiFps??f.jsFps??0)-(p.uiFps??p.jsFps??0)||(p.cpu??p.longTaskPct??Infinity)-(f.cpu??f.longTaskPct??Infinity)||(p.memMb??Infinity)-(f.memMb??Infinity)).map(p=>p.caseName);return{batchId:n,baselineName:l?.caseName??null,deviceMaxRefreshRate:h.find(p=>p.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:u,leaders:w,flagged:g,balanced:b}}function pageLoadLines(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 r=a=>(a??"").replace(/\/+$/,"")||"/";const s=a=>{const i=a.pageLoad;if(!i?.loadPath||r(i.loadPath)!==r(a.route))return false;const c=new URLSearchParams(i.loadSearch??"");const u=[...c.keys()];if(new Set(u).size!==u.length)return false;const l=Object.entries(a.params);return u.length===l.length&&l.every(([h,d])=>c.get(h)===d)};if(n.some(a=>!s(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 i=a.pageLoad;return`| ${a.caseName} | ${o(i.ttfbMs)} | ${o(i.fcpMs)} | ${o(i.lcpMs)} | ${o(i.domContentLoadedMs)} | ${o(i.scriptKb)} | ${o(i.totalKb)} |`}),"",`_KB is bytes over the network. 0 can mean the file came from cache, so compare cold loads with cold loads._`]}function closeCallNote(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[r,s]=t;if((r.validRuns??0)<2||(s.validRuns??0)<2){return`**Too few runs to separate the top cases.** ${r.caseName} and ${s.caseName} each need 2+ valid runs before their FPS gap means anything.`}if((s.fpsMax??0)>=(r.fpsMin??0)||(n(r)??0)-(n(s)??0)<=2){return`**Too close to call:** ${r.caseName} (${r.fpsMin}\u2013${r.fpsMax} FPS) and ${s.caseName} (${s.fpsMin}\u2013${s.fpsMax} FPS) overlap or sit within 2 FPS. No clear winner between them; more runs may separate them.`}return null}function formatRankingMarkdown(e){const n=(d,m=1)=>d==null?"\u2014":d.toFixed(m);const t=d=>d==null?"":d>0?` (+${d.toFixed(1)})`:` (${d.toFixed(1)})`;const r=[];r.push(`### Benchmark batch \`${e.batchId}\``);if(e.rows.some(d=>d.platform==="web")){formatWebTable(e,r);return r.join("\n")}r.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);r.push("");const s=e.rows.some(d=>d.thermalState);const o=s?" Thermal |":"";const a=s?"-------|":"";r.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);r.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const d of e.rows){const m=s?` ${THERMAL_MARK[d.thermalState??"unknown"]??"\u2014"} |`:"";if(d.failed){r.push(`| ${d.batchIndex} | ${d.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${m} \u274C FAILED (${d.failureReason}) |`);continue}const w=d.flags.length?`\u26A0\uFE0F ${d.flags.join("; ")}`:"no flags";r.push(`| ${d.batchIndex} | ${d.caseName} | ${n(d.uiFps)}${t(d.deltaUiFps)} | ${n(d.jsFps)}${t(d.deltaJsFps)} | ${n(d.cpu)} | ${n(d.memMb)} | ${n(d.jsJank,0)} | ${n(d.uiJank,0)} |${m} ${w} |`)}const i=closeCallNote(e.rows);if(i){r.push("");r.push(i)}const c=e.rows.filter(d=>d.thermalState==="serious"||d.thermalState==="critical");if(c.length){r.push("");r.push(`**\u{1F525} Thermally throttled: ${c.map(d=>d.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}r.push("");const u=e.leaders;const l=[u.uiFps&&`UI FPS \u2192 **${u.uiFps}**`,u.jsFps&&`JS FPS \u2192 **${u.jsFps}**`,u.lowestCpu&&`lowest CPU \u2192 **${u.lowestCpu}**`,u.lowestMem&&`lowest mem \u2192 **${u.lowestMem}**`,u.fewestJank&&`fewest jank \u2192 **${u.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(l)r.push(`**Per-metric best:** ${l}`);r.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){r.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(d=>`${d.caseName} (${d.flags.join("; ")})`).join(" \xB7 ")}`)}const h=e.rows.filter(d=>!d.failed&&(d.renderCommits!=null||d.topRenderers.length>0)).map(d=>{const m=d.topRenderers.map(b=>`${b.name} \xD7${b.renders} (${b.totalMs}ms${b.cause?`, ${b.cause}`:""})`).join(", ");const w=d.renderCommits!=null?`${d.renderCommits} commits`:"";const g=d.renderWasted!=null?`${d.renderWasted} wasted`:"";return`- **${d.caseName}** \u2014 ${[w,g,m].filter(Boolean).join(": ")}`});if(h.length){r.push("");r.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");r.push(...h)}r.push("");r.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 r.join("\n")}function compareReportStats(e,n){const t=(r,s)=>({a:{avg:round(r.avg,1),p95:round(r.p95,1)},b:{avg:round(s.avg,1),p95:round(s.p95,1)},deltaAvg:round(s.avg-r.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,n.renders)}}function compareWebReports(e,n){const t=(a,i,c)=>({a:a??null,b:i??null,delta:a!=null&&i!=null?round(i-a,1):null,better:c});const r=a=>a.webRun.heapSupported?round(a.stats.memoryUsage.max,1):null;const s=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(s.fps??e.stats.jsFps.avg,o.fps??n.stats.jsFps.avg,"higher"),frameGapP95Ms:t(s.frameGapP95Ms,o.frameGapP95Ms,"lower"),frameGapMaxMs:t(s.frameGapMaxMs,o.frameGapMaxMs,"lower"),longFrames:t(s.longFrames,o.longFrames,"lower"),longTaskPct:t(s.longTaskPct,o.longTaskPct,"lower"),slowFrameBlockingMs:t(s.loaf?.blockingMs,o.loaf?.blockingMs,"lower"),styleLayoutPaintMs:t(s.loaf?.styleLayoutMs,o.loaf?.styleLayoutMs,"lower"),forcedLayoutMs:t(s.loaf?.forcedLayoutMs,o.loaf?.forcedLayoutMs,"lower"),worstInteractionMs:t(s.interactions?.worstMs,o.interactions?.worstMs,"lower"),layoutShift:t(s.layoutShift,o.layoutShift,"lower"),domNodes:t(s.domNodesEnd,o.domNodesEnd,"lower"),jsHeapMb:t(r(e),r(n),"lower"),topScripts:{a:s.loaf?.topScripts??[],b:o.loaf?.topScripts??[]},...compareRenders(e.renders,n.renders)}}function compareRenders(e,n){if(!e&&!n)return null;const t=r=>r??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function round(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}function formatWebTable(e,n){const t=(l,h=1)=>l==null?"\u2014":l.toFixed(h);const r=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.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 l of e.rows){const h=l.isBaseline?`${l.caseName} (baseline)`:l.caseName;if(l.failed){n.push(`| ${l.batchIndex} | ${h} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u274C FAILED (${l.failureReason}) |`);continue}const d=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"no flags";n.push(`| ${l.batchIndex} | ${h} | ${t(l.jsFps)}${r(l.deltaJsFps)} | ${t(l.frameGapP95Ms)} | ${t(l.frameGapMaxMs)} | ${t(l.longFrames,0)} | ${t(l.longTaskPct)} | ${t(l.memMb)} | ${d} |`)}const s=e.rows.filter(l=>!l.failed).map(l=>{const h=[l.loafBlockingMs!=null&&l.loafBlockingMs>0&&`slow frames blocked input ${l.loafBlockingMs} ms`,l.styleLayoutMs!=null&&l.styleLayoutMs>0&&`style + layout ${l.styleLayoutMs} ms`,l.forcedLayoutMs!=null&&l.forcedLayoutMs>0&&`forced layout ${l.forcedLayoutMs} ms`,l.worstInteractionMs!=null&&`slowest input ${l.worstInteractionMs} ms`,l.layoutShift!=null&&l.layoutShift>0&&`layout shift ${l.layoutShift}`,l.domNodes!=null&&`${l.domNodes} DOM nodes${l.domGrowth?` (+${l.domGrowth} over the runs)`:""}`,l.topScripts&&l.topScripts.length>0&&`top scripts: ${l.topScripts.slice(0,3).map(d=>`${d.name} ${d.ms} ms`).join(", ")}`].filter(Boolean);return h.length?`- **${l.caseName}**: ${h.join(" \xB7 ")}`:null}).filter(Boolean);if(s.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(...s)}const o=pageLoadLines(e.rows);if(o.length){n.push("");n.push(...o)}const a=closeCallNote(e.rows);if(a){n.push("");n.push(a)}const i=Array.from(new Set(e.rows.flatMap(l=>l.missingMetrics??[])));if(i.length){n.push("");n.push(`**Not measured in this browser:** ${i.join(", ")}. These show as \u2014 and can't win or lose.`)}if(e.rows.some(l=>l.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 u=[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(u)n.push(`**Per-metric leaders:** ${u}`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.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._`)}var POLL_INTERVAL_MS=1500;var delay2=e=>new Promise(n=>setTimeout(n,e));var json2=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var caseSchema=import_zod3.z.object({name:import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),params:import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function readDeviceSettings(e,n){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:u,reloadBetweenCases:l,captureRenders:h}=t;return{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:u,reloadBetweenCases:l,captureRenders:h}}catch{return{}}}async function readPerfSnapshot(e,n){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,n,{timeoutMs:4e3});return t}function batchIdsIn(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function runCountFor(e,n){return(e??[]).filter(t=>t.batchId===n).length}function newestBatchId(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 failuresFromIndex(e,n){const t=new Map;for(const r of e??[]){if(r.batchId===n&&r.batchFailureReason&&!t.has(r.batchIndex??0)){t.set(r.batchIndex??0,r.batchFailureReason)}}return[...t.entries()].map(([r,s])=>({index:r,reason:s}))}function expectedRunsFor(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}var SETTLE_WAIT_MS=1e4;function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,n,t,r=SETTLE_WAIT_MS){const s=Date.now();while(Date.now()-s<r){const o=await readPerfSnapshot(e,n).catch(()=>null);if(o&&!isRunnerBusy(o.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(r/1e3)}s`);return false}async function waitForBatchByIndex(e,n,t){const{preBatchIds:r,expectedRuns:s,maxWaitMs:o,log:a}=t;const i=t.reloadsBetweenCases?10:3;const c=Date.now();let u="";let l=false;let h=0;while(Date.now()-c<o){let w;try{w=await readPerfSnapshot(e,n)}catch{await delay2(POLL_INTERVAL_MS);continue}const g=w.index??[];const b=w.live?.automation?.phase??"idle";const p=w.live?.automationCompleted;if(p?.phase==="cancelled"&&!r.has(p.batchId)){return{batchId:p.batchId,failures:[],cancelled:true}}if(p?.phase==="done"&&!r.has(p.batchId)){return{batchId:p.batchId,failures:p.failures??failuresFromIndex(g,p.batchId)}}if(b!=="idle")l=true;if(b!==u){u=b;a(`automation phase: ${b}`)}const f=[...batchIdsIn(g)].find(v=>!r.has(v));if(f){const v=runCountFor(g,f);if(s>0&&v>=s){await waitForRunnerSettled(e,n,a);a(`batch ${f} complete (${v}/${s} runs saved)`);return{batchId:f,failures:failuresFromIndex(g,f)}}const y=(g??[]).filter(M=>M.batchId===f&&M.name!==WARMUP_CASE_NAME).length;const k=w.live?.pendingBatchId===f;const S=Boolean(w.live&&"pendingBatchId"in w.live);const _=!t.reloadsBetweenCases||S;if(b==="idle"&&l&&y>0&&!k&&_){if(++h>=i){a(`batch ${f} settled (${v} runs, automation idle)`);return{batchId:f,failures:failuresFromIndex(g,f)}}}else{h=0}}await delay2(POLL_INTERVAL_MS)}const d=await readPerfSnapshot(e,n).catch(()=>null);const m=d?[...batchIdsIn(d.index)].find(w=>!r.has(w)):void 0;if(m&&d)return{batchId:m,failures:failuresFromIndex(d.index,m),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${u||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}function registerPerfMonitorTools(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:import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),maxWaitMs:import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const r=s=>process.stderr.write(`[buoy-mcp] ${s}
148
+ `);try{const s=n.requireProDevice(t.deviceId);const o=await readDeviceSettings(n,s);const{config:a,warnings:i}=buildAutomationConfig(t,o);const c=estimateBatchMs(a);const u=t.maxWaitMs??c*3+6e4;const l=expectedRunsFor(a);if(!await waitForRunnerSettled(n,s,r)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const h=await readPerfSnapshot(n,s);const d=batchIdsIn(h.index);r(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${s}`);await n.callAction(PERF_TOOL_ID,"startAutomation",{config:a},s);const m=await waitForBatchByIndex(n,s,{preBatchIds:d,expectedRuns:l,maxWaitMs:u,log:r,reloadsBetweenCases:a.reloadBetweenCases===true});if(m.cancelled){await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});return fail2(`Batch ${m.batchId} was cancelled before completing.`)}await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const w=await readPerfSnapshot(n,s);const g=rankBatchFromIndex(w.index??[],m.batchId,a.cases,m.failures);await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},s).catch(()=>{});if(g.rows.length===0){const f=n.connectedDevices().some(k=>k.deviceId===s);const v=w.live?.pendingBatchId===m.batchId;const y=!f?`The device disconnected during the batch (a browser tab that closes, reloads or sleeps drops off). Reconnect it and run again.`:v?`The batch reloaded the app between cases and never resumed. Check that <AutomationResumer/> is mounted (the web host mounts it), or pass reloadBetweenCases: false.`:m.timedOut?`No runs were saved before the wait cap (${Math.round(u/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 fail2(`Benchmark batch ${m.batchId||"(no id)"} saved no runs. ${y}`)}const b=formatRankingMarkdown(g);const p={batchId:m.batchId,leaders:g.leaders,balanced:g.balanced,flagged:g.flagged,rows:g.rows,failures:m.failures,warnings:i,estimateMs:c,...m.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return text(`${b}
70
149
 
71
150
  \`\`\`json
72
151
  ${JSON.stringify(p,null,2)}
73
- \`\`\``)}catch(s){return fail2(errMsg2(s))}});e.registerTool("get_automation_status",{description:"Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",inputSchema:{deviceId:import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async({deviceId:t})=>{try{const r=n.requireProDevice(t);const s=await readPerfSnapshot(n,r);return json2({deviceId:r,automation:s.live?.automation??{phase:"idle"},automationCompleted:s.live?.automationCompleted??null})}catch(r){return fail2(errMsg2(r))}});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:deviceIdInput,maxWaitMs:import_zod3.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")}},async({deviceId:t,maxWaitMs:r})=>{const s=i=>process.stderr.write(`[buoy-mcp] ${i}
74
- `);const o=r??6e5;try{const i=n.requireProDevice(t);const a=Date.now();let c=false;let u=0;let l=-1;let d=null;while(Date.now()-a<o){let v;try{v=await readPerfSnapshot(n,i)}catch{await delay2(POLL_INTERVAL_MS);continue}const m=v.live?.automationCompleted;if(m?.phase==="cancelled")return fail2(`Batch ${m.batchId} was cancelled.`);if(m?.phase==="done"){d={batchId:m.batchId,failures:m.failures??failuresFromIndex(v.index,m.batchId)};break}const g=v.live?.automation?.phase??"idle";if(g!=="idle")c=true;const p=newestBatchId(v.index);if(g==="idle"&&p){const f=runCountFor(v.index,p);if(f===l){if(c&&++u>=3){d={batchId:p,failures:failuresFromIndex(v.index,p)};break}if(!c&&Date.now()-a>4e3){d={batchId:p,failures:failuresFromIndex(v.index,p)};break}}else{u=0;l=f}}else{u=0}await delay2(POLL_INTERVAL_MS)}if(!d)return fail2("No automation batch completed within the wait window.");await n.callAction(PERF_TOOL_ID,"refreshIndex",{},i).catch(()=>{});const h=await readPerfSnapshot(n,i);const y=rankBatchFromIndex(h.index??[],d.batchId,void 0,d.failures);await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},i).catch(()=>{});return text(`${formatRankingMarkdown(y)}
152
+ \`\`\``)}catch(s){return fail2(errMsg2(s))}});e.registerTool("get_automation_status",{description:"Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",inputSchema:{deviceId:import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async({deviceId:t})=>{try{const r=n.requireProDevice(t);const s=await readPerfSnapshot(n,r);return json2({deviceId:r,automation:s.live?.automation??{phase:"idle"},automationCompleted:s.live?.automationCompleted??null})}catch(r){return fail2(errMsg2(r))}});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:deviceIdInput,maxWaitMs:import_zod3.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")}},async({deviceId:t,maxWaitMs:r})=>{const s=a=>process.stderr.write(`[buoy-mcp] ${a}
153
+ `);const o=r??6e5;try{const a=n.requireProDevice(t);const i=Date.now();let c=false;let u=0;let l=-1;let h=null;while(Date.now()-i<o){let w;try{w=await readPerfSnapshot(n,a)}catch{await delay2(POLL_INTERVAL_MS);continue}const g=w.live?.automationCompleted;if(g?.phase==="cancelled")return fail2(`Batch ${g.batchId} was cancelled.`);if(g?.phase==="done"){h={batchId:g.batchId,failures:g.failures??failuresFromIndex(w.index,g.batchId)};break}const b=w.live?.automation?.phase??"idle";if(b!=="idle")c=true;const p=newestBatchId(w.index);if(b==="idle"&&p){const f=runCountFor(w.index,p);if(f===l){if(c&&++u>=3){h={batchId:p,failures:failuresFromIndex(w.index,p)};break}if(!c&&Date.now()-i>4e3){h={batchId:p,failures:failuresFromIndex(w.index,p)};break}}else{u=0;l=f}}else{u=0}await delay2(POLL_INTERVAL_MS)}if(!h)return fail2("No automation batch completed within the wait window.");await n.callAction(PERF_TOOL_ID,"refreshIndex",{},a).catch(()=>{});const d=await readPerfSnapshot(n,a);const m=rankBatchFromIndex(d.index??[],h.batchId,void 0,h.failures);if(m.rows.length===0){await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},a).catch(()=>{});return fail2(`Batch ${h.batchId} saved no measured runs (only a warmup, or nothing). Check get_automation_status and run it again.`)}await n.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},a).catch(()=>{});return text(`${formatRankingMarkdown(m)}
75
154
 
76
155
  \`\`\`json
77
- ${JSON.stringify(y,null,2)}
78
- \`\`\``)}catch(i){return fail2(errMsg2(i))}});e.registerTool("get_batch_report",{description:"Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",inputSchema:{batchId:import_zod3.z.string().describe("The batch id to report on."),deviceId:deviceIdInput}},async({batchId:t,deviceId:r})=>{try{const s=n.requireProDevice(r);await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const o=await readPerfSnapshot(n,s);const i=(o.index??[]).filter(c=>c.batchId===t);if(i.length===0)return fail2(`No saved runs found for batch "${t}".`);const a=rankBatchFromIndex(o.index??[],t);return text(`${formatRankingMarkdown(a)}
156
+ ${JSON.stringify(m,null,2)}
157
+ \`\`\``)}catch(a){return fail2(errMsg2(a))}});e.registerTool("get_batch_report",{description:"Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",inputSchema:{batchId:import_zod3.z.string().describe("The batch id to report on."),deviceId:deviceIdInput}},async({batchId:t,deviceId:r})=>{try{const s=n.requireProDevice(r);await n.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const o=await readPerfSnapshot(n,s);const a=(o.index??[]).filter(c=>c.batchId===t);if(a.length===0)return fail2(`No saved runs found for batch "${t}".`);const i=rankBatchFromIndex(o.index??[],t);if(i.rows.length===0)return fail2(`Batch "${t}" has no measured runs, only a warmup.`);return text(`${formatRankingMarkdown(i)}
79
158
 
80
159
  \`\`\`json
81
- ${JSON.stringify(a,null,2)}
82
- \`\`\``)}catch(s){return fail2(errMsg2(s))}});e.registerTool("compare_reports",{description:"Compare two saved benchmark runs head-to-head: avg + p95 for FPS/CPU/memory, jank, AND 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:import_zod3.z.string().describe("First (baseline) report id."),reportIdB:import_zod3.z.string().describe("Second report id to compare against the baseline."),deviceId:deviceIdInput}},async({reportIdA:t,reportIdB:r,deviceId:s})=>{try{const o=n.requireProDevice(s);const[i,a]=await Promise.all([n.callAction(PERF_TOOL_ID,"loadReport",{id:t},o),n.callAction(PERF_TOOL_ID,"loadReport",{id:r},o)]);if(!i)return fail2(`Report "${t}" not found.`);if(!a)return fail2(`Report "${r}" not found.`);return json2(compareReportStats({name:i.metadata.name,stats:i.stats,renders:i.renders},{name:a.metadata.name,stats:a.stats,renders:a.renders}))}catch(o){return fail2(errMsg2(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:import_zod3.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),deviceId:deviceIdInput}},async({pages:t,deviceId:r})=>{try{const s=n.requireProDevice(r);const o=await readDeviceSettings(n,s);const i=Math.max(1,Math.round(t??1));const{config:a}=buildAutomationConfig({targetRoute:"/_preview",cases:Array.from({length:i},(u,l)=>({name:`page ${l+1}`}))},o);const c=Object.keys(o).length>0?"this device's saved settings":"default profile (none saved)";return text(`**Benchmark settings (${c}):** ${describeBenchmarkSettings(a)}
160
+ ${JSON.stringify(i,null,2)}
161
+ \`\`\``)}catch(s){return fail2(errMsg2(s))}});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:import_zod3.z.string().describe("First (baseline) report id."),reportIdB:import_zod3.z.string().describe("Second report id to compare against the baseline."),deviceId:deviceIdInput}},async({reportIdA:t,reportIdB:r,deviceId:s})=>{try{const o=n.requireProDevice(s);const[a,i]=await Promise.all([n.callAction(PERF_TOOL_ID,"loadReport",{id:t},o),n.callAction(PERF_TOOL_ID,"loadReport",{id:r},o)]);if(!a)return fail2(`Report "${t}" not found.`);if(!i)return fail2(`Report "${r}" not found.`);const c=a.metadata.webRun;const u=i.metadata.webRun;if(c&&u){return json2(compareWebReports({name:a.metadata.name,stats:a.stats,webRun:c,renders:a.renders},{name:i.metadata.name,stats:i.stats,webRun:u,renders:i.renders}))}if(c||u){return json2({...compareReportStats({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:i.metadata.name,stats:i.stats,renders:i.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 json2(compareReportStats({name:a.metadata.name,stats:a.stats,renders:a.renders},{name:i.metadata.name,stats:i.stats,renders:i.renders}))}catch(o){return fail2(errMsg2(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:import_zod3.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),deviceId:deviceIdInput}},async({pages:t,deviceId:r})=>{try{const s=n.requireProDevice(r);const o=await readDeviceSettings(n,s);const a=Math.max(1,Math.round(t??1));const{config:i}=buildAutomationConfig({targetRoute:"/_preview",cases:Array.from({length:a},(u,l)=>({name:`page ${l+1}`}))},o);const c=Object.keys(o).length>0?"this device's saved settings":"default profile (none saved)";return text(`**Benchmark settings (${c}):** ${describeBenchmarkSettings(i)}
83
162
 
84
163
  \`\`\`json
85
164
  ${JSON.stringify(o,null,2)}
86
- \`\`\``)}catch(s){return fail2(errMsg2(s))}});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:import_zod3.z.number().optional().describe("Recording length per page (ms)."),settleMs:import_zod3.z.number().optional().describe("Idle after navigation before recording (ms)."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases (ms)."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),runsPerCase:import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERF_TOOL_ID,"getAutomationConfig",{},r)??{};const o=["perCaseDurationMs","settleMs","coolDownMs","navTimeoutMs","runsPerCase","discardWarmupRuns","shuffleCases","reloadBetweenCases","captureRenders"];const i={};for(const d of o){const h=t[d];if(h!==void 0)i[d]=h}if(Object.keys(i).length===0){return fail2("No settings provided to save. Pass at least one field (e.g. perCaseDurationMs).")}const a={...s,...i};const c=await n.callAction(PERF_TOOL_ID,"setAutomationConfig",{config:a},r);const u=await readDeviceSettings(n,r);const{config:l}=buildAutomationConfig({targetRoute:"/_preview",cases:[{name:"page 1"}]},u);return text(`Saved. **New benchmark settings:** ${describeBenchmarkSettings(l)}
165
+ \`\`\``)}catch(s){return fail2(errMsg2(s))}});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:import_zod3.z.number().optional().describe("Recording length per page (ms)."),settleMs:import_zod3.z.number().optional().describe("Idle after navigation before recording (ms)."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases (ms)."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),runsPerCase:import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERF_TOOL_ID,"getAutomationConfig",{},r)??{};const o=["perCaseDurationMs","settleMs","coolDownMs","navTimeoutMs","runsPerCase","discardWarmupRuns","shuffleCases","reloadBetweenCases","captureRenders"];const a={};for(const h of o){const d=t[h];if(d!==void 0)a[h]=d}if(Object.keys(a).length===0){return fail2("No settings provided to save. Pass at least one field (e.g. perCaseDurationMs).")}const i={...s,...a};const c=await n.callAction(PERF_TOOL_ID,"setAutomationConfig",{config:i},r);const u=await readDeviceSettings(n,r);const{config:l}=buildAutomationConfig({targetRoute:"/_preview",cases:[{name:"page 1"}]},u);return text(`Saved. **New benchmark settings:** ${describeBenchmarkSettings(l)}
87
166
 
88
167
  \`\`\`json
89
168
  ${JSON.stringify(c??u,null,2)}
90
- \`\`\``)}catch(r){return fail2(errMsg2(r))}})}var import_zod4=require("zod");var EVENTS_TOOL_ID="events";var SOURCE_ALIASES={storage:["storage-async","storage-mmkv"],"react-query":["react-query","react-query-query","react-query-mutation"]};function expandSources(e){if(!e||e.length===0)return void 0;return[...new Set(e.flatMap(n=>SOURCE_ALIASES[n]??[n]))]}function registerEventsTools(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:import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),sources:import_zod4.z.array(import_zod4.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),status:import_zod4.z.enum(["all","errors","success","pending"]).optional().describe("Filter by status. Default all."),includeData:import_zod4.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),format:import_zod4.z.enum(["markdown","json","text","mermaid"]).optional().describe("Output format. Default markdown."),preset:import_zod4.z.enum(["llm","bugReport","json","errors","minimal","mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s={};const o=expandSources(t.sources);if(o)s.filterSources=o;if(t.status)s.filterMode=t.status;if(t.includeData!==void 0)s.includeEventData=t.includeData;if(t.format)s.format=t.format==="text"?"plaintext":t.format;const i=await n.callAction(EVENTS_TOOL_ID,"exportEvents",{preset:t.preset,settings:s,limit:t.limit??25},r);if(!i||typeof i.output!=="string"){return fail2("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 a=i.includedData?"":" \u2014 compact view; pass includeData:true, a specific source, or a bigger limit for full payloads";const c=`
169
+ \`\`\``)}catch(r){return fail2(errMsg2(r))}})}var import_zod4=require("zod");var EVENTS_TOOL_ID="events";var SOURCE_ALIASES={storage:["storage-async","storage-mmkv"],"react-query":["react-query","react-query-query","react-query-mutation"]};function expandSources(e){if(!e||e.length===0)return void 0;return[...new Set(e.flatMap(n=>SOURCE_ALIASES[n]??[n]))]}function registerEventsTools(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:import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),sources:import_zod4.z.array(import_zod4.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),status:import_zod4.z.enum(["all","errors","success","pending"]).optional().describe("Filter by status. Default all."),includeData:import_zod4.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),format:import_zod4.z.enum(["markdown","json","text","mermaid"]).optional().describe("Output format. Default markdown."),preset:import_zod4.z.enum(["llm","bugReport","json","errors","minimal","mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s={};const o=expandSources(t.sources);if(o)s.filterSources=o;if(t.status)s.filterMode=t.status;if(t.includeData!==void 0)s.includeEventData=t.includeData;if(t.format)s.format=t.format==="text"?"plaintext":t.format;const a=await n.callAction(EVENTS_TOOL_ID,"exportEvents",{preset:t.preset,settings:s,limit:t.limit??25},r);if(!a||typeof a.output!=="string"){return fail2("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 i=a.includedData?"":" \u2014 compact view; pass includeData:true, a specific source, or a bigger limit for full payloads";const c=`
91
170
 
92
- _Showing ${i.returned} of ${i.totalAvailable} captured event(s)${a}._`;return text((i.output||"(no events captured)")+c)}catch(r){return fail2(errMsg2(r))}})}var import_zod5=require("zod");var NETWORK_TOOL_ID="network";var masterNote=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 parseSnapshot(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 statusIcon(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 statusLabel(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 formatBytes(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 matchesStatus(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 truncate(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 MAX_BODY_FETCHES=8;async function resolveBodies(e,n,t,r=MAX_BODY_FETCHES){const s=n.filter(c=>c.id&&(c.requestBodyOmitted===true&&c.requestData===void 0||c.responseBodyOmitted===true&&c.responseData===void 0));const o=new Map;if(s.length===0)return{bodies:o,skipped:0};const i=s.slice(0,r);const a=await Promise.allSettled(i.map(c=>e.callAction(NETWORK_TOOL_ID,"getEventBody",{id:c.id},t)));a.forEach((c,u)=>{const l=i[u].id;const d=c.status==="fulfilled"?c.value:null;o.set(l,d?{...d}:{unresolved:true})});return{bodies:o,skipped:s.length-i.length}}async function captureDiagnosis(e,n){let t=null;try{t=await e.callAction(NETWORK_TOOL_ID,"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 skippedNote(e){return e>0?`
171
+ _Showing ${a.returned} of ${a.totalAvailable} captured event(s)${i}._`;return text((a.output||"(no events captured)")+c)}catch(r){return fail2(errMsg2(r))}})}var import_zod5=require("zod");var NETWORK_TOOL_ID="network";var masterNote=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 parseSnapshot(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 statusIcon(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 statusLabel(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 formatBytes(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 matchesStatus(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 truncate(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 MAX_BODY_FETCHES=8;async function resolveBodies(e,n,t,r=MAX_BODY_FETCHES){const s=n.filter(c=>c.id&&(c.requestBodyOmitted===true&&c.requestData===void 0||c.responseBodyOmitted===true&&c.responseData===void 0));const o=new Map;if(s.length===0)return{bodies:o,skipped:0};const a=s.slice(0,r);const i=await Promise.allSettled(a.map(c=>e.callAction(NETWORK_TOOL_ID,"getEventBody",{id:c.id},t)));i.forEach((c,u)=>{const l=a[u].id;const h=c.status==="fulfilled"?c.value:null;o.set(l,h?{...h}:{unresolved:true})});return{bodies:o,skipped:s.length-a.length}}async function captureDiagnosis(e,n){let t=null;try{t=await e.callAction(NETWORK_TOOL_ID,"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 skippedNote(e){return e>0?`
93
172
 
94
- _${e} more withheld ${e===1?"body was":"bodies were"} not fetched (cap ${MAX_BODY_FETCHES} per listing, they are large). Get one in full with its id._`:""}function bodyLines(e,n){const t=e.id?n.get(e.id):void 0;const r=(s,o,i)=>{const a=s??o;if(a!==void 0&&a!==null)return truncate(a);if(!i)return"(none)";return t?.unresolved?"(withheld \u2014 the device did not answer getEventBody)":"(none)"};return`
173
+ _${e} more withheld ${e===1?"body was":"bodies were"} not fetched (cap ${MAX_BODY_FETCHES} per listing, they are large). Get one in full with its id._`:""}function bodyLines(e,n){const t=e.id?n.get(e.id):void 0;const r=(s,o,a)=>{const i=s??o;if(i!==void 0&&i!==null)return truncate(i);if(!a)return"(none)";return t?.unresolved?"(withheld \u2014 the device did not answer getEventBody)":"(none)"};return`
95
174
  \u2514\u2500 request: ${r(e.requestData,t?.requestData,e.requestBodyOmitted)}
96
175
  \u2514\u2500 response: ${r(e.responseData,t?.responseData,e.responseBodyOmitted)}`}function formatRow(e,n,t){const r=n.path||n.url;const s=[`${n.duration!==void 0?`${Math.round(n.duration)}ms`:"\u2014"}`,formatBytes(n.responseSize),n.host??"",n.requestClient??""].filter(Boolean);const o=n.operationName?` (${n.operationName})`:"";return`${e}. \`${n.id}\` ${statusIcon(n)} ${statusLabel(n)} ${n.method} ${r}${o}${t?` ${t}`:""}
97
- \u2514\u2500 ${s.join(" \xB7 ")}`}function registerNetworkTools(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:import_zod5.z.enum(["get","set"]).describe("Read the effective profile or select one."),profile:import_zod5.z.enum(["normal","offline","slow","verySlow"]).optional().describe("Required for set."),deviceId:deviceIdInput}},async t=>{try{if(t.action==="set"&&!t.profile)return fail2("Set requires a profile.");const r=n.resolveDevice(t.deviceId);const s=await n.callAction(NETWORK_TOOL_ID,t.action==="get"?"getNetworkConditions":"setNetworkConditions",t.action==="set"?{profile:t.profile}:void 0,r);if(!s||typeof s!=="object"||!("profile"in s))return fail2("The device did not report its effective network conditions.");return text(JSON.stringify(s))}catch(r){return fail2(errMsg2(r))}});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. Requires the network integration and Buoy Pro.",inputSchema:{limit:import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),pattern:import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),status:import_zod5.z.enum(["all","errors","success","pending"]).optional().describe("Filter by outcome. Default all."),flagged:import_zod5.z.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:import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(NETWORK_TOOL_ID,r);const{events:o,saved:i}=parseSnapshot(s);const a=typeof t.limit==="number"&&t.limit>0?t.limit:25;const c=t.pattern?.toLowerCase();const u=new Set;const l=new Set;for(const m of i){if(m.pinned)u.add(m.event?.id);if(m.saved)l.add(m.event?.id);if(m.pinned)u.add(m.liveId);if(m.saved)l.add(m.liveId)}if(t.flagged){const m=i.filter(w=>t.flagged==="pinned"?w.pinned:t.flagged==="saved"?w.saved:w.pinned||w.saved);const g=m.filter(w=>w.event&&matchesStatus(w.event,t.status)).filter(w=>!c||(w.event.url??"").toLowerCase().includes(c)).slice(0,a);if(g.length===0){return text(`No ${t.flagged==="any"?"pinned or saved":t.flagged} requests on ${r}.
176
+ \u2514\u2500 ${s.join(" \xB7 ")}`}function registerNetworkTools(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:import_zod5.z.enum(["get","set"]).describe("Read the effective profile or select one."),profile:import_zod5.z.enum(["normal","offline","slow","verySlow"]).optional().describe("Required for set."),deviceId:deviceIdInput}},async t=>{try{if(t.action==="set"&&!t.profile)return fail2("Set requires a profile.");const r=n.resolveDevice(t.deviceId);const s=await n.callAction(NETWORK_TOOL_ID,t.action==="get"?"getNetworkConditions":"setNetworkConditions",t.action==="set"?{profile:t.profile}:void 0,r);if(!s||typeof s!=="object"||!("profile"in s))return fail2("The device did not report its effective network conditions.");return text(JSON.stringify(s))}catch(r){return fail2(errMsg2(r))}});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. Requires the network integration and Buoy Pro.",inputSchema:{limit:import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),pattern:import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),status:import_zod5.z.enum(["all","errors","success","pending"]).optional().describe("Filter by outcome. Default all."),flagged:import_zod5.z.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:import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(NETWORK_TOOL_ID,r);const{events:o,saved:a}=parseSnapshot(s);const i=typeof t.limit==="number"&&t.limit>0?t.limit:25;const c=t.pattern?.toLowerCase();const u=new Set;const l=new Set;for(const g of a){if(g.pinned)u.add(g.event?.id);if(g.saved)l.add(g.event?.id);if(g.pinned)u.add(g.liveId);if(g.saved)l.add(g.liveId)}if(t.flagged){const g=a.filter(v=>t.flagged==="pinned"?v.pinned:t.flagged==="saved"?v.saved:v.pinned||v.saved);const b=g.filter(v=>v.event&&matchesStatus(v.event,t.status)).filter(v=>!c||(v.event.url??"").toLowerCase().includes(c)).slice(0,i);if(b.length===0){return text(`No ${t.flagged==="any"?"pinned or saved":t.flagged} requests on ${r}.
98
177
 
99
- 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 resolveBodies(n,g.map(w=>w.event),r):{bodies:new Map,skipped:0};const f=g.map((w,b)=>{const _=[w.pinned?"\u{1F4CC} pinned":"",w.saved?"\u{1F516} saved":""].filter(Boolean).join(" ");const k=w.event.id?.startsWith("saved:")?" \xB7 from an earlier app run":"";const $=w.bodyTruncated?" \xB7 body truncated":"";let A=formatRow(b+1,w.event,_);A+=`
100
- \u2514\u2500 key \`${w.key}\`${k}${$}`;if(t.includeBodies)A+=bodyLines(w.event,p.bodies);return A}).join("\n");return text(`**Kept requests on ${r}** \u2014 ${g.length} of ${m.length}
178
+ 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 resolveBodies(n,b.map(v=>v.event),r):{bodies:new Map,skipped:0};const f=b.map((v,y)=>{const k=[v.pinned?"\u{1F4CC} pinned":"",v.saved?"\u{1F516} saved":""].filter(Boolean).join(" ");const S=v.event.id?.startsWith("saved:")?" \xB7 from an earlier app run":"";const _=v.bodyTruncated?" \xB7 body truncated":"";let M=formatRow(y+1,v.event,k);M+=`
179
+ \u2514\u2500 key \`${v.key}\`${S}${_}`;if(t.includeBodies)M+=bodyLines(v.event,p.bodies);return M}).join("\n");return text(`**Kept requests on ${r}** \u2014 ${b.length} of ${g.length}
101
180
 
102
181
  ${f}
103
182
 
104
- _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._${skippedNote(p.skipped)}`)}const d=o.filter(m=>matchesStatus(m,t.status)).filter(m=>!c||(m.url??"").toLowerCase().includes(c)).slice(0,a);if(d.length===0){const m=o.length===0?await captureDiagnosis(n,r):"No request matched the filter.";return text(`No requests on ${r}. ${m}`)}const h=t.includeBodies?await resolveBodies(n,d,r):{bodies:new Map,skipped:0};const y=d.map((m,g)=>{const p=[u.has(m.id)?"\u{1F4CC}":"",l.has(m.id)?"\u{1F516}":""].filter(Boolean).join("");let f=formatRow(g+1,m,p);if(t.includeBodies)f+=bodyLines(m,h.bodies);return f}).join("\n");const v=i.length>0?` \xB7 ${i.length} kept (flagged:'any' to list)`:"";return text(`**Network requests on ${r}** \u2014 ${d.length} of ${o.length}${v}
183
+ _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._${skippedNote(p.skipped)}`)}const h=o.filter(g=>matchesStatus(g,t.status)).filter(g=>!c||(g.url??"").toLowerCase().includes(c)).slice(0,i);if(h.length===0){const g=o.length===0?await captureDiagnosis(n,r):"No request matched the filter.";return text(`No requests on ${r}. ${g}`)}const d=t.includeBodies?await resolveBodies(n,h,r):{bodies:new Map,skipped:0};const m=h.map((g,b)=>{const p=[u.has(g.id)?"\u{1F4CC}":"",l.has(g.id)?"\u{1F516}":""].filter(Boolean).join("");let f=formatRow(b+1,g,p);if(t.includeBodies)f+=bodyLines(g,d.bodies);return f}).join("\n");const w=a.length>0?` \xB7 ${a.length} kept (flagged:'any' to list)`:"";return text(`**Network requests on ${r}** \u2014 ${h.length} of ${o.length}${w}
105
184
 
106
- ${y}
185
+ ${m}
107
186
 
108
- _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${d[0].id}"})._${skippedNote(h.skipped)}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod5.z.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:import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),key:import_zod5.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=["pin","unpin","save","unsave"].includes(t.action);if(s&&!t.id){return fail2(`"${t.action}" needs an \`id\`. List requests with get_network_requests to get one.`)}if(t.action==="remove"&&!t.key){return fail2('"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(NETWORK_TOOL_ID,"setPinned",{id:t.id,pinned:t.action==="pin"},r);if(o===null)return fail2(`No request "${t.id}" on ${r}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const i=typeof o==="object"&&o!==null&&o.reason==="pin-cap";if(i)return fail2(`Pin limit reached on ${r}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);return text(`${t.action==="pin"?"\u{1F4CC} Pinned":"Unpinned"} \`${t.id}\` on ${r}.`)}case"save":case"unsave":{const o=await n.callAction(NETWORK_TOOL_ID,"setSaved",{id:t.id,saved:t.action==="save"},r);if(o===null)return fail2(`No request "${t.id}" on ${r}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);const i=typeof o==="object"&&o!==null&&o.reason==="saved-cap";if(i)return fail2(`Saved-request limit reached on ${r}. Remove one first, or network_action({action:"clearSaved"}).`);return text(`${t.action==="save"?"\u{1F516} Saved":"Unsaved"} \`${t.id}\` on ${r}.`)}case"remove":await n.callAction(NETWORK_TOOL_ID,"removeSavedRecord",{key:t.key},r);return text(`Removed kept record \`${t.key}\` on ${r}.`);case"clearPinned":await n.callAction(NETWORK_TOOL_ID,"clearPinnedRequests",{},r);return text(`Unpinned all requests on ${r} (saved ones kept).`);case"clearSaved":await n.callAction(NETWORK_TOOL_ID,"clearSavedRequests",{},r);return text(`Cleared saved requests on ${r} (pinned ones kept).`);case"clearRequests":await n.callAction(NETWORK_TOOL_ID,"clearEvents",{},r);return text(`Clear request sent to ${r}. Re-list to confirm the live capture list; this action does not clear retained records.`)}}catch(r){return fail2(errMsg2(r))}});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:import_zod5.z.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:import_zod5.z.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:import_zod5.z.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:import_zod5.z.array(import_zod5.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),kind:import_zod5.z.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:import_zod5.z.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:import_zod5.z.union([import_zod5.z.string(),import_zod5.z.record(import_zod5.z.unknown()),import_zod5.z.array(import_zod5.z.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:import_zod5.z.record(import_zod5.z.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:import_zod5.z.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:import_zod5.z.record(import_zod5.z.string()).optional().describe("For kind 'respond': response headers to send."),failKind:import_zod5.z.enum(["network","timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),delayMs:import_zod5.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),times:import_zod5.z.number().optional().describe("Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."),alternate:import_zod5.z.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:import_zod5.z.string().optional().describe("Label shown in the developer's UI."),enabled:import_zod5.z.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:import_zod5.z.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:import_zod5.z.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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);switch(t.action){case"add":{if(!t.urlPattern&&!t.fromRequestId){return fail2("`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 s=await n.callAction(NETWORK_TOOL_ID,"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}},r);const o=s;if(!o?.ok){return fail2(o?.error??`Could not add the rule on ${r}.`)}const i=o.rule?.urlPattern??t.urlPattern;const a=o.rule?.enabled===false?`
187
+ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${h[0].id}"})._${skippedNote(d.skipped)}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod5.z.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:import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),key:import_zod5.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=["pin","unpin","save","unsave"].includes(t.action);if(s&&!t.id){return fail2(`"${t.action}" needs an \`id\`. List requests with get_network_requests to get one.`)}if(t.action==="remove"&&!t.key){return fail2('"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(NETWORK_TOOL_ID,"setPinned",{id:t.id,pinned:t.action==="pin"},r);if(o===null)return fail2(`No request "${t.id}" on ${r}. 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 fail2(`Pin limit reached on ${r}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);return text(`${t.action==="pin"?"\u{1F4CC} Pinned":"Unpinned"} \`${t.id}\` on ${r}.`)}case"save":case"unsave":{const o=await n.callAction(NETWORK_TOOL_ID,"setSaved",{id:t.id,saved:t.action==="save"},r);if(o===null)return fail2(`No request "${t.id}" on ${r}. 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 fail2(`Saved-request limit reached on ${r}. Remove one first, or network_action({action:"clearSaved"}).`);return text(`${t.action==="save"?"\u{1F516} Saved":"Unsaved"} \`${t.id}\` on ${r}.`)}case"remove":await n.callAction(NETWORK_TOOL_ID,"removeSavedRecord",{key:t.key},r);return text(`Removed kept record \`${t.key}\` on ${r}.`);case"clearPinned":await n.callAction(NETWORK_TOOL_ID,"clearPinnedRequests",{},r);return text(`Unpinned all requests on ${r} (saved ones kept).`);case"clearSaved":await n.callAction(NETWORK_TOOL_ID,"clearSavedRequests",{},r);return text(`Cleared saved requests on ${r} (pinned ones kept).`);case"clearRequests":await n.callAction(NETWORK_TOOL_ID,"clearEvents",{},r);return text(`Clear request sent to ${r}. Re-list to confirm the live capture list; this action does not clear retained records.`)}}catch(r){return fail2(errMsg2(r))}});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:import_zod5.z.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:import_zod5.z.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:import_zod5.z.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:import_zod5.z.array(import_zod5.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),kind:import_zod5.z.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:import_zod5.z.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:import_zod5.z.union([import_zod5.z.string(),import_zod5.z.record(import_zod5.z.unknown()),import_zod5.z.array(import_zod5.z.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:import_zod5.z.record(import_zod5.z.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:import_zod5.z.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:import_zod5.z.record(import_zod5.z.string()).optional().describe("For kind 'respond': response headers to send."),failKind:import_zod5.z.enum(["network","timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),delayMs:import_zod5.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),times:import_zod5.z.number().optional().describe("Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."),alternate:import_zod5.z.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:import_zod5.z.string().optional().describe("Label shown in the developer's UI."),enabled:import_zod5.z.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:import_zod5.z.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:import_zod5.z.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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);switch(t.action){case"add":{if(!t.urlPattern&&!t.fromRequestId){return fail2("`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 s=await n.callAction(NETWORK_TOOL_ID,"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}},r);const o=s;if(!o?.ok){return fail2(o?.error??`Could not add the rule on ${r}.`)}const a=o.rule?.urlPattern??t.urlPattern;const i=o.rule?.enabled===false?`
109
188
 
110
- It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${o.rule?.id}"}).`:"";return text(`Override added on ${r} \u2014 \`${o.rule?.id}\` matching \`${i}\`.${a}
189
+ It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${o.rule?.id}"}).`:"";return text(`Override added on ${r} \u2014 \`${o.rule?.id}\` matching \`${a}\`.${i}
111
190
 
112
- 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 fail2('`remove` needs an `id`. List them with network_override({action:"list"}).')}await n.callAction(NETWORK_TOOL_ID,"deleteOverrideRule",{id:t.id},r);return text(`Removed override \`${t.id}\` on ${r}.`)}case"clear":await n.callAction(NETWORK_TOOL_ID,"clearOverrideRules",{},r);return text(`Removed all override rules on ${r}. Other interception or simulation settings may still apply.`);case"enable":case"disable":{const s=t.action==="enable";if(t.id){await n.callAction(NETWORK_TOOL_ID,"setOverrideRuleEnabled",{id:t.id,enabled:s},r);return text(`Rule \`${t.id}\` was requested ${s?"ON":"OFF"} on ${r}. The other rules and the master switch are unchanged.`)}await n.callAction(NETWORK_TOOL_ID,"setOverridesEnabled",{enabled:s},r);return text(s?`Overrides enabled in settings for ${r}.`:`Overrides switched OFF for ${r}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`)}case"list":{const s=await n.callAction(NETWORK_TOOL_ID,"listOverrideRules",{},r);const o=s?.rules??[];if(t.id){const a=o.find(c=>c.id===t.id);if(!a){return fail2(`No override rule \`${t.id}\` on ${r}. List them with network_override({action:"list"}).`)}return text(`**Override rule \`${a.id}\`** on ${r}${masterNote(s)}
191
+ 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 fail2('`remove` needs an `id`. List them with network_override({action:"list"}).')}await n.callAction(NETWORK_TOOL_ID,"deleteOverrideRule",{id:t.id},r);return text(`Removed override \`${t.id}\` on ${r}.`)}case"clear":await n.callAction(NETWORK_TOOL_ID,"clearOverrideRules",{},r);return text(`Removed all override rules on ${r}. Other interception or simulation settings may still apply.`);case"enable":case"disable":{const s=t.action==="enable";if(t.id){await n.callAction(NETWORK_TOOL_ID,"setOverrideRuleEnabled",{id:t.id,enabled:s},r);return text(`Rule \`${t.id}\` was requested ${s?"ON":"OFF"} on ${r}. The other rules and the master switch are unchanged.`)}await n.callAction(NETWORK_TOOL_ID,"setOverridesEnabled",{enabled:s},r);return text(s?`Overrides enabled in settings for ${r}.`:`Overrides switched OFF for ${r}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`)}case"list":{const s=await n.callAction(NETWORK_TOOL_ID,"listOverrideRules",{},r);const o=s?.rules??[];if(t.id){const i=o.find(c=>c.id===t.id);if(!i){return fail2(`No override rule \`${t.id}\` on ${r}. List them with network_override({action:"list"}).`)}return text(`**Override rule \`${i.id}\`** on ${r}${masterNote(s)}
113
192
 
114
193
  \`\`\`json
115
- ${JSON.stringify(a,null,2)}
194
+ ${JSON.stringify(i,null,2)}
116
195
  \`\`\``)}if(o.length===0){return text(`No override rules on ${r}.
117
196
 
118
- Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`)}const i=o.map((a,c)=>{const u=a.kind==="respond"?`\u2192 ${a.status??200}${a.statusText?` ${a.statusText}`:""}`:a.kind==="fail"?`\u2192 ${a.failKind==="timeout"?"timeout":"network error"}`:"\u2192 real response";const l=[a.methods?.length?a.methods.join("/"):"any method",u,a.kind==="respond"&&a.body?`body ${a.body.length}B`:"",a.headers&&Object.keys(a.headers).length?`${Object.keys(a.headers).length} header(s)`:"",a.delayMs?`${a.delayMs}ms delay`:"",a.alternate?"every other request":"",typeof a.times==="number"?`${a.times}\xD7 then off`:"",`${a.hits} hit${a.hits===1?"":"s"}`,a.seen!=null&&a.seen!==a.hits?`${a.seen} matched`:"",a.enabled?"":"DISABLED"].filter(Boolean);return`${c+1}. \`${a.id}\`${a.name?` ${a.name}`:""}
119
- \u2514\u2500 \`${a.urlPattern}\` \xB7 ${l.join(" \xB7 ")}`});return text(`**Override rules on ${r}**${masterNote(s)}
197
+ Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`)}const a=o.map((i,c)=>{const u=i.kind==="respond"?`\u2192 ${i.status??200}${i.statusText?` ${i.statusText}`:""}`:i.kind==="fail"?`\u2192 ${i.failKind==="timeout"?"timeout":"network error"}`:"\u2192 real response";const l=[i.methods?.length?i.methods.join("/"):"any method",u,i.kind==="respond"&&i.body?`body ${i.body.length}B`:"",i.headers&&Object.keys(i.headers).length?`${Object.keys(i.headers).length} header(s)`:"",i.delayMs?`${i.delayMs}ms delay`:"",i.alternate?"every other request":"",typeof i.times==="number"?`${i.times}\xD7 then off`:"",`${i.hits} hit${i.hits===1?"":"s"}`,i.seen!=null&&i.seen!==i.hits?`${i.seen} matched`:"",i.enabled?"":"DISABLED"].filter(Boolean);return`${c+1}. \`${i.id}\`${i.name?` ${i.name}`:""}
198
+ \u2514\u2500 \`${i.urlPattern}\` \xB7 ${l.join(" \xB7 ")}`});return text(`**Override rules on ${r}**${masterNote(s)}
120
199
 
121
- ${i.join("\n")}
200
+ ${a.join("\n")}
122
201
 
123
202
  Full detail incl. body: network_override({action:"list", id:"${o[0]?.id}"}).`)}case"debug":{const s=await n.callAction(NETWORK_TOOL_ID,"debugOverrides",{url:t.url},r);if(!s){return fail2(`${r} did not answer the diagnostics call \u2014 the network tool may not be mounted.`)}return text(`**Override diagnostics on ${r}**${t.url?` for \`${t.url}\``:""}
124
203
 
@@ -126,157 +205,155 @@ Full detail incl. body: network_override({action:"list", id:"${o[0]?.id}"}).`)}c
126
205
  ${JSON.stringify(s,null,2)}
127
206
  \`\`\`
128
207
 
129
- 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(r){return fail2(errMsg2(r))}})}var import_zod6=require("zod");var RQ_TOOL_ID="query";var RQ_TOOL_ID_LEGACY="react-query";async function callQueryAction(e,n,t,r){try{return await e.callAction(RQ_TOOL_ID,n,t,r)}catch(s){const o=errMsg2(s);if(!/unknown tool/i.test(o))throw s;return await e.callAction(RQ_TOOL_ID_LEGACY,n,t,r)}}var STATUS_ICON={fresh:"\u{1F7E2}",stale:"\u{1F7E1}",fetching:"\u{1F504}",error:"\u{1F534}",inactive:"\u26AA\uFE0F",paused:"\u23F8\uFE0F",disabled:"\u{1F6AB}"};function relTime(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 registerReactQueryTools(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:import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),staleOnly:import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),limit:import_zod6.z.number().optional().describe("Most-recently-updated N queries. Default 25."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await callQueryAction(n,"listQueries",{includeData:t.includeData??false,staleOnly:t.staleOnly??false,limit:t.limit??25},r);if(!s||!Array.isArray(s.queries)){return fail2("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(s.queries.length===0){return text(`React Query cache is empty (0 of ${s.total} queries${t.staleOnly?" match staleOnly":""}).`)}const o=s.queries.map((u,l)=>{const d=STATUS_ICON[u.status]??"\u2022";const h=`${l+1}. ${d} ${u.status} \xB7 \`${u.queryHash}\` \xB7 ${u.observers} obs \xB7 ${relTime(u.updatedAt)}`;const y=u.error?` \u2014 \u26A0\uFE0F ${u.error}`:"";const v=s.includedData?`
130
- data: ${JSON.stringify(u.data)}`:"";return h+y+v});const i=s.queries.filter(u=>u.isStale).length;const a=`**React Query cache \u2014 ${s.total} queries (showing ${s.returned}, ${i} stale):**`;const c=s.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 text(`${a}
208
+ 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(r){return fail2(errMsg2(r))}})}var import_zod6=require("zod");var RQ_TOOL_ID="query";var RQ_TOOL_ID_LEGACY="react-query";async function callQueryAction(e,n,t,r){try{return await e.callAction(RQ_TOOL_ID,n,t,r)}catch(s){const o=errMsg2(s);if(!/unknown tool/i.test(o))throw s;return await e.callAction(RQ_TOOL_ID_LEGACY,n,t,r)}}var STATUS_ICON={fresh:"\u{1F7E2}",stale:"\u{1F7E1}",fetching:"\u{1F504}",error:"\u{1F534}",inactive:"\u26AA\uFE0F",paused:"\u23F8\uFE0F",disabled:"\u{1F6AB}"};function relTime(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 registerReactQueryTools(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:import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),staleOnly:import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),limit:import_zod6.z.number().optional().describe("Most-recently-updated N queries. Default 25."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await callQueryAction(n,"listQueries",{includeData:t.includeData??false,staleOnly:t.staleOnly??false,limit:t.limit??25},r);if(!s||!Array.isArray(s.queries)){return fail2("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(s.queries.length===0){return text(`React Query cache is empty (0 of ${s.total} queries${t.staleOnly?" match staleOnly":""}).`)}const o=s.queries.map((u,l)=>{const h=STATUS_ICON[u.status]??"\u2022";const d=`${l+1}. ${h} ${u.status} \xB7 \`${u.queryHash}\` \xB7 ${u.observers} obs \xB7 ${relTime(u.updatedAt)}`;const m=u.error?` \u2014 \u26A0\uFE0F ${u.error}`:"";const w=s.includedData?`
209
+ data: ${JSON.stringify(u.data)}`:"";return d+m+w});const a=s.queries.filter(u=>u.isStale).length;const i=`**React Query cache \u2014 ${s.total} queries (showing ${s.returned}, ${a} stale):**`;const c=s.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 text(`${i}
131
210
 
132
- ${o.join("\n")}${c}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod6.z.enum(["invalidate","refetch","reset","remove","clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),queryHash:import_zod6.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);if(t.action==="clear"){await callQueryAction(n,"clearQueryCache",{},r);return text("Requested clearing of the connected React Query cache. Read it again to confirm.")}if(!t.queryHash){return fail2(`'${t.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`)}await callQueryAction(n,t.action,{queryHash:t.queryHash},r);return text(`Requested ${t.action} on query \`${t.queryHash}\`.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod7=require("zod");var ZUSTAND_TOOL_ID="zustand";function registerZustandTools(e,n){e.registerTool("get_zustand_state",{description:'Read registered Zustand stores, available state keys, change counts and persistence metadata. includeValues:true requests values. Use get_events with sources:["zustand"] for retained changes.',inputSchema:{includeValues:import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),limit:import_zod7.z.number().optional().describe("First N stores. Default all."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const s=await n.callAction(ZUSTAND_TOOL_ID,"listStores",{includeValues:t.includeValues??false,limit:t.limit},r);if(!s||!Array.isArray(s.stores)){return fail2(inactiveToolMessage({toolId:ZUSTAND_TOOL_ID,mcpTool:"get_zustand_state",action:"listStores"}))}if(s.stores.length===0){return text("No Zustand stores are registered (call watchStores to register them).")}const o=s.stores.map((a,c)=>{const u=a.isPersisted?` \xB7 persisted${a.persistName?` (${a.persistName})`:""}`:"";const l=s.includedValues?`
133
- ${JSON.stringify(a.currentState)}`:` \xB7 keys: ${(a.keys??[]).join(", ")||"\u2014"}`;return`${c+1}. **${a.name}** \xB7 ${a.changes} changes${u}${l}`});const i=s.includedValues?"":"\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";return text(`**Zustand stores \u2014 ${s.total} (showing ${s.returned}):**
211
+ ${o.join("\n")}${c}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod6.z.enum(["invalidate","refetch","reset","remove","clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),queryHash:import_zod6.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);if(t.action==="clear"){await callQueryAction(n,"clearQueryCache",{},r);return text("Requested clearing of the connected React Query cache. Read it again to confirm.")}if(!t.queryHash){return fail2(`'${t.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`)}await callQueryAction(n,t.action,{queryHash:t.queryHash},r);return text(`Requested ${t.action} on query \`${t.queryHash}\`.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod7=require("zod");var ZUSTAND_TOOL_ID="zustand";function registerZustandTools(e,n){e.registerTool("get_zustand_state",{description:'Read registered Zustand stores, available state keys, change counts and persistence metadata. includeValues:true requests values. Use get_events with sources:["zustand"] for retained changes.',inputSchema:{includeValues:import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),limit:import_zod7.z.number().optional().describe("First N stores. Default all."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const s=await n.callAction(ZUSTAND_TOOL_ID,"listStores",{includeValues:t.includeValues??false,limit:t.limit},r);if(!s||!Array.isArray(s.stores)){return fail2(inactiveToolMessage({toolId:ZUSTAND_TOOL_ID,mcpTool:"get_zustand_state",action:"listStores"}))}if(s.stores.length===0){return text("No Zustand stores are registered (call watchStores to register them).")}const o=s.stores.map((i,c)=>{const u=i.isPersisted?` \xB7 persisted${i.persistName?` (${i.persistName})`:""}`:"";const l=s.includedValues?`
212
+ ${JSON.stringify(i.currentState)}`:` \xB7 keys: ${(i.keys??[]).join(", ")||"\u2014"}`;return`${c+1}. **${i.name}** \xB7 ${i.changes} changes${u}${l}`});const a=s.includedValues?"":"\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";return text(`**Zustand stores \u2014 ${s.total} (showing ${s.returned}):**
134
213
 
135
- ${o.join("\n")}${i}`)}));e.registerTool("zustand_action",{description:"Set the state of a Zustand store (time-travel / reset / tweak during debugging). Targets a store by name (from get_zustand_state). By default replaces the whole state; pass replace:false to merge a partial.",inputSchema:{store:import_zod7.z.string().describe("Store name to target (from get_zustand_state)."),state:import_zod7.z.record(import_zod7.z.string(),import_zod7.z.unknown()).describe("The new state object."),replace:import_zod7.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{await n.callAction(ZUSTAND_TOOL_ID,"setState",{storeName:t.store,state:t.state,replace:t.replace??true},r);return text(`Requested state update on Zustand store "${t.store}" (${t.replace===false?"merged":"replaced"}).`)}))}var import_zod8=require("zod");var JOTAI_TOOL_ID="jotai";function registerJotaiTools(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:import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),limit:import_zod8.z.number().optional().describe("First N atoms. Default all."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const s=await n.callAction(JOTAI_TOOL_ID,"listAtoms",{includeValues:t.includeValues??false,limit:t.limit},r);if(!s||!Array.isArray(s.atoms)){return fail2(inactiveToolMessage({toolId:JOTAI_TOOL_ID,mcpTool:"get_jotai_state",action:"listAtoms"}))}if(s.atoms.length===0){return text("No Jotai atoms are being watched (call watchAtoms to register them).")}const o=s.atoms.map((a,c)=>{const u=a.writable?"writable":"read-only";const l=s.includedValues?`
136
- ${JSON.stringify(a.currentValue)}`:"";return`${c+1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${u}${l}`});const i=s.includedValues?"":"\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";return text(`**Jotai atoms \u2014 ${s.total} (showing ${s.returned}):**
214
+ ${o.join("\n")}${a}`)}));e.registerTool("zustand_action",{description:"Set the state of a Zustand store (time-travel / reset / tweak during debugging). Targets a store by name (from get_zustand_state). By default replaces the whole state; pass replace:false to merge a partial.",inputSchema:{store:import_zod7.z.string().describe("Store name to target (from get_zustand_state)."),state:import_zod7.z.record(import_zod7.z.string(),import_zod7.z.unknown()).describe("The new state object."),replace:import_zod7.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{await n.callAction(ZUSTAND_TOOL_ID,"setState",{storeName:t.store,state:t.state,replace:t.replace??true},r);return text(`Requested state update on Zustand store "${t.store}" (${t.replace===false?"merged":"replaced"}).`)}))}var import_zod8=require("zod");var JOTAI_TOOL_ID="jotai";function registerJotaiTools(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:import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),limit:import_zod8.z.number().optional().describe("First N atoms. Default all."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const s=await n.callAction(JOTAI_TOOL_ID,"listAtoms",{includeValues:t.includeValues??false,limit:t.limit},r);if(!s||!Array.isArray(s.atoms)){return fail2(inactiveToolMessage({toolId:JOTAI_TOOL_ID,mcpTool:"get_jotai_state",action:"listAtoms"}))}if(s.atoms.length===0){return text("No Jotai atoms are being watched (call watchAtoms to register them).")}const o=s.atoms.map((i,c)=>{const u=i.writable?"writable":"read-only";const l=s.includedValues?`
215
+ ${JSON.stringify(i.currentValue)}`:"";return`${c+1}. **${i.label}** \xB7 ${i.changes} changes \xB7 ${u}${l}`});const a=s.includedValues?"":"\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";return text(`**Jotai atoms \u2014 ${s.total} (showing ${s.returned}):**
137
216
 
138
- ${o.join("\n")}${i}`)}));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:import_zod8.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),value:import_zod8.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{await n.callAction(JOTAI_TOOL_ID,"setAtom",{label:t.atom,value:t.value},r);return text(`Requested update of Jotai atom "${t.atom}" to ${JSON.stringify(t.value)}.`)}))}var import_zod9=require("zod");var REDUX_TOOL_ID="redux";function noStoreMessage(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 TOP_LEVEL_ONLY_NOTE="\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 registerReduxTools(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:import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(REDUX_TOOL_ID,"getState",{includeValues:t.includeValues??false},r);if(!s){return fail2("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(!s.available)return fail2(noStoreMessage(s.reason));const o=s.slices.length?s.slices.join(", "):"(none)";const i=s.capture==="top-level-only"?TOP_LEVEL_ONLY_NOTE:"";if(t.includeValues){return text(`**Redux store \u2014 slices: ${o}**
217
+ ${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:import_zod8.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),value:import_zod8.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{await n.callAction(JOTAI_TOOL_ID,"setAtom",{label:t.atom,value:t.value},r);return text(`Requested update of Jotai atom "${t.atom}" to ${JSON.stringify(t.value)}.`)}))}var import_zod9=require("zod");var REDUX_TOOL_ID="redux";function noStoreMessage(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 TOP_LEVEL_ONLY_NOTE="\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 registerReduxTools(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:import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(REDUX_TOOL_ID,"getState",{includeValues:t.includeValues??false},r);if(!s){return fail2("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(!s.available)return fail2(noStoreMessage(s.reason));const o=s.slices.length?s.slices.join(", "):"(none)";const a=s.capture==="top-level-only"?TOP_LEVEL_ONLY_NOTE:"";if(t.includeValues){return text(`**Redux store \u2014 slices: ${o}**
139
218
 
140
219
  \`\`\`json
141
220
  ${JSON.stringify(s.state,null,2)}
142
- \`\`\`${i}`)}return text(`**Redux store \u2014 slices: ${o}**
221
+ \`\`\`${a}`)}return text(`**Redux store \u2014 slices: ${o}**
143
222
 
144
- _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${i}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod9.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),payload:import_zod9.z.unknown().optional().describe("Optional action payload (any JSON)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s={type:t.type};if(t.payload!==void 0)s.payload=t.payload;const o=await n.callAction(REDUX_TOOL_ID,"dispatch",{action:s},r);if(!o||!o.dispatched)return fail2(noStoreMessage(o?.reason));return text(`Dispatched \`${t.type}\`${t.payload!==void 0?` with payload ${JSON.stringify(t.payload)}`:""}.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod10=require("zod");var STORAGE_TOOL_ID="storage";function truncate2(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 registerStorageTools(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:import_zod10.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),includeValues:import_zod10.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),limit:import_zod10.z.number().optional().describe("Max keys per backend section. Default 100."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=t.pattern?.toLowerCase();const o=typeof t.limit==="number"&&t.limit>0?t.limit:100;const i=v=>!s||v.toLowerCase().includes(s);const a=[];const c=await n.callAction(STORAGE_TOOL_ID,"async.getAllKeys",{},r);if(!Array.isArray(c)){return fail2("No storage result from the device. The storage tool may be inactive.")}const u=c.filter(i).slice(0,o);if(u.length>0){let v;if(t.includeValues){const m=await n.callAction(STORAGE_TOOL_ID,"async.multiGet",{keys:u},r);const g=new Map((m??[]).map(([p,f])=>[p,f]));v=u.map(p=>`- \`${p}\` = ${truncate2(g.get(p)??null)}`)}else{v=u.map(m=>`- \`${m}\``)}a.push(`**AsyncStorage (${u.length}/${c.length} keys):**
145
- ${v.join("\n")}`)}const l=await n.callAction(STORAGE_TOOL_ID,"mmkv.snapshot",{},r).catch(()=>null);for(const v of l??[]){const m=v.entries.filter(f=>i(f.key)).slice(0,o);if(m.length===0)continue;const g=m.map(f=>t.includeValues?`- \`${f.key}\` (${f.valueType}) = ${truncate2(f.value)}`:`- \`${f.key}\``);const p=[v.encrypted&&"encrypted",v.readOnly&&"read-only"].filter(Boolean).join(", ");a.push(`**MMKV \`${v.id}\`${p?` (${p})`:""} (${m.length}/${v.entries.length} keys):**
146
- ${g.join("\n")}`)}const d=await n.callAction(STORAGE_TOOL_ID,"secure.keys",{},r).catch(()=>null);const h=(d??[]).filter(v=>i(v.key)).slice(0,o);if(h.length>0){const v=await Promise.all(h.map(async m=>{if(m.requireAuthentication)return`- \`${m.key}\` \u{1F512} (biometric-protected \u2014 not read)`;if(!t.includeValues)return`- \`${m.key}\``;const g=await n.callAction(STORAGE_TOOL_ID,"secure.get",{key:m.key},r).catch(()=>null);return`- \`${m.key}\` = ${truncate2(g)}`}));a.push(`**SecureStore (${h.length} registered keys):**
147
- ${v.join("\n")}`)}if(a.length===0){return text(`No storage keys${s?` matching "${t.pattern}"`:""} in any backend.`)}const y=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return text(`${a.join("\n\n")}${y}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod10.z.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:import_zod10.z.string().optional().describe("The key (required for set and remove)."),value:import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(STORAGE_TOOL_ID,"clearAppStorage",{},r);return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return fail2(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(STORAGE_TOOL_ID,"async.removeItem",{key:t.key},r);return text(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return fail2("'set' needs a value (a string).")}await n.callAction(STORAGE_TOOL_ID,"async.setItem",{key:t.key,value:t.value},r);return text(`Requested update of storage key "${t.key}".`)}catch(r){return fail2(errMsg2(r))}})}var import_zod11=require("zod");var ROUTES_TOOL_ID="route-events";function flattenRoutes(e,n=[]){for(const t of e??[]){if(t.path)n.push(t.path);if(t.children?.length)flattenRoutes(t.children,n)}return n}function registerRoutesTools(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:import_zod11.z.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(ROUTES_TOOL_ID,r);const o=Array.isArray(s)?{events:s}:s??{};const i=Array.isArray(o.stack)?o.stack:[];const a=i.find(d=>d.isFocused)??i[i.length-1];const c=a?.pathname??"(unknown)";const u=[`**Current route:** \`${c}\``];if(i.length){const d=i.map((h,y)=>`${y+1}. \`${h.pathname??h.name??"?"}\`${h.isFocused?" \u2190 focused":""}`);u.push(`
223
+ _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${a}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod9.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),payload:import_zod9.z.unknown().optional().describe("Optional action payload (any JSON)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s={type:t.type};if(t.payload!==void 0)s.payload=t.payload;const o=await n.callAction(REDUX_TOOL_ID,"dispatch",{action:s},r);if(!o||!o.dispatched)return fail2(noStoreMessage(o?.reason));return text(`Dispatched \`${t.type}\`${t.payload!==void 0?` with payload ${JSON.stringify(t.payload)}`:""}.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod10=require("zod");var STORAGE_TOOL_ID="storage";function truncate2(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 registerStorageTools(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:import_zod10.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),includeValues:import_zod10.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),limit:import_zod10.z.number().optional().describe("Max keys per backend section. Default 100."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=t.pattern?.toLowerCase();const o=typeof t.limit==="number"&&t.limit>0?t.limit:100;const a=w=>!s||w.toLowerCase().includes(s);const i=[];const c=await n.callAction(STORAGE_TOOL_ID,"async.getAllKeys",{},r);if(!Array.isArray(c)){return fail2("No storage result from the device. The storage tool may be inactive.")}const u=c.filter(a).slice(0,o);if(u.length>0){let w;if(t.includeValues){const g=await n.callAction(STORAGE_TOOL_ID,"async.multiGet",{keys:u},r);const b=new Map((g??[]).map(([p,f])=>[p,f]));w=u.map(p=>`- \`${p}\` = ${truncate2(b.get(p)??null)}`)}else{w=u.map(g=>`- \`${g}\``)}i.push(`**AsyncStorage (${u.length}/${c.length} keys):**
224
+ ${w.join("\n")}`)}const l=await n.callAction(STORAGE_TOOL_ID,"mmkv.snapshot",{},r).catch(()=>null);for(const w of l??[]){const g=w.entries.filter(f=>a(f.key)).slice(0,o);if(g.length===0)continue;const b=g.map(f=>t.includeValues?`- \`${f.key}\` (${f.valueType}) = ${truncate2(f.value)}`:`- \`${f.key}\``);const p=[w.encrypted&&"encrypted",w.readOnly&&"read-only"].filter(Boolean).join(", ");i.push(`**MMKV \`${w.id}\`${p?` (${p})`:""} (${g.length}/${w.entries.length} keys):**
225
+ ${b.join("\n")}`)}const h=await n.callAction(STORAGE_TOOL_ID,"secure.keys",{},r).catch(()=>null);const d=(h??[]).filter(w=>a(w.key)).slice(0,o);if(d.length>0){const w=await Promise.all(d.map(async g=>{if(g.requireAuthentication)return`- \`${g.key}\` \u{1F512} (biometric-protected \u2014 not read)`;if(!t.includeValues)return`- \`${g.key}\``;const b=await n.callAction(STORAGE_TOOL_ID,"secure.get",{key:g.key},r).catch(()=>null);return`- \`${g.key}\` = ${truncate2(b)}`}));i.push(`**SecureStore (${d.length} registered keys):**
226
+ ${w.join("\n")}`)}if(i.length===0){return text(`No storage keys${s?` matching "${t.pattern}"`:""} in any backend.`)}const m=t.includeValues?"":"\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";return text(`${i.join("\n\n")}${m}`)}catch(r){return fail2(errMsg2(r))}});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:import_zod10.z.enum(["set","remove","clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),key:import_zod10.z.string().optional().describe("The key (required for set and remove)."),value:import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);if(t.action==="clear"){await n.callAction(STORAGE_TOOL_ID,"clearAppStorage",{},r);return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.")}if(!t.key){return fail2(`'${t.action}' needs a key.`)}if(t.action==="remove"){await n.callAction(STORAGE_TOOL_ID,"async.removeItem",{key:t.key},r);return text(`Requested removal of storage key "${t.key}".`)}if(t.value===void 0){return fail2("'set' needs a value (a string).")}await n.callAction(STORAGE_TOOL_ID,"async.setItem",{key:t.key,value:t.value},r);return text(`Requested update of storage key "${t.key}".`)}catch(r){return fail2(errMsg2(r))}})}var import_zod11=require("zod");var ROUTES_TOOL_ID="route-events";function flattenRoutes(e,n=[]){for(const t of e??[]){if(t.path)n.push(t.path);if(t.children?.length)flattenRoutes(t.children,n)}return n}function registerRoutesTools(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:import_zod11.z.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(ROUTES_TOOL_ID,r);const o=Array.isArray(s)?{events:s}:s??{};const a=Array.isArray(o.stack)?o.stack:[];const i=a.find(h=>h.isFocused)??a[a.length-1];const c=i?.pathname??"(unknown)";const u=[`**Current route:** \`${c}\``];if(a.length){const h=a.map((d,m)=>`${m+1}. \`${d.pathname??d.name??"?"}\`${d.isFocused?" \u2190 focused":""}`);u.push(`
148
227
  **Navigation stack** (top last):
149
- ${d.join("\n")}`)}const l=flattenRoutes(o.sitemap?.routes);if(t.includeSitemap){const d=l.slice(0,200);u.push(`
150
- **Sitemap (${l.length} routes${d.length<l.length?`, showing ${d.length}`:""}):**
151
- ${d.map(h=>`- \`${h}\``).join("\n")}`)}else{u.push(`
152
- **Sitemap:** ${l.length} routes _(pass includeSitemap:true to list them)_`)}u.push("\n_Navigate with navigate({ path })._");return text(u.join("\n"))}catch(r){return fail2(errMsg2(r))}});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:import_zod11.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),replace:import_zod11.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(ROUTES_TOOL_ID,"navigate",{path:t.path,replace:t.replace===true},r);return text(`Navigated to \`${t.path}\`${t.replace?" (replaced current screen)":""}.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod12=require("zod");var CONSOLE_TOOL_ID="console";var SEVERITY={verbose:0,info:1,warning:2,error:3};var TAG={verbose:"\xB7",info:"info",warning:"warn",error:"ERR"};function hhmmss(e){const n=new Date(e);const t=(r,s=2)=>String(r).padStart(s,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}.${t(n.getMilliseconds(),3)}`}function registerConsoleTools(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. Requires configured console capture and Buoy Pro.",inputSchema:{level:import_zod12.z.enum(["verbose","info","warning","error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),limit:import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),pattern:import_zod12.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const{payload:s,stale:o}=await n.getSnapshot(CONSOLE_TOOL_ID,r);const i=staleHeader(o);const a=Array.isArray(s)?{entries:s}:s??{};const c=Array.isArray(a.entries)?a.entries:[];const u=t.level?SEVERITY[t.level]:0;const l=t.pattern?.toLowerCase();const d=c.filter(f=>SEVERITY[f.level]>=u&&(!l||(f.message??"").toLowerCase().includes(l)));const h=[...d].sort((f,w)=>f.id-w.id);const y=clampLimit(t.limit,50);const v=h.slice(-y);if(v.length===0){return text(`${i}No console entries${t.level?` at level \u2265 ${t.level}`:""}${l?` matching "${t.pattern}"`:""} (${c.length} total captured).`)}const m=v.map(f=>`${hhmmss(f.timestamp)} [${TAG[f.level]??f.level}] ${f.message}`);const g=t.level?`, level \u2265 ${t.level}`:"";const p=`**Console \u2014 ${c.length} entries (showing ${v.length}${g}):**`;return text(`${i}${p}
228
+ ${h.join("\n")}`)}const l=flattenRoutes(o.sitemap?.routes);if(t.includeSitemap){const h=l.slice(0,200);u.push(`
229
+ **Sitemap (${l.length} routes${h.length<l.length?`, showing ${h.length}`:""}):**
230
+ ${h.map(d=>`- \`${d}\``).join("\n")}`)}else{u.push(`
231
+ **Sitemap:** ${l.length} routes _(pass includeSitemap:true to list them)_`)}u.push("\n_Navigate with navigate({ path })._");return text(u.join("\n"))}catch(r){return fail2(errMsg2(r))}});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:import_zod11.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),replace:import_zod11.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(ROUTES_TOOL_ID,"navigate",{path:t.path,replace:t.replace===true},r);return text(`Navigated to \`${t.path}\`${t.replace?" (replaced current screen)":""}.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod12=require("zod");var CONSOLE_TOOL_ID="console";var SEVERITY={verbose:0,info:1,warning:2,error:3};var TAG={verbose:"\xB7",info:"info",warning:"warn",error:"ERR"};function hhmmss(e){const n=new Date(e);const t=(r,s=2)=>String(r).padStart(s,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}.${t(n.getMilliseconds(),3)}`}function registerConsoleTools(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. Requires configured console capture and Buoy Pro.",inputSchema:{level:import_zod12.z.enum(["verbose","info","warning","error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),limit:import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),pattern:import_zod12.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),deviceId:deviceIdInput}},withDevice(n,async(t,r)=>{const{payload:s,stale:o}=await n.getSnapshot(CONSOLE_TOOL_ID,r);const a=staleHeader(o);const i=Array.isArray(s)?{entries:s}:s??{};const c=Array.isArray(i.entries)?i.entries:[];const u=t.level?SEVERITY[t.level]:0;const l=t.pattern?.toLowerCase();const h=c.filter(f=>SEVERITY[f.level]>=u&&(!l||(f.message??"").toLowerCase().includes(l)));const d=[...h].sort((f,v)=>f.id-v.id);const m=clampLimit(t.limit,50);const w=d.slice(-m);if(w.length===0){return text(`${a}No console entries${t.level?` at level \u2265 ${t.level}`:""}${l?` matching "${t.pattern}"`:""} (${c.length} total captured).`)}const g=w.map(f=>`${hhmmss(f.timestamp)} [${TAG[f.level]??f.level}] ${f.message}`);const b=t.level?`, level \u2265 ${t.level}`:"";const p=`**Console \u2014 ${c.length} entries (showing ${w.length}${b}):**`;return text(`${a}${p}
153
232
 
154
233
  \`\`\`
155
- ${m.join("\n")}
234
+ ${g.join("\n")}
156
235
  \`\`\`
157
236
 
158
- _Tail of recent logs. Use get_snapshot for available console entry details._`)}))}var import_zod13=require("zod");var SENTRY_TOOL_ID="sentry";var text2=e=>({content:[{type:"text",text:e}]});var fail3=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg3=e=>e instanceof Error?e.message:String(e);function hhmmss2(e){const n=new Date(e);const t=r=>String(r).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}function bytes(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 units(e,n){if(n==="bytes")return bytes(e);if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;if(e>=1e3)return`${(e/1e3).toFixed(1)}k`;return String(e)}var FREE_QUOTA={errors:5e3,spans:5e6,replays:50,logBytes:5*1024**3,attachmentBytes:1*1024**3};var CATEGORY_FORMAT={errors:"count",spans:"count",replays:"count",logBytes:"bytes",attachmentBytes:"bytes"};function statusNote(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 registerSentryTools(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:import_zod13.z.string().optional().describe("Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."),limit:import_zod13.z.number().optional().describe("Most-recent N envelopes. Default 25."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const i=o?`${o}
237
+ _Tail of recent logs. Use get_snapshot for available console entry details._`)}))}var import_zod13=require("zod");var SENTRY_TOOL_ID="sentry";var text2=e=>({content:[{type:"text",text:e}]});var fail3=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg3=e=>e instanceof Error?e.message:String(e);function hhmmss2(e){const n=new Date(e);const t=r=>String(r).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}function bytes(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 units(e,n){if(n==="bytes")return bytes(e);if(e>=1e6)return`${(e/1e6).toFixed(1)}M`;if(e>=1e3)return`${(e/1e3).toFixed(1)}k`;return String(e)}var FREE_QUOTA={errors:5e3,spans:5e6,replays:50,logBytes:5*1024**3,attachmentBytes:1*1024**3};var CATEGORY_FORMAT={errors:"count",spans:"count",replays:"count",logBytes:"bytes",attachmentBytes:"bytes"};function statusNote(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 registerSentryTools(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:import_zod13.z.string().optional().describe("Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."),limit:import_zod13.z.number().optional().describe("Most-recent N envelopes. Default 25."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const a=o?`${o}
159
238
 
160
- `:"";const a=s??{};const c=Array.isArray(a.envelopes)?a.envelopes:[];const u=t.type?c.filter(v=>v.items.some(m=>m.type===t.type)):c;const l=typeof t.limit==="number"&&t.limit>0?t.limit:25;const d=u.slice(-l);if(d.length===0){return text2(`${i}No Sentry envelopes${t.type?` containing a '${t.type}' item`:""} (${c.length} captured this session).${statusNote(a.status)}`)}const h=d.map(v=>{const m=v.items.map(w=>w.type).join("+");const g=v.items.reduce((w,b)=>w+(b.spanCount??0),0);const p=g>0?` \xB7 ${g} span${g===1?"":"s"}`:"";const f=`${v.id}. ${hhmmss2(v.timestamp)} [${m}] ${bytes(v.totalBytes)}${p}`;return`${f}
161
- \u2514\u2500 ${v.items[0]?.summary??"envelope"}`});const y=`**Sentry \u2014 ${c.length} envelope${c.length===1?"":"s"} sent (showing ${d.length}${t.type?`, type '${t.type}'`:""}):**`;return text2(`${i}${y}${statusNote(a.status)}
239
+ `:"";const i=s??{};const c=Array.isArray(i.envelopes)?i.envelopes:[];const u=t.type?c.filter(w=>w.items.some(g=>g.type===t.type)):c;const l=typeof t.limit==="number"&&t.limit>0?t.limit:25;const h=u.slice(-l);if(h.length===0){return text2(`${a}No Sentry envelopes${t.type?` containing a '${t.type}' item`:""} (${c.length} captured this session).${statusNote(i.status)}`)}const d=h.map(w=>{const g=w.items.map(v=>v.type).join("+");const b=w.items.reduce((v,y)=>v+(y.spanCount??0),0);const p=b>0?` \xB7 ${b} span${b===1?"":"s"}`:"";const f=`${w.id}. ${hhmmss2(w.timestamp)} [${g}] ${bytes(w.totalBytes)}${p}`;return`${f}
240
+ \u2514\u2500 ${w.items[0]?.summary??"envelope"}`});const m=`**Sentry \u2014 ${c.length} envelope${c.length===1?"":"s"} sent (showing ${h.length}${t.type?`, type '${t.type}'`:""}):**`;return text2(`${a}${m}${statusNote(i.status)}
162
241
 
163
- ${h.join("\n")}
242
+ ${d.join("\n")}
164
243
 
165
- _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`)}catch(r){return fail3(errMsg3(r))}});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:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const i=o?`${o}
244
+ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`)}catch(r){return fail3(errMsg3(r))}});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:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const a=o?`${o}
166
245
 
167
- `:"";const a=s??{};const c=a.usage;if(!c?.session){return text2(`${i}No usage is available in this capture.${statusNote(a.status)}`)}const u=c.session;const l=c.projectedMonthly;const d=Object.keys(u);const h=d.map(g=>{const p=CATEGORY_FORMAT[g];const f=units(u[g],p);if(!l)return`- ${g}: ${f} this session`;const w=l[g];const b=FREE_QUOTA[g];const _=b>0?Math.round(w/b*100):null;const k=_===null?" (no configured reference quota)":` \u2014 ${_}% of the configured reference quota of ${units(b,p)}`;return`- ${g}: ${f} this session \u2192 ~${units(w,p)}/mo${k}`});const y=c.windowMs?` over ${Math.round(c.windowMs/1e3)}s`:"";const v=c.topContributors?.length?`
246
+ `:"";const i=s??{};const c=i.usage;if(!c?.session){return text2(`${a}No usage is available in this capture.${statusNote(i.status)}`)}const u=c.session;const l=c.projectedMonthly;const h=Object.keys(u);const d=h.map(b=>{const p=CATEGORY_FORMAT[b];const f=units(u[b],p);if(!l)return`- ${b}: ${f} this session`;const v=l[b];const y=FREE_QUOTA[b];const k=y>0?Math.round(v/y*100):null;const S=k===null?" (no configured reference quota)":` \u2014 ${k}% of the configured reference quota of ${units(y,p)}`;return`- ${b}: ${f} this session \u2192 ~${units(v,p)}/mo${S}`});const m=c.windowMs?` over ${Math.round(c.windowMs/1e3)}s`:"";const w=c.topContributors?.length?`
168
247
 
169
248
  **What produced it:**
170
- ${c.topContributors.map(g=>`- ${g.label} \u2014 ${g.units} ${g.category}`).join("\n")}`:"";const m=l?"\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 text2(`${i}**Sentry usage estimate \u2014 ${a.envelopes?.length??0} envelopes${y}:**
249
+ ${c.topContributors.map(b=>`- ${b.label} \u2014 ${b.units} ${b.category}`).join("\n")}`:"";const g=l?"\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 text2(`${a}**Sentry usage estimate \u2014 ${i.envelopes?.length??0} envelopes${m}:**
171
250
 
172
- ${h.join("\n")}${v}${m}`)}catch(r){return fail3(errMsg3(r))}});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:import_zod13.z.number().optional().describe("Most-recent N drop records. Default 30."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const i=o?`${o}
251
+ ${d.join("\n")}${w}${g}`)}catch(r){return fail3(errMsg3(r))}});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:import_zod13.z.number().optional().describe("Most-recent N drop records. Default 30."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s,stale:o}=await n.getSnapshot(SENTRY_TOOL_ID,r);const a=o?`${o}
173
252
 
174
- `:"";const a=s??{};const c=a.drops?.summary??[];const u=a.drops?.recent??[];if(c.length===0){return text2(`${i}No drops are reported in the available capture.${statusNote(a.status)}
253
+ `:"";const i=s??{};const c=i.drops?.summary??[];const u=i.drops?.recent??[];if(c.length===0){return text2(`${a}No drops are reported in the available capture.${statusNote(i.status)}
175
254
 
176
- _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 l=typeof t.limit==="number"&&t.limit>0?t.limit:30;const d=u.slice(-l);const h=c.map(v=>`- ${v.reason}: ${v.units} unit${v.units===1?"":"s"} across ${v.records} record${v.records===1?"":"s"}`);const y=d.map(v=>`${hhmmss2(v.timestamp)} [${v.reason}] ${v.category}${v.quantity>1?` \xD7${v.quantity}`:""} \u2014 ${v.detail} (${v.source})`);return text2(`${i}**Sentry drops \u2014 by reason:**
255
+ _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 l=typeof t.limit==="number"&&t.limit>0?t.limit:30;const h=u.slice(-l);const d=c.map(w=>`- ${w.reason}: ${w.units} unit${w.units===1?"":"s"} across ${w.records} record${w.records===1?"":"s"}`);const m=h.map(w=>`${hhmmss2(w.timestamp)} [${w.reason}] ${w.category}${w.quantity>1?` \xD7${w.quantity}`:""} \u2014 ${w.detail} (${w.source})`);return text2(`${a}**Sentry drops \u2014 by reason:**
177
256
 
178
- ${h.join("\n")}
257
+ ${d.join("\n")}
179
258
 
180
- **Recent (${d.length}):**
259
+ **Recent (${h.length}):**
181
260
 
182
261
  \`\`\`
183
- ${y.join("\n")}
262
+ ${m.join("\n")}
184
263
  \`\`\`
185
264
 
186
- _"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(r){return fail3(errMsg3(r))}});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:import_zod13.z.enum(["clear"]).describe("The action to perform."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(SENTRY_TOOL_ID,"clearEnvelopes",{},r);return text2("Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost.")}catch(r){return fail3(errMsg3(r))}})}var import_zod14=require("zod");var TIME_MACHINE_TOOL_ID="time-machine";function kb(e){return e<1024?`${e} B`:`${(e/1024).toFixed(1)} KB`}function formatOutcome(e){const n=Object.entries(e.results).map(([r,s])=>{const o=s.skipped.slice(0,3).map(a=>`
187
- \xB7 ${a.id}: ${a.reason}`).join("");const i=s.warnings.slice(0,3).map(a=>`
188
- \u26A0 ${a}`).join("");return` ${s.ok?"\u2713":"\u2715"} ${r} \u2014 applied ${s.applied}${s.skipped.length?`, skipped ${s.skipped.length}`:""}${o}${i}`});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":""}):
189
- ${n.join("\n")}`}function registerTimeMachineTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(TIME_MACHINE_TOOL_ID,r);const o=s??{};const i=o.snapshots??[];const a=o.providers??[];const c=a.map(d=>{const h=d.canCapture?d.canRestore?"capture+restore":"capture only":"unavailable";return`- ${d.label} (${d.id}): ${h}${d.detail?` \u2014 ${d.detail}`:""}`});if(o.route){c.push(o.route.available?`- Route (route): capture+restore \u2014 recorded with every snapshot; pass restoreRoute:true to return to it${o.route.current?` (now ${o.route.current})`:""}`:"- Route (route): unavailable \u2014 no Expo Router on this device, snapshots record no route")}const u=i.map(d=>{const h=Object.values(d.sources).reduce((m,g)=>m+g.warnings.length,0);const y=d.baseline?"baseline: clear supported providers and request reload":`${kb(d.sizeBytes)} \xB7 ${Object.keys(d.sources).join(", ")}`;const v=d.route?` \xB7 route ${d.route.href}`:"";return`- \`${d.id}\` **${d.name}** \u2014 ${new Date(d.createdAt).toISOString()} \xB7 ${y}${v}${h?` \xB7 \u26A0 ${h}`:""}`});const l=[`**Time Machine \u2014 ${i.length} snapshot(s):**`,u.length?u.join("\n"):"_none yet \u2014 capture one with time_machine_action_",`**Sources:**
265
+ _"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(r){return fail3(errMsg3(r))}});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:import_zod13.z.enum(["clear"]).describe("The action to perform."),deviceId:import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(SENTRY_TOOL_ID,"clearEnvelopes",{},r);return text2("Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost.")}catch(r){return fail3(errMsg3(r))}})}var import_zod14=require("zod");var TIME_MACHINE_TOOL_ID="time-machine";function kb(e){return e<1024?`${e} B`:`${(e/1024).toFixed(1)} KB`}function formatOutcome(e){const n=Object.entries(e.results).map(([r,s])=>{const o=s.skipped.slice(0,3).map(i=>`
266
+ \xB7 ${i.id}: ${i.reason}`).join("");const a=s.warnings.slice(0,3).map(i=>`
267
+ \u26A0 ${i}`).join("");return` ${s.ok?"\u2713":"\u2715"} ${r} \u2014 applied ${s.applied}${s.skipped.length?`, skipped ${s.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":""}):
268
+ ${n.join("\n")}`}function registerTimeMachineTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(TIME_MACHINE_TOOL_ID,r);const o=s??{};const a=o.snapshots??[];const i=o.providers??[];const c=i.map(h=>{const d=h.canCapture?h.canRestore?"capture+restore":"capture only":"unavailable";return`- ${h.label} (${h.id}): ${d}${h.detail?` \u2014 ${h.detail}`:""}`});if(o.route){c.push(o.route.available?`- Route (route): capture+restore \u2014 recorded with every snapshot; pass restoreRoute:true to return to it${o.route.current?` (now ${o.route.current})`:""}`:"- Route (route): unavailable \u2014 no Expo Router on this device, snapshots record no route")}const u=a.map(h=>{const d=Object.values(h.sources).reduce((g,b)=>g+b.warnings.length,0);const m=h.baseline?"baseline: clear supported providers and request reload":`${kb(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 ${m}${w}${d?` \xB7 \u26A0 ${d}`:""}`});const l=[`**Time Machine \u2014 ${a.length} snapshot(s):**`,u.length?u.join("\n"):"_none yet \u2014 capture one with time_machine_action_",`**Sources:**
190
269
  ${c.join("\n")||"_no providers registered_"}`];if(o.lastOutcome)l.push(`**Last restore:**
191
- ${formatOutcome(o.lastOutcome)}`);return text(l.join("\n\n"))}catch(r){return fail2(errMsg2(r))}});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:import_zod14.z.enum(["capture","capture_baseline","preview","restore","delete","rename"]).describe("What to do."),id:import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),name:import_zod14.z.string().optional().describe("Snapshot name (for capture and rename)."),mode:import_zod14.z.enum(["live","reload"]).optional().describe("Restore mode. Default 'live'."),excludeKeys:import_zod14.z.array(import_zod14.z.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:import_zod14.z.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:import_zod14.z.array(import_zod14.z.string()).optional().describe("Capture/restore only these source ids (default all)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(o,i)=>n.callAction(TIME_MACHINE_TOOL_ID,o,i,r);switch(t.action){case"capture":{const o=await s("capture",{name:t.name??"",sourceIds:t.sourceIds});const i=o.snapshot;const a=Object.entries(i.sources).flatMap(([c,u])=>u.warnings.map(l=>`
192
- \u26A0 ${c}: ${l}`)).join("");return text(`Captured \`${i.id}\` "${i.name}" \u2014 ${kb(i.sizeBytes)} \xB7 ${Object.keys(i.sources).join(", ")}${a}`)}case"capture_baseline":{const o=await s("captureBaseline",{});return text(`Captured baseline \`${o.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`)}case"preview":{if(!t.id)return fail2("preview requires `id` (from get_time_machine).");const o=await s("preview",{id:t.id});const i=[];let a=0;for(const u of o.sources){const l={};for(const y of u.items){l[y.verdict]=(l[y.verdict]??0)+1}const d=(l.added??0)+(l.removed??0)+(l.changed??0)+(l["too-large"]??0);a+=d;const h=Object.entries(l).filter(([y])=>y!=="unchanged").map(([y,v])=>`${v} ${y}`);i.push(`- **${u.label}**: ${h.length?h.join(", "):"no changes"}${u.error?` \u2014 \u26A0 ${u.error}`:""}`);for(const y of u.items.filter(v=>v.verdict!=="unchanged").slice(0,8)){i.push(` ${y.verdict==="added"?"+":y.verdict==="removed"?"\u2212":y.verdict==="wont-apply"?"\u2298":"\u2248"} ${y.label}${y.reason?` (${y.reason})`:""}`)}for(const y of u.notes)i.push(` \xB7 ${y}`)}const c=a===0?"**Restore would change NOTHING** (states identical or nothing applicable).":`**Restore would apply ${a} change(s):**`;return text(`${c}
193
- ${i.join("\n")}`)}case"restore":{if(!t.id)return fail2("restore requires `id` (from get_time_machine).");const o=await s("restore",{id:t.id,mode:t.mode??"live",excludeKeys:t.excludeKeys,restoreRoute:t.restoreRoute});return text(formatOutcome(o))}case"delete":{if(!t.id)return fail2("delete requires `id`.");await s("delete",{id:t.id});return text(`Deleted snapshot \`${t.id}\`.`)}case"rename":{if(!t.id||!t.name)return fail2("rename requires `id` and `name`.");await s("rename",{id:t.id,name:t.name});return text(`Renamed \`${t.id}\` to "${t.name}".`)}}}catch(r){return fail2(errMsg2(r))}})}var import_zod15=require("zod");var CLOCK_TOOL_ID="clock";function duration(e){const n=e<0?"-":"+";let t=Math.abs(Math.round(e/1e3));if(t===0)return"0s";const r=[];for(const[s,o]of[["d",86400],["h",3600],["m",60],["s",1]]){if(t>=o){r.push(`${Math.floor(t/o)}${s}`);t%=o}if(r.length===2)break}return n+r.join(" ")}function describe(e){const n=[e.active?`**App clock: ${e.virtualIso}** (${e.mode}${e.rate!==1?`, ${e.rate}\xD7`:""}), ${duration(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: ${duration(e.lastJump.byMs)}${e.lastJump.firedTimers?`, ran ${e.lastJump.firedTimers} due timer(s)`:""}`)}n.push(...describeTokens(e));n.push(`Settings: persist=${e.settings.persist} showChip=${e.settings.showChip} fireTimersOnJump=${e.settings.fireTimersOnJump}`);return n.join("\n")}function describeTokens(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 s of n.tokens){const o=s.expiresAt!==void 0?s.expiresAt-e.virtualNow:void 0;const i=s.serverExpiresAt!==void 0?s.serverExpiresAt-e.realNow:void 0;const a=s.neverExpires?"never expires":o===void 0?"expiry unknown":o>0?`expires in ${duration(o).slice(1)} on the app's clock`:`expired ${duration(-o).slice(1)} ago on the app's clock`;const c=i!==void 0&&o!==void 0&&Math.abs(i-o)>2e3?i>0?`; the server accepts it for ${duration(i).slice(1)} more`:"; the server no longer accepts it":"";const u=[s.hint??s.formatLabel,s.expirySource?`expiry from ${s.expirySource}`:null,s.issuedBy?`issued by ${s.issuedBy}`:null,s.claims.sub?`sub ${s.claims.sub}`:null,`${s.uses} request(s)`,s.refreshes?`${s.refreshes} refresh(es)`:null,s.expiredSends?`SENT ${s.expiredSends}\xD7 AFTER EXPIRY`:null,s.rejected?`rejected ${s.rejected}\xD7`:null,s.looping?`REFRESH LOOP (${s.recentRefreshes} new tokens in 20s)`:null].filter(Boolean);t.push(`- [${s.id}] ${s.label} \u2192 ${s.host}: ${a}${c} (${u.join(", ")})`)}const r=n.check;if(r){const s=r.status==="refreshed"?`refreshed${r.kind==="401"&&r.forcedAt?" after the forced 401":""}${r.newIssuedBy?` via ${r.newIssuedBy}`:""}${r.newLifetimeMs?`; new token lasts ${duration(r.newLifetimeMs).slice(1)}`:""}`:r.status==="loop"?`REFRESH LOOP: ${r.newTokens} new tokens since the check started`:r.kind==="401"&&r.forcedAt===void 0?"waiting: the next request to that API will get a 401 (the app must send one)":`waiting for a new token (${duration(e.realNow-r.startedAt).slice(1)} so far)`;const o=r.staleAfterFailure?`; the app re-sent the old token ${r.staleAfterFailure}\xD7 after the 401`:"";t.push(`Refresh check (${r.kind==="401"?"forced 401":"jump to expiry"}): ${s}${o}`)}return t}function registerClockTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(CLOCK_TOOL_ID,"getState",{},r);return text(describe(s))}catch(r){return fail2(errMsg2(r))}});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:import_zod15.z.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:import_zod15.z.union([import_zod15.z.string(),import_zod15.z.number()]).optional().describe("set_time (required) / freeze (optional): target time."),by:import_zod15.z.union([import_zod15.z.string(),import_zod15.z.number()]).optional().describe("shift (required): signed duration like '+1d', '-2h30m', '90s', or ms."),freeze:import_zod15.z.boolean().optional().describe("set_time: stop the clock at `time`. Default false."),fireTimers:import_zod15.z.boolean().optional().describe("shift: run app timers that come due inside a forward jump. Default on."),rate:import_zod15.z.number().optional().describe("set_rate (required): speed multiplier, 0.1-86400; 1 is normal."),persist:import_zod15.z.boolean().optional().describe("settings: keep the override across reloads."),showChip:import_zod15.z.boolean().optional().describe("settings: show the floating clock chip while overridden."),fireTimersOnJump:import_zod15.z.boolean().optional().describe("settings: default for shift's fireTimers."),token:import_zod15.z.string().optional().describe("jump_to_token_expiry / fail_next_request: token id from get_clock. Default: the most recently used token."),leadMs:import_zod15.z.number().optional().describe("jump_to_token_expiry: land this many ms before expiry. Default 10000."),status:import_zod15.z.number().optional().describe("fail_next_request: 401 (default) or 403."),times:import_zod15.z.number().optional().describe("fail_next_request: how many requests to fail, 1-10. Default 1."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(i,a={})=>n.callAction(CLOCK_TOOL_ID,i,a,r);let o;switch(t.action){case"set_time":if(t.time===void 0)return fail2("set_time requires `time`.");o=await s("setTime",{time:t.time,freeze:t.freeze??false});break;case"shift":if(t.by===void 0)return fail2("shift requires `by`, e.g. '+1d'.");o=await s("shift",{by:t.by,...t.fireTimers!==void 0?{fireTimers:t.fireTimers}:{}});break;case"freeze":o=await s("freeze",t.time!==void 0?{time:t.time}:{});break;case"resume":o=await s("resume");break;case"set_rate":if(t.rate===void 0)return fail2("set_rate requires `rate`.");o=await s("setRate",{rate:t.rate});break;case"reset":o=await s("reset");break;case"settings":o=await s("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 s("jumpToTokenExpiry",{...t.token?{token:t.token}:{},...t.leadMs!==void 0?{leadMs:t.leadMs}:{}});break;case"fail_next_request":o=await s("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 s("stopTokenCheck");break}return text(describe(o))}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process=require("child_process");var import_node_util=require("util");var import_zod16=require("zod");var PERMISSIONS_TOOL_ID="permissions";var execFileAsync=(0,import_node_util.promisify)(import_node_child_process.execFile);var PERMISSION_IDS=["location","locationBackground","camera","microphone","notifications","photos","contacts","calendar","reminders","tracking","motion","bluetooth"];function describe2(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 s of e.permissions){const o=[s.override?`override **${s.override}**`:"not overridden",`system ${s.real??"unknown"}`,s.calls?`asked ${s.calls}\xD7`:"not asked yet",s.sources.length?`via ${s.sources.join(", ")}`:null,`can be: ${s.states.join(", ")}`].filter(Boolean);t.push(`- ${s.id} (${s.label}): ${o.join("; ")}`);if(s.warning)t.push(` \u26A0 ${s.warning}`)}const r=e.settings;t.push(`Settings: requestAnswer=${r.requestAnswer} notifyApp=${r.notifyApp} interceptSettings=${r.interceptSettings} enforce=${r.enforce} persist=${r.persist} showChip=${r.showChip}`);if(e.log.length){t.push("Recent calls (newest first):");for(const s of e.log.slice(0,n)){const o=new Date(s.at).toISOString().slice(11,19);const i=s.error??s.returned??"no answer";t.push(`- ${o} ${s.source}.${s.method} \u2192 ${i}${s.overridden?" (override)":""}${s.real&&s.real!==s.returned?`, system ${s.real}`:""}${s.answer?`, answered ${s.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 SIMCTL_SERVICE={location:"location",locationBackground:"location-always",camera:"camera",microphone:"microphone",photos:"photos",contacts:"contacts",calendar:"calendar",reminders:"reminders",motion:"motion"};var ANDROID_PERMISSIONS={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 run(e,n){const{stdout:t}=await execFileAsync(e,n,{timeout:2e4});return t}async function findSimulator(e,n){if(n)return n;const t=JSON.parse(await run("xcrun",["simctl","list","devices","booted","-j"]));const r=Object.values(t.devices).flat();const s=r.filter(o=>o.name===e.host.deviceName);if(s.length===1)return s[0].udid;if(!e.host.deviceName&&r.length===1)return r[0].udid;throw new Error(s.length===0?`No booted simulator is named "${e.host.deviceName??"?"}". Pass \`udid\`.`:`${s.length} booted simulators are named "${e.host.deviceName}". Pass \`udid\`.`)}async function findAdbSerial(e){if(e)return e;const n=await run("adb",["devices"]);const t=n.split("\n").slice(1).map(r=>r.trim().split(/\s+/)).filter(r=>r[1]==="device").map(r=>r[0]);if(t.length===1)return t[0];throw new Error(t.length===0?"No Android device is connected to adb.":"Several Android devices are connected. Pass `serial`.")}async function systemPermission(e,n,t,r){const s=e.host.bundleId;if(!s)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":SIMCTL_SERVICE[t];if(!c){throw new Error(`simctl has no privacy service for ${t}. Override it in the app instead (permission_action set).`)}const u=await findSimulator(e,r.udid);await run("xcrun",["simctl","privacy",u,n,c,s]);return`simctl privacy ${n} ${c} for ${s} on ${u}.${n==="grant"?"":" iOS ends the app's process when a permission is revoked or reset; relaunch it."}`}const o=await findAdbSerial(r.serial);const i=t==="all"?Object.values(ANDROID_PERMISSIONS).flat():ANDROID_PERMISSIONS[t];if(!i)throw new Error(`No Android permission is known for ${t}.`);const a=[];for(const c of i){const u=`android.permission.${c}`;try{if(n==="grant")await run("adb",["-s",o,"shell","pm","grant",s,u]);else{await run("adb",["-s",o,"shell","pm","revoke",s,u]);if(n==="reset"){await run("adb",["-s",o,"shell","pm","clear-permission-flags",s,u,"user-set","user-fixed"])}}a.push(c)}catch{}}if(a.length===0)throw new Error(`None of ${i.join(", ")} could be changed. Is it declared in the app's manifest?`);return`adb ${n} ${a.join(", ")} for ${s} on ${o}.${n==="grant"?"":" Android restarts the app's process when a permission is revoked."}`}function registerPermissionsTools(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:import_zod16.z.number().optional().describe("How many recent calls to list. Default 12."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERMISSIONS_TOOL_ID,"getState",{},r);return text(describe2(s,t.logLimit??12))}catch(r){return fail2(errMsg2(r))}});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:import_zod16.z.enum(["set","clear","reset","answer","settings","request_real","return_from_settings","clear_log"]).describe("What to do."),permission:import_zod16.z.string().optional().describe(`set / clear / request_real (required): ${PERMISSION_IDS.join(", ")}, or android:<android.permission.NAME>.`),state:import_zod16.z.enum(["undetermined","granted","limited","denied","blocked","restricted"]).optional().describe("set (required): the state the app sees."),answer:import_zod16.z.enum(["ask","allow","limited","deny"]).optional().describe("answer (required): how a request from 'not asked' is answered while overridden."),notifyApp:import_zod16.z.boolean().optional().describe("settings: send background \u2192 active after each change. Default on."),interceptSettings:import_zod16.z.boolean().optional().describe("settings: stand in for Settings while overridden. Default on."),enforce:import_zod16.z.boolean().optional().describe("settings: fail calls that need a refused permission. Default on."),persist:import_zod16.z.boolean().optional().describe("settings: keep overrides across reloads. Default on."),showChip:import_zod16.z.boolean().optional().describe("settings: show the floating chip while overridden. Default on."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(i,a={})=>n.callAction(PERMISSIONS_TOOL_ID,i,a,r);let o;switch(t.action){case"set":if(!t.permission||!t.state)return fail2("set requires `permission` and `state`.");o=await s("setOverride",{permission:t.permission,state:t.state});break;case"clear":if(!t.permission)return fail2("clear requires `permission`.");o=await s("clearOverride",{permission:t.permission});break;case"reset":o=await s("resetAll");break;case"answer":if(!t.answer)return fail2("answer requires `answer`: ask, allow, limited or deny.");o=await s("updateSettings",{requestAnswer:t.answer});break;case"settings":o=await s("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 fail2("request_real requires `permission`.");o=await s("requestReal",{permission:t.permission});break;case"return_from_settings":o=await s("simulateReturnFromSettings");break;case"clear_log":o=await s("clearLog");break}return text(describe2(o))}catch(r){return fail2(errMsg2(r))}});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:import_zod16.z.enum(["grant","revoke","reset"]).describe("grant, revoke, or reset to not asked."),permission:import_zod16.z.string().describe(`${PERMISSION_IDS.join(", ")}, or all.`),udid:import_zod16.z.string().optional().describe("iOS: the simulator udid, when its name is ambiguous."),serial:import_zod16.z.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERMISSIONS_TOOL_ID,"getState",{},r);const o=await systemPermission(s,t.action,t.permission,{udid:t.udid,serial:t.serial});return text(o)}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process2=require("child_process");var import_node_util2=require("util");var import_zod17=require("zod");var LIFECYCLE_TOOL_ID="lifecycle";var execFileAsync2=(0,import_node_util2.promisify)(import_node_child_process2.execFile);function listeners(e){if(e==null)return"can't count";return e===0?"none":String(e)}function describe3(e){const n=[];if(e.away){const r=Math.round((Date.now()-e.away.startedAt)/1e3);n.push(`**Simulated: ${e.away.kind==="background"?"in the background":"interrupted (inactive)"}** for ${r}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 ${listeners(e.listeners.appState)}, memory warning ${listeners(e.listeners.memoryWarning)}, deep link ${listeners(e.listeners.deepLink)}`+(e.platform==="android"?`, window focus ${listeners(e.listeners.focus)}`:"")+(e.batteryLibraries.length?`, battery ${listeners(e.listeners.battery)} (${e.batteryLibraries.join(", ")})`:", battery: no battery library installed")+".");if(e.report){const r=e.report;n.push(`
194
- **Report: ${r.label}**${r.collectingUntil!=null?" (still collecting)":""}`);if(r.unavailable)n.push(r.unavailable);if(r.finding)n.push(`Finding: ${r.finding}`);if(r.note)n.push(`Note: ${r.note}`);const s=Object.entries(r.counts).map(([o,i])=>`${i} ${o}`).join(", ");if(!r.unavailable)n.push(`Counts: ${s||"nothing happened"}`);for(const o of r.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 r=e.relaunch;n.push(r.restored==null&&r.after==null?`
270
+ ${formatOutcome(o.lastOutcome)}`);return text(l.join("\n\n"))}catch(r){return fail2(errMsg2(r))}});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:import_zod14.z.enum(["capture","capture_baseline","preview","restore","delete","rename"]).describe("What to do."),id:import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),name:import_zod14.z.string().optional().describe("Snapshot name (for capture and rename)."),mode:import_zod14.z.enum(["live","reload"]).optional().describe("Restore mode. Default 'live'."),excludeKeys:import_zod14.z.array(import_zod14.z.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:import_zod14.z.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:import_zod14.z.array(import_zod14.z.string()).optional().describe("Capture/restore only these source ids (default all)."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(o,a)=>n.callAction(TIME_MACHINE_TOOL_ID,o,a,r);switch(t.action){case"capture":{const o=await s("capture",{name:t.name??"",sourceIds:t.sourceIds});const a=o.snapshot;const i=Object.entries(a.sources).flatMap(([c,u])=>u.warnings.map(l=>`
271
+ \u26A0 ${c}: ${l}`)).join("");return text(`Captured \`${a.id}\` "${a.name}" \u2014 ${kb(a.sizeBytes)} \xB7 ${Object.keys(a.sources).join(", ")}${i}`)}case"capture_baseline":{const o=await s("captureBaseline",{});return text(`Captured baseline \`${o.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`)}case"preview":{if(!t.id)return fail2("preview requires `id` (from get_time_machine).");const o=await s("preview",{id:t.id});const a=[];let i=0;for(const u of o.sources){const l={};for(const m of u.items){l[m.verdict]=(l[m.verdict]??0)+1}const h=(l.added??0)+(l.removed??0)+(l.changed??0)+(l["too-large"]??0);i+=h;const d=Object.entries(l).filter(([m])=>m!=="unchanged").map(([m,w])=>`${w} ${m}`);a.push(`- **${u.label}**: ${d.length?d.join(", "):"no changes"}${u.error?` \u2014 \u26A0 ${u.error}`:""}`);for(const m of u.items.filter(w=>w.verdict!=="unchanged").slice(0,8)){a.push(` ${m.verdict==="added"?"+":m.verdict==="removed"?"\u2212":m.verdict==="wont-apply"?"\u2298":"\u2248"} ${m.label}${m.reason?` (${m.reason})`:""}`)}for(const m of u.notes)a.push(` \xB7 ${m}`)}const c=i===0?"**Restore would change NOTHING** (states identical or nothing applicable).":`**Restore would apply ${i} change(s):**`;return text(`${c}
272
+ ${a.join("\n")}`)}case"restore":{if(!t.id)return fail2("restore requires `id` (from get_time_machine).");const o=await s("restore",{id:t.id,mode:t.mode??"live",excludeKeys:t.excludeKeys,restoreRoute:t.restoreRoute});return text(formatOutcome(o))}case"delete":{if(!t.id)return fail2("delete requires `id`.");await s("delete",{id:t.id});return text(`Deleted snapshot \`${t.id}\`.`)}case"rename":{if(!t.id||!t.name)return fail2("rename requires `id` and `name`.");await s("rename",{id:t.id,name:t.name});return text(`Renamed \`${t.id}\` to "${t.name}".`)}}}catch(r){return fail2(errMsg2(r))}})}var import_zod15=require("zod");var CLOCK_TOOL_ID="clock";function duration(e){const n=e<0?"-":"+";let t=Math.abs(Math.round(e/1e3));if(t===0)return"0s";const r=[];for(const[s,o]of[["d",86400],["h",3600],["m",60],["s",1]]){if(t>=o){r.push(`${Math.floor(t/o)}${s}`);t%=o}if(r.length===2)break}return n+r.join(" ")}function describe(e){const n=[e.active?`**App clock: ${e.virtualIso}** (${e.mode}${e.rate!==1?`, ${e.rate}\xD7`:""}), ${duration(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: ${duration(e.lastJump.byMs)}${e.lastJump.firedTimers?`, ran ${e.lastJump.firedTimers} due timer(s)`:""}`)}n.push(...describeTokens(e));n.push(`Settings: persist=${e.settings.persist} showChip=${e.settings.showChip} fireTimersOnJump=${e.settings.fireTimersOnJump}`);return n.join("\n")}function describeTokens(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 s of n.tokens){const o=s.expiresAt!==void 0?s.expiresAt-e.virtualNow:void 0;const a=s.serverExpiresAt!==void 0?s.serverExpiresAt-e.realNow:void 0;const i=s.neverExpires?"never expires":o===void 0?"expiry unknown":o>0?`expires in ${duration(o).slice(1)} on the app's clock`:`expired ${duration(-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 ${duration(a).slice(1)} more`:"; the server no longer accepts it":"";const u=[s.hint??s.formatLabel,s.expirySource?`expiry from ${s.expirySource}`:null,s.issuedBy?`issued by ${s.issuedBy}`:null,s.claims.sub?`sub ${s.claims.sub}`:null,`${s.uses} request(s)`,s.refreshes?`${s.refreshes} refresh(es)`:null,s.expiredSends?`SENT ${s.expiredSends}\xD7 AFTER EXPIRY`:null,s.rejected?`rejected ${s.rejected}\xD7`:null,s.looping?`REFRESH LOOP (${s.recentRefreshes} new tokens in 20s)`:null].filter(Boolean);t.push(`- [${s.id}] ${s.label} \u2192 ${s.host}: ${i}${c} (${u.join(", ")})`)}const r=n.check;if(r){const s=r.status==="refreshed"?`refreshed${r.kind==="401"&&r.forcedAt?" after the forced 401":""}${r.newIssuedBy?` via ${r.newIssuedBy}`:""}${r.newLifetimeMs?`; new token lasts ${duration(r.newLifetimeMs).slice(1)}`:""}`:r.status==="loop"?`REFRESH LOOP: ${r.newTokens} new tokens since the check started`:r.kind==="401"&&r.forcedAt===void 0?"waiting: the next request to that API will get a 401 (the app must send one)":`waiting for a new token (${duration(e.realNow-r.startedAt).slice(1)} so far)`;const o=r.staleAfterFailure?`; the app re-sent the old token ${r.staleAfterFailure}\xD7 after the 401`:"";t.push(`Refresh check (${r.kind==="401"?"forced 401":"jump to expiry"}): ${s}${o}`)}return t}function registerClockTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(CLOCK_TOOL_ID,"getState",{},r);return text(describe(s))}catch(r){return fail2(errMsg2(r))}});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:import_zod15.z.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:import_zod15.z.union([import_zod15.z.string(),import_zod15.z.number()]).optional().describe("set_time (required) / freeze (optional): target time."),by:import_zod15.z.union([import_zod15.z.string(),import_zod15.z.number()]).optional().describe("shift (required): signed duration like '+1d', '-2h30m', '90s', or ms."),freeze:import_zod15.z.boolean().optional().describe("set_time: stop the clock at `time`. Default false."),fireTimers:import_zod15.z.boolean().optional().describe("shift: run app timers that come due inside a forward jump. Default on."),rate:import_zod15.z.number().optional().describe("set_rate (required): speed multiplier, 0.1-86400; 1 is normal."),persist:import_zod15.z.boolean().optional().describe("settings: keep the override across reloads."),showChip:import_zod15.z.boolean().optional().describe("settings: show the floating clock chip while overridden."),fireTimersOnJump:import_zod15.z.boolean().optional().describe("settings: default for shift's fireTimers."),token:import_zod15.z.string().optional().describe("jump_to_token_expiry / fail_next_request: token id from get_clock. Default: the most recently used token."),leadMs:import_zod15.z.number().optional().describe("jump_to_token_expiry: land this many ms before expiry. Default 10000."),status:import_zod15.z.number().optional().describe("fail_next_request: 401 (default) or 403."),times:import_zod15.z.number().optional().describe("fail_next_request: how many requests to fail, 1-10. Default 1."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(a,i={})=>n.callAction(CLOCK_TOOL_ID,a,i,r);let o;switch(t.action){case"set_time":if(t.time===void 0)return fail2("set_time requires `time`.");o=await s("setTime",{time:t.time,freeze:t.freeze??false});break;case"shift":if(t.by===void 0)return fail2("shift requires `by`, e.g. '+1d'.");o=await s("shift",{by:t.by,...t.fireTimers!==void 0?{fireTimers:t.fireTimers}:{}});break;case"freeze":o=await s("freeze",t.time!==void 0?{time:t.time}:{});break;case"resume":o=await s("resume");break;case"set_rate":if(t.rate===void 0)return fail2("set_rate requires `rate`.");o=await s("setRate",{rate:t.rate});break;case"reset":o=await s("reset");break;case"settings":o=await s("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 s("jumpToTokenExpiry",{...t.token?{token:t.token}:{},...t.leadMs!==void 0?{leadMs:t.leadMs}:{}});break;case"fail_next_request":o=await s("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 s("stopTokenCheck");break}return text(describe(o))}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process=require("child_process");var import_node_util=require("util");var import_zod16=require("zod");var PERMISSIONS_TOOL_ID="permissions";var execFileAsync=(0,import_node_util.promisify)(import_node_child_process.execFile);var PERMISSION_IDS=["location","locationBackground","camera","microphone","notifications","photos","contacts","calendar","reminders","tracking","motion","bluetooth"];function describe2(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 s of e.permissions){const o=[s.override?`override **${s.override}**`:"not overridden",`system ${s.real??"unknown"}`,s.calls?`asked ${s.calls}\xD7`:"not asked yet",s.sources.length?`via ${s.sources.join(", ")}`:null,`can be: ${s.states.join(", ")}`].filter(Boolean);t.push(`- ${s.id} (${s.label}): ${o.join("; ")}`);if(s.warning)t.push(` \u26A0 ${s.warning}`)}const r=e.settings;t.push(`Settings: requestAnswer=${r.requestAnswer} notifyApp=${r.notifyApp} interceptSettings=${r.interceptSettings} enforce=${r.enforce} persist=${r.persist} showChip=${r.showChip}`);if(e.log.length){t.push("Recent calls (newest first):");for(const s of e.log.slice(0,n)){const o=new Date(s.at).toISOString().slice(11,19);const a=s.error??s.returned??"no answer";t.push(`- ${o} ${s.source}.${s.method} \u2192 ${a}${s.overridden?" (override)":""}${s.real&&s.real!==s.returned?`, system ${s.real}`:""}${s.answer?`, answered ${s.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 SIMCTL_SERVICE={location:"location",locationBackground:"location-always",camera:"camera",microphone:"microphone",photos:"photos",contacts:"contacts",calendar:"calendar",reminders:"reminders",motion:"motion"};var ANDROID_PERMISSIONS={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 run(e,n){const{stdout:t}=await execFileAsync(e,n,{timeout:2e4});return t}async function findSimulator(e,n){if(n)return n;const t=JSON.parse(await run("xcrun",["simctl","list","devices","booted","-j"]));const r=Object.values(t.devices).flat();const s=r.filter(o=>o.name===e.host.deviceName);if(s.length===1)return s[0].udid;if(!e.host.deviceName&&r.length===1)return r[0].udid;throw new Error(s.length===0?`No booted simulator is named "${e.host.deviceName??"?"}". Pass \`udid\`.`:`${s.length} booted simulators are named "${e.host.deviceName}". Pass \`udid\`.`)}async function findAdbSerial(e){if(e)return e;const n=await run("adb",["devices"]);const t=n.split("\n").slice(1).map(r=>r.trim().split(/\s+/)).filter(r=>r[1]==="device").map(r=>r[0]);if(t.length===1)return t[0];throw new Error(t.length===0?"No Android device is connected to adb.":"Several Android devices are connected. Pass `serial`.")}async function systemPermission(e,n,t,r){const s=e.host.bundleId;if(!s)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":SIMCTL_SERVICE[t];if(!c){throw new Error(`simctl has no privacy service for ${t}. Override it in the app instead (permission_action set).`)}const u=await findSimulator(e,r.udid);await run("xcrun",["simctl","privacy",u,n,c,s]);return`simctl privacy ${n} ${c} for ${s} on ${u}.${n==="grant"?"":" iOS ends the app's process when a permission is revoked or reset; relaunch it."}`}const o=await findAdbSerial(r.serial);const a=t==="all"?Object.values(ANDROID_PERMISSIONS).flat():ANDROID_PERMISSIONS[t];if(!a)throw new Error(`No Android permission is known for ${t}.`);const i=[];for(const c of a){const u=`android.permission.${c}`;try{if(n==="grant")await run("adb",["-s",o,"shell","pm","grant",s,u]);else{await run("adb",["-s",o,"shell","pm","revoke",s,u]);if(n==="reset"){await run("adb",["-s",o,"shell","pm","clear-permission-flags",s,u,"user-set","user-fixed"])}}i.push(c)}catch{}}if(i.length===0)throw new Error(`None of ${a.join(", ")} could be changed. Is it declared in the app's manifest?`);return`adb ${n} ${i.join(", ")} for ${s} on ${o}.${n==="grant"?"":" Android restarts the app's process when a permission is revoked."}`}function registerPermissionsTools(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:import_zod16.z.number().optional().describe("How many recent calls to list. Default 12."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERMISSIONS_TOOL_ID,"getState",{},r);return text(describe2(s,t.logLimit??12))}catch(r){return fail2(errMsg2(r))}});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:import_zod16.z.enum(["set","clear","reset","answer","settings","request_real","return_from_settings","clear_log"]).describe("What to do."),permission:import_zod16.z.string().optional().describe(`set / clear / request_real (required): ${PERMISSION_IDS.join(", ")}, or android:<android.permission.NAME>.`),state:import_zod16.z.enum(["undetermined","granted","limited","denied","blocked","restricted"]).optional().describe("set (required): the state the app sees."),answer:import_zod16.z.enum(["ask","allow","limited","deny"]).optional().describe("answer (required): how a request from 'not asked' is answered while overridden."),notifyApp:import_zod16.z.boolean().optional().describe("settings: send background \u2192 active after each change. Default on."),interceptSettings:import_zod16.z.boolean().optional().describe("settings: stand in for Settings while overridden. Default on."),enforce:import_zod16.z.boolean().optional().describe("settings: fail calls that need a refused permission. Default on."),persist:import_zod16.z.boolean().optional().describe("settings: keep overrides across reloads. Default on."),showChip:import_zod16.z.boolean().optional().describe("settings: show the floating chip while overridden. Default on."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(a,i={})=>n.callAction(PERMISSIONS_TOOL_ID,a,i,r);let o;switch(t.action){case"set":if(!t.permission||!t.state)return fail2("set requires `permission` and `state`.");o=await s("setOverride",{permission:t.permission,state:t.state});break;case"clear":if(!t.permission)return fail2("clear requires `permission`.");o=await s("clearOverride",{permission:t.permission});break;case"reset":o=await s("resetAll");break;case"answer":if(!t.answer)return fail2("answer requires `answer`: ask, allow, limited or deny.");o=await s("updateSettings",{requestAnswer:t.answer});break;case"settings":o=await s("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 fail2("request_real requires `permission`.");o=await s("requestReal",{permission:t.permission});break;case"return_from_settings":o=await s("simulateReturnFromSettings");break;case"clear_log":o=await s("clearLog");break}return text(describe2(o))}catch(r){return fail2(errMsg2(r))}});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:import_zod16.z.enum(["grant","revoke","reset"]).describe("grant, revoke, or reset to not asked."),permission:import_zod16.z.string().describe(`${PERMISSION_IDS.join(", ")}, or all.`),udid:import_zod16.z.string().optional().describe("iOS: the simulator udid, when its name is ambiguous."),serial:import_zod16.z.string().optional().describe("Android: the adb serial, when several devices are connected."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(PERMISSIONS_TOOL_ID,"getState",{},r);const o=await systemPermission(s,t.action,t.permission,{udid:t.udid,serial:t.serial});return text(o)}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process2=require("child_process");var import_node_util2=require("util");var import_zod17=require("zod");var LIFECYCLE_TOOL_ID="lifecycle";var execFileAsync2=(0,import_node_util2.promisify)(import_node_child_process2.execFile);function listeners(e){if(e==null)return"can't count";return e===0?"none":String(e)}function describe3(e){const n=[];if(e.away){const r=Math.round((Date.now()-e.away.startedAt)/1e3);n.push(`**Simulated: ${e.away.kind==="background"?"in the background":"interrupted (inactive)"}** for ${r}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 ${listeners(e.listeners.appState)}, memory warning ${listeners(e.listeners.memoryWarning)}, deep link ${listeners(e.listeners.deepLink)}`+(e.platform==="android"?`, window focus ${listeners(e.listeners.focus)}`:"")+(e.batteryLibraries.length?`, battery ${listeners(e.listeners.battery)} (${e.batteryLibraries.join(", ")})`:", battery: no battery library installed")+".");if(e.report){const r=e.report;n.push(`
273
+ **Report: ${r.label}**${r.collectingUntil!=null?" (still collecting)":""}`);if(r.unavailable)n.push(r.unavailable);if(r.finding)n.push(`Finding: ${r.finding}`);if(r.note)n.push(`Note: ${r.note}`);const s=Object.entries(r.counts).map(([o,a])=>`${a} ${o}`).join(", ");if(!r.unavailable)n.push(`Counts: ${s||"nothing happened"}`);for(const o of r.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 r=e.relaunch;n.push(r.restored==null&&r.after==null?`
195
274
  Relaunch: checking where the app landed (was on ${r.before??"unknown"}).`:`
196
275
  Relaunch: ${r.restored?`restored ${r.after}`:`landed on ${r.after??"unknown"}, was on ${r.before??"unknown"}`}${r.tookMs!=null?` (${(r.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(`
197
- Recent: ${e.log.slice(0,5).map(r=>r.text).join(" \xB7 ")}`);const t=e.recipes.filter(r=>!r.unavailable).map(r=>r.id);n.push(`Recipes: ${t.join(", ")}`);return n.join("\n")}async function simctl(e){const{stdout:n}=await execFileAsync2("xcrun",["simctl",...e],{timeout:2e4});return n}async function findSimulator2(e,n){if(process.platform!=="darwin")throw new Error("Real lifecycle events need macOS with the iOS Simulator.");if(e.platform!=="ios"||e.host.isSimulator!==true){throw new Error("Real lifecycle events work on an iOS simulator only (Android support is not built yet). Leave `real` off to simulate from inside the app.")}if(n)return n;const t=JSON.parse(await simctl(["list","devices","booted","-j"]));const r=Object.values(t.devices).flat().filter(s=>s.name===e.host.deviceName);if(r.length===1)return r[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\`.`)}function needBundle(e){if(!e.host.bundleId)throw new Error("The app did not report its bundle id (needs expo-application).");return e.host.bundleId}var sleep=e=>new Promise(n=>setTimeout(n,e));function registerLifecycleTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(LIFECYCLE_TOOL_ID,"getState",{},r);return text(describe3(s))}catch(r){return fail2(errMsg2(r))}});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 or react-native-device-info), 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` on an iOS simulator, background/return/relaunch/color_scheme/open_url use `xcrun simctl` for the real OS event instead (relaunch then kills the process, a real cold start). Read get_lifecycle afterwards for the report.",inputSchema:{action:import_zod17.z.enum(["background","interrupt","return","memory_warning","open_url","press_back","color_scheme","battery","reset_battery","relaunch","recipe","clear_report","reset","settings"]).describe("What to do."),duration:import_zod17.z.union([import_zod17.z.string(),import_zod17.z.number()]).optional().describe("background / interrupt: return by itself after this long ('5s', '5m', ms). Omit to stay until return."),skipWait:import_zod17.z.boolean().optional().describe("background / interrupt: move the app clock forward by `duration` and return at once. Needs @buoy-gg/clock."),url:import_zod17.z.string().optional().describe("open_url (required): the link, with its scheme."),scheme:import_zod17.z.enum(["light","dark","system"]).optional().describe("color_scheme (required). 'system' ends the simulated scheme."),mode:import_zod17.z.enum(["js","native"]).optional().describe("color_scheme: 'js' emits the change to JS listeners (default); 'native' overrides the app's native appearance too."),level:import_zod17.z.number().optional().describe("battery: 0 to 1 (or a percentage above 1)."),lowPowerMode:import_zod17.z.boolean().optional().describe("battery: low power mode on or off."),state:import_zod17.z.enum(["unplugged","charging","full","unknown"]).optional().describe("battery: charging state."),id:import_zod17.z.string().optional().describe("recipe (required): a recipe id from get_lifecycle, e.g. 'away-31m'."),showChip:import_zod17.z.boolean().optional().describe("settings: show the floating status chip while simulating."),reportWindow:import_zod17.z.union([import_zod17.z.string(),import_zod17.z.number()]).optional().describe("settings: how long reports keep collecting after the app returns ('5s')."),real:import_zod17.z.boolean().optional().describe("iOS simulator only: cause the real OS event through xcrun simctl (background, return, relaunch, color_scheme, open_url)."),udid:import_zod17.z.string().optional().describe("real: the simulator udid, when its name is ambiguous."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(i,a={})=>n.callAction(LIFECYCLE_TOOL_ID,i,a,r);if(t.real){const i=await s("getState");const a=await findSimulator2(i,t.udid);let c;switch(t.action){case"background":await simctl(["launch",a,"com.apple.Preferences"]);c="Sent to the background for real (Settings opened on top). Use return with real: true.";break;case"return":await simctl(["launch",a,needBundle(i)]);c="Brought back to the foreground for real.";break;case"relaunch":{const l=needBundle(i);await s("prepareRelaunch");await simctl(["terminate",a,l]);await simctl(["launch",a,l]);c="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 fail2("color_scheme requires `scheme`.");if(t.scheme==="system")return fail2("With real: true, pass light or dark; the simulator has no system value to go back to.");await simctl(["ui",a,"appearance",t.scheme]);c=`Simulator appearance set to ${t.scheme} (affects every app on this simulator).`;break;case"open_url":if(!t.url)return fail2("open_url requires `url`.");await simctl(["openurl",a,t.url]);c='Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.';break;default:return fail2(`${t.action} has no real (host-side) version. iOS has no command-line memory warning; leave \`real\` off to simulate it.`)}await sleep(1200);const u=t.action==="relaunch"||t.action==="background"?i:await s("getState");return text(`${c}
276
+ Recent: ${e.log.slice(0,5).map(r=>r.text).join(" \xB7 ")}`);const t=e.recipes.filter(r=>!r.unavailable).map(r=>r.id);n.push(`Recipes: ${t.join(", ")}`);return n.join("\n")}async function simctl(e){const{stdout:n}=await execFileAsync2("xcrun",["simctl",...e],{timeout:2e4});return n}async function findSimulator2(e,n){if(process.platform!=="darwin")throw new Error("Real lifecycle events need macOS with the iOS Simulator.");if(e.platform!=="ios"||e.host.isSimulator!==true){throw new Error("Real lifecycle events work on an iOS simulator only (Android support is not built yet). Leave `real` off to simulate from inside the app.")}if(n)return n;const t=JSON.parse(await simctl(["list","devices","booted","-j"]));const r=Object.values(t.devices).flat().filter(s=>s.name===e.host.deviceName);if(r.length===1)return r[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\`.`)}function needBundle(e){if(!e.host.bundleId)throw new Error("The app did not report its bundle id (needs expo-application).");return e.host.bundleId}var sleep=e=>new Promise(n=>setTimeout(n,e));function registerLifecycleTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(LIFECYCLE_TOOL_ID,"getState",{},r);return text(describe3(s))}catch(r){return fail2(errMsg2(r))}});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 or react-native-device-info), 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` on an iOS simulator, background/return/relaunch/color_scheme/open_url use `xcrun simctl` for the real OS event instead (relaunch then kills the process, a real cold start). Read get_lifecycle afterwards for the report.",inputSchema:{action:import_zod17.z.enum(["background","interrupt","return","memory_warning","open_url","press_back","color_scheme","battery","reset_battery","relaunch","recipe","clear_report","reset","settings"]).describe("What to do."),duration:import_zod17.z.union([import_zod17.z.string(),import_zod17.z.number()]).optional().describe("background / interrupt: return by itself after this long ('5s', '5m', ms). Omit to stay until return."),skipWait:import_zod17.z.boolean().optional().describe("background / interrupt: move the app clock forward by `duration` and return at once. Needs @buoy-gg/clock."),url:import_zod17.z.string().optional().describe("open_url (required): the link, with its scheme."),scheme:import_zod17.z.enum(["light","dark","system"]).optional().describe("color_scheme (required). 'system' ends the simulated scheme."),mode:import_zod17.z.enum(["js","native"]).optional().describe("color_scheme: 'js' emits the change to JS listeners (default); 'native' overrides the app's native appearance too."),level:import_zod17.z.number().optional().describe("battery: 0 to 1 (or a percentage above 1)."),lowPowerMode:import_zod17.z.boolean().optional().describe("battery: low power mode on or off."),state:import_zod17.z.enum(["unplugged","charging","full","unknown"]).optional().describe("battery: charging state."),id:import_zod17.z.string().optional().describe("recipe (required): a recipe id from get_lifecycle, e.g. 'away-31m'."),showChip:import_zod17.z.boolean().optional().describe("settings: show the floating status chip while simulating."),reportWindow:import_zod17.z.union([import_zod17.z.string(),import_zod17.z.number()]).optional().describe("settings: how long reports keep collecting after the app returns ('5s')."),real:import_zod17.z.boolean().optional().describe("iOS simulator only: cause the real OS event through xcrun simctl (background, return, relaunch, color_scheme, open_url)."),udid:import_zod17.z.string().optional().describe("real: the simulator udid, when its name is ambiguous."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(a,i={})=>n.callAction(LIFECYCLE_TOOL_ID,a,i,r);if(t.real){const a=await s("getState");const i=await findSimulator2(a,t.udid);let c;switch(t.action){case"background":await simctl(["launch",i,"com.apple.Preferences"]);c="Sent to the background for real (Settings opened on top). Use return with real: true.";break;case"return":await simctl(["launch",i,needBundle(a)]);c="Brought back to the foreground for real.";break;case"relaunch":{const l=needBundle(a);await s("prepareRelaunch");await simctl(["terminate",i,l]);await simctl(["launch",i,l]);c="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 fail2("color_scheme requires `scheme`.");if(t.scheme==="system")return fail2("With real: true, pass light or dark; the simulator has no system value to go back to.");await simctl(["ui",i,"appearance",t.scheme]);c=`Simulator appearance set to ${t.scheme} (affects every app on this simulator).`;break;case"open_url":if(!t.url)return fail2("open_url requires `url`.");await simctl(["openurl",i,t.url]);c='Opened through the OS. A custom scheme may first show an "Open in" prompt on the simulator.';break;default:return fail2(`${t.action} has no real (host-side) version. iOS has no command-line memory warning; leave \`real\` off to simulate it.`)}await sleep(1200);const u=t.action==="relaunch"||t.action==="background"?a:await s("getState");return text(`${c}
198
277
 
199
278
  ${describe3(u)}`)}let o;switch(t.action){case"background":o=await s("background",{duration:t.duration,skipWait:t.skipWait});break;case"interrupt":o=await s("interrupt",{duration:t.duration,skipWait:t.skipWait});break;case"return":o=await s("returnToApp");break;case"memory_warning":o=await s("memoryWarning");break;case"open_url":if(!t.url)return fail2("open_url requires `url`.");o=await s("openUrl",{url:t.url});break;case"press_back":o=await s("pressBack");break;case"color_scheme":if(!t.scheme)return fail2("color_scheme requires `scheme`.");o=await s("setColorScheme",{scheme:t.scheme,mode:t.mode??"js"});break;case"battery":o=await s("setPower",{...t.level!==void 0?{level:t.level}:{},...t.lowPowerMode!==void 0?{lowPowerMode:t.lowPowerMode}:{},...t.state?{state:t.state}:{}});break;case"reset_battery":o=await s("resetPower");break;case"relaunch":o=await s("relaunch");return text(`Relaunching: the app's JS restarts now. Read get_lifecycle in a few seconds for where it landed.
200
279
 
201
- ${describe3(o)}`);case"recipe":if(!t.id)return fail2("recipe requires `id`; get_lifecycle lists them.");o=await s("runRecipe",{id:t.id});break;case"clear_report":o=await s("clearReport");break;case"reset":o=await s("reset");break;case"settings":o=await s("updateSettings",{...t.showChip!==void 0?{showChip:t.showChip}:{},...t.reportWindow!==void 0?{reportWindow:t.reportWindow}:{}});break}return text(describe3(o))}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process3=require("child_process");var import_node_util3=require("util");var import_zod18=require("zod");var execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process3.execFile);var LOCATION_TOOL_ID="location";function meters(e){if(e==null||!Number.isFinite(e))return"?";return e<1e3?`${Math.round(e)} m`:`${(e/1e3).toFixed(e<1e4?1:0)} km`}function coord(e){return`${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`}function describe4(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 i=o?coord(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, ${meters(e.progress.along)} of ${meters(e.progress.length)}${n.route.loop?" (loops)":""} \u2192 ${i}`)}else if(n.mode==="fixed"){t.push(`**Location: pinned** ${n.place.label?`"${n.place.label}" `:""}\u2192 ${i}`)}else{t.push(`**Location: real position**, with overrides below`)}if(o){t.push(`Reported: \xB1${meters(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 ${meters(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 r=e.watches.filter(o=>o.kind==="position");t.push(`Live watches: ${r.length?r.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 i of o.regions){t.push(`- ${i.identifier}${i.label?` (${i.label})`:""}: ${i.state}, radius ${meters(i.radius)}${i.distance!=null?`, ${meters(i.distance)} from center`:""} (${coord(i)})`)}}else{t.push(`Background location task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs)`)}}const s=e.log.slice(-10);if(s.length){t.push("Recent:");for(const o of s)t.push(`- [${o.kind}] ${o.text}${o.detail?` (${o.detail})`:""}`)}if(e.places.length)t.push(`App places: ${e.places.map(o=>o.label??coord(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 bootedSimulators(){const{stdout:e}=await execFileAsync3("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:r})=>({udid:t,name:r}))}async function resolveSimulator(e){if(process.platform!=="darwin")throw new Error("sim_location needs macOS with Xcode's iOS Simulator.");const n=await bootedSimulators();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 registerLocationTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(LOCATION_TOOL_ID,"getState",{},r);return text(describe4(s))}catch(r){return fail2(errMsg2(r))}});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:import_zod18.z.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:import_zod18.z.number().optional().describe("set_location: latitude, -90 to 90."),longitude:import_zod18.z.number().optional().describe("set_location: longitude, -180 to 180."),label:import_zod18.z.string().optional().describe("set_location: a name to show for the place."),query:import_zod18.z.string().optional().describe("set_location: 'lat, lon' text or a Google/Apple Maps link."),place:import_zod18.z.string().optional().describe("set_location: a place name from get_location (app places and built-ins)."),points:import_zod18.z.array(import_zod18.z.object({latitude:import_zod18.z.number(),longitude:import_zod18.z.number()})).optional().describe("start_route: two or more points to travel through."),route:import_zod18.z.string().optional().describe("start_route: a preset id (walk, run, drive, highway, tunnel, loop) or an app route id."),to:import_zod18.z.string().optional().describe("start_route: travel in a straight line to this place name or 'lat, lon'."),speed:import_zod18.z.number().optional().describe("start_route / set_speed: meters per second (1.4 walk, 13.4 city, 30 highway)."),heading:import_zod18.z.number().optional().describe("start_route with a preset: direction to head, degrees from north. tune: heading reported while pinned."),loop:import_zod18.z.boolean().optional().describe("start_route with points: start over after the last point."),meters:import_zod18.z.number().optional().describe("seek_route: meters from the route's start."),fraction:import_zod18.z.number().optional().describe("seek_route: 0 to 1 along the route."),identifier:import_zod18.z.string().optional().describe("enter_region / exit_region (required): the region's identifier from get_location."),task:import_zod18.z.string().optional().describe("enter_region / exit_region: the geofencing task name, when two tasks share an identifier."),conditions:import_zod18.z.enum(["good","weak","no-signal","off"]).optional().describe("conditions (required): good, weak, no-signal or off."),on:import_zod18.z.boolean().optional().describe("signal / services (required): true = working, false = off."),accuracy:import_zod18.z.number().optional().describe("tune: reported accuracy radius in meters."),jitter:import_zod18.z.number().optional().describe("tune: GPS drift in meters (0 = none)."),interval:import_zod18.z.number().optional().describe("tune: ms between updates to live watches (default 1000)."),persist:import_zod18.z.boolean().optional().describe("settings: keep the override across reloads."),showChip:import_zod18.z.boolean().optional().describe("settings: show the floating location chip while overridden."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(a,c={})=>n.callAction(LOCATION_TOOL_ID,a,c,r);const o=(...a)=>{const c={};for(const u of a)if(t[u]!==void 0)c[u]=t[u];return c};let i;switch(t.action){case"set_location":i=await s("setLocation",o("latitude","longitude","label","query","place"));break;case"start_route":i=await s("startRoute",o("points","route","to","speed","heading","loop"));break;case"pause_route":i=await s("pauseRoute");break;case"resume_route":i=await s("resumeRoute");break;case"stop_route":i=await s("stopRoute");break;case"seek_route":i=await s("seekRoute",o("meters","fraction"));break;case"set_speed":if(t.speed===void 0)return fail2("set_speed requires `speed` in m/s.");i=await s("setSpeed",{speed:t.speed});break;case"enter_region":case"exit_region":if(!t.identifier)return fail2(`${t.action} requires \`identifier\`; get_location lists the regions.`);i=await s(t.action==="enter_region"?"enterRegion":"exitRegion",o("identifier","task"));break;case"conditions":if(!t.conditions)return fail2("conditions requires `conditions`: good, weak, no-signal or off.");i=await s("setConditions",{conditions:t.conditions});break;case"signal":if(t.on===void 0)return fail2("signal requires `on`.");i=await s("setSignal",{on:t.on});break;case"services":if(t.on===void 0)return fail2("services requires `on`.");i=await s("setServices",{on:t.on});break;case"tune":i=await s("tune",o("accuracy","jitter","heading","interval"));break;case"use_real":i=await s("useRealLocation");break;case"reset":i=await s("reset");break;case"settings":i=await s("updateSettings",o("persist","showChip"));break;case"clear_log":i=await s("clearLog");break}return text(describe4(i))}catch(r){return fail2(errMsg2(r))}});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:import_zod18.z.enum(["set","route","clear"]).describe("What to do."),latitude:import_zod18.z.number().optional().describe("set: latitude."),longitude:import_zod18.z.number().optional().describe("set: longitude."),points:import_zod18.z.array(import_zod18.z.object({latitude:import_zod18.z.number(),longitude:import_zod18.z.number()})).optional().describe("route: two or more waypoints."),speed:import_zod18.z.number().optional().describe("route: meters per second. Default 20."),interval:import_zod18.z.number().optional().describe("route: seconds between updates. Default 1."),udid:import_zod18.z.string().optional().describe("The simulator's UDID. Required when several are booted.")}},async t=>{try{const r=await resolveSimulator(t.udid);const s=(...o)=>execFileAsync3("xcrun",["simctl","location",r,...o],{timeout:15e3});switch(t.action){case"set":if(t.latitude===void 0||t.longitude===void 0)return fail2("set requires `latitude` and `longitude`.");await s("set",`${t.latitude},${t.longitude}`);return text(`Simulator ${r} location set to ${t.latitude}, ${t.longitude}.`);case"route":{const o=t.points??[];if(o.length<2)return fail2("route requires two or more `points`.");const i=[`--speed=${t.speed??20}`,`--interval=${t.interval??1}`];await s("start",...i,...o.map(a=>`${a.latitude},${a.longitude}`));return text(`Simulator ${r} is moving through ${o.length} points at ${t.speed??20} m/s.`)}case"clear":await s("clear");return text(`Simulator ${r} location cleared.`)}}catch(r){return fail2(errMsg2(r))}})}var import_zod19=require("zod");var SCENARIOS_TOOL_ID="scenarios";var text3=e=>({content:[{type:"text",text:e}]});var fail4=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg4=e=>e instanceof Error?e.message:String(e);function varSpec(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 summarize(e){const n=e.vars?.length?`
280
+ ${describe3(o)}`);case"recipe":if(!t.id)return fail2("recipe requires `id`; get_lifecycle lists them.");o=await s("runRecipe",{id:t.id});break;case"clear_report":o=await s("clearReport");break;case"reset":o=await s("reset");break;case"settings":o=await s("updateSettings",{...t.showChip!==void 0?{showChip:t.showChip}:{},...t.reportWindow!==void 0?{reportWindow:t.reportWindow}:{}});break}return text(describe3(o))}catch(r){return fail2(errMsg2(r))}})}var import_node_child_process3=require("child_process");var import_node_util3=require("util");var import_zod18=require("zod");var execFileAsync3=(0,import_node_util3.promisify)(import_node_child_process3.execFile);var LOCATION_TOOL_ID="location";function meters(e){if(e==null||!Number.isFinite(e))return"?";return e<1e3?`${Math.round(e)} m`:`${(e/1e3).toFixed(e<1e4?1:0)} km`}function coord(e){return`${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`}function describe4(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?coord(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, ${meters(e.progress.along)} of ${meters(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${meters(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 ${meters(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 r=e.watches.filter(o=>o.kind==="position");t.push(`Live watches: ${r.length?r.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 ${meters(a.radius)}${a.distance!=null?`, ${meters(a.distance)} from center`:""} (${coord(a)})`)}}else{t.push(`Background location task "${o.name}" (${o.intercepted?"simulated":"native"}, ${o.runs} runs)`)}}const s=e.log.slice(-10);if(s.length){t.push("Recent:");for(const o of s)t.push(`- [${o.kind}] ${o.text}${o.detail?` (${o.detail})`:""}`)}if(e.places.length)t.push(`App places: ${e.places.map(o=>o.label??coord(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 bootedSimulators(){const{stdout:e}=await execFileAsync3("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:r})=>({udid:t,name:r}))}async function resolveSimulator(e){if(process.platform!=="darwin")throw new Error("sim_location needs macOS with Xcode's iOS Simulator.");const n=await bootedSimulators();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 registerLocationTools(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:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(LOCATION_TOOL_ID,"getState",{},r);return text(describe4(s))}catch(r){return fail2(errMsg2(r))}});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:import_zod18.z.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:import_zod18.z.number().optional().describe("set_location: latitude, -90 to 90."),longitude:import_zod18.z.number().optional().describe("set_location: longitude, -180 to 180."),label:import_zod18.z.string().optional().describe("set_location: a name to show for the place."),query:import_zod18.z.string().optional().describe("set_location: 'lat, lon' text or a Google/Apple Maps link."),place:import_zod18.z.string().optional().describe("set_location: a place name from get_location (app places and built-ins)."),points:import_zod18.z.array(import_zod18.z.object({latitude:import_zod18.z.number(),longitude:import_zod18.z.number()})).optional().describe("start_route: two or more points to travel through."),route:import_zod18.z.string().optional().describe("start_route: a preset id (walk, run, drive, highway, tunnel, loop) or an app route id."),to:import_zod18.z.string().optional().describe("start_route: travel in a straight line to this place name or 'lat, lon'."),speed:import_zod18.z.number().optional().describe("start_route / set_speed: meters per second (1.4 walk, 13.4 city, 30 highway)."),heading:import_zod18.z.number().optional().describe("start_route with a preset: direction to head, degrees from north. tune: heading reported while pinned."),loop:import_zod18.z.boolean().optional().describe("start_route with points: start over after the last point."),meters:import_zod18.z.number().optional().describe("seek_route: meters from the route's start."),fraction:import_zod18.z.number().optional().describe("seek_route: 0 to 1 along the route."),identifier:import_zod18.z.string().optional().describe("enter_region / exit_region (required): the region's identifier from get_location."),task:import_zod18.z.string().optional().describe("enter_region / exit_region: the geofencing task name, when two tasks share an identifier."),conditions:import_zod18.z.enum(["good","weak","no-signal","off"]).optional().describe("conditions (required): good, weak, no-signal or off."),on:import_zod18.z.boolean().optional().describe("signal / services (required): true = working, false = off."),accuracy:import_zod18.z.number().optional().describe("tune: reported accuracy radius in meters."),jitter:import_zod18.z.number().optional().describe("tune: GPS drift in meters (0 = none)."),interval:import_zod18.z.number().optional().describe("tune: ms between updates to live watches (default 1000)."),persist:import_zod18.z.boolean().optional().describe("settings: keep the override across reloads."),showChip:import_zod18.z.boolean().optional().describe("settings: show the floating location chip while overridden."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(i,c={})=>n.callAction(LOCATION_TOOL_ID,i,c,r);const o=(...i)=>{const c={};for(const u of i)if(t[u]!==void 0)c[u]=t[u];return c};let a;switch(t.action){case"set_location":a=await s("setLocation",o("latitude","longitude","label","query","place"));break;case"start_route":a=await s("startRoute",o("points","route","to","speed","heading","loop"));break;case"pause_route":a=await s("pauseRoute");break;case"resume_route":a=await s("resumeRoute");break;case"stop_route":a=await s("stopRoute");break;case"seek_route":a=await s("seekRoute",o("meters","fraction"));break;case"set_speed":if(t.speed===void 0)return fail2("set_speed requires `speed` in m/s.");a=await s("setSpeed",{speed:t.speed});break;case"enter_region":case"exit_region":if(!t.identifier)return fail2(`${t.action} requires \`identifier\`; get_location lists the regions.`);a=await s(t.action==="enter_region"?"enterRegion":"exitRegion",o("identifier","task"));break;case"conditions":if(!t.conditions)return fail2("conditions requires `conditions`: good, weak, no-signal or off.");a=await s("setConditions",{conditions:t.conditions});break;case"signal":if(t.on===void 0)return fail2("signal requires `on`.");a=await s("setSignal",{on:t.on});break;case"services":if(t.on===void 0)return fail2("services requires `on`.");a=await s("setServices",{on:t.on});break;case"tune":a=await s("tune",o("accuracy","jitter","heading","interval"));break;case"use_real":a=await s("useRealLocation");break;case"reset":a=await s("reset");break;case"settings":a=await s("updateSettings",o("persist","showChip"));break;case"clear_log":a=await s("clearLog");break}return text(describe4(a))}catch(r){return fail2(errMsg2(r))}});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:import_zod18.z.enum(["set","route","clear"]).describe("What to do."),latitude:import_zod18.z.number().optional().describe("set: latitude."),longitude:import_zod18.z.number().optional().describe("set: longitude."),points:import_zod18.z.array(import_zod18.z.object({latitude:import_zod18.z.number(),longitude:import_zod18.z.number()})).optional().describe("route: two or more waypoints."),speed:import_zod18.z.number().optional().describe("route: meters per second. Default 20."),interval:import_zod18.z.number().optional().describe("route: seconds between updates. Default 1."),udid:import_zod18.z.string().optional().describe("The simulator's UDID. Required when several are booted.")}},async t=>{try{const r=await resolveSimulator(t.udid);const s=(...o)=>execFileAsync3("xcrun",["simctl","location",r,...o],{timeout:15e3});switch(t.action){case"set":if(t.latitude===void 0||t.longitude===void 0)return fail2("set requires `latitude` and `longitude`.");await s("set",`${t.latitude},${t.longitude}`);return text(`Simulator ${r} location set to ${t.latitude}, ${t.longitude}.`);case"route":{const o=t.points??[];if(o.length<2)return fail2("route requires two or more `points`.");const a=[`--speed=${t.speed??20}`,`--interval=${t.interval??1}`];await s("start",...a,...o.map(i=>`${i.latitude},${i.longitude}`));return text(`Simulator ${r} is moving through ${o.length} points at ${t.speed??20} m/s.`)}case"clear":await s("clear");return text(`Simulator ${r} location cleared.`)}}catch(r){return fail2(errMsg2(r))}})}var import_zod19=require("zod");var SCENARIOS_TOOL_ID="scenarios";var text3=e=>({content:[{type:"text",text:e}]});var fail4=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg4=e=>e instanceof Error?e.message:String(e);function varSpec(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 summarize(e){const n=e.vars?.length?`
202
281
  variables: ${e.vars.map(varSpec).join("; ")}`:"";const t=e.stepLines?.length?`
203
282
  ${e.stepLines.map(o=>` \xB7 ${o.label} [${o.durability}]`).join("\n")}`:"";const r=[`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 s=e.expectedOutcome?`
204
283
  expected: ${e.expectedOutcome}`:"";return`- \`${e.id}\` **${e.name}** \u2014 ${r}${n}${s}${t}`}function renderSection(e,n,t){if(!n.length)return`**${e} (0):**
205
284
  _none_`;if(!t.length){return`**${e} (${n.length}):**
206
- ${n.map(summarize).join("\n")}`}const r=[`**${e} (${n.length}):**`];for(const o of t){const i=n.filter(a=>(a.folder??"").toLowerCase()===o.toLowerCase());if(!i.length)continue;r.push(`_${o} (${i.length})_
207
- ${i.map(summarize).join("\n")}`)}const s=n.filter(o=>!o.folder);if(s.length){r.push(`_Ungrouped (${s.length})_
285
+ ${n.map(summarize).join("\n")}`}const r=[`**${e} (${n.length}):**`];for(const o of t){const a=n.filter(i=>(i.folder??"").toLowerCase()===o.toLowerCase());if(!a.length)continue;r.push(`_${o} (${a.length})_
286
+ ${a.map(summarize).join("\n")}`)}const s=n.filter(o=>!o.folder);if(s.length){r.push(`_Ungrouped (${s.length})_
208
287
  ${s.map(summarize).join("\n")}`)}return r.join("\n")}function formatActive(e){const n=Object.entries(e.varValues).map(([t,r])=>`${t}=${String(r)}`).join(", ");return[`**${e.name}** (\`${e.scenarioId}\` v${e.version}) \u2014 active since ${new Date(e.runAt).toISOString()}`,n?`variables: ${n}`:null,`effects:
209
- ${e.effects.map(t=>` \xB7 ${t.label} [${t.kind}]`).join("\n")}`].filter(Boolean).join("\n")}function registerScenariosTools(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:import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(SCENARIOS_TOOL_ID,r);const o=s??{};const i=o.code??[];const a=o.device??[];const c=o.drafts??[];const u=o.folders??[];const l=[];l.push(o.active?`\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
210
- ${formatActive(o.active)}`:"**No scenario active** \u2014 the app is showing real data.");if(u.length){l.push(`**Folders:** ${u.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.`)}l.push(renderSection("Shipped with this app",i,u));l.push(renderSection("Saved on this device",a,u));if(c.length){l.push(`**Drafts awaiting review on the device (${c.length}):** these are INERT and cannot run until a human accepts them on the device.
211
- ${c.map(summarize).join("\n")}`)}return text3(l.join("\n\n"))}catch(r){return fail4(errMsg4(r))}});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:import_zod19.z.enum(["preview","run","deactivate","save","setFolder","delete"]).describe("What to do."),folder:import_zod19.z.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:import_zod19.z.string().optional().describe("Scenario id (required for preview/run/delete; from get_scenarios)."),params:import_zod19.z.record(import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.boolean()])).optional().describe("Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."),scenario:import_zod19.z.object({id:import_zod19.z.string().describe("Stable kebab-case id."),name:import_zod19.z.string().describe("Human name; may contain {{variable}} placeholders."),description:import_zod19.z.string().optional(),expectedOutcome:import_zod19.z.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:import_zod19.z.string().optional(),authoredAgainst:import_zod19.z.string().optional().describe("App version this was written against."),folder:import_zod19.z.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:import_zod19.z.array(import_zod19.z.string()).optional(),vars:import_zod19.z.array(import_zod19.z.object({key:import_zod19.z.string(),label:import_zod19.z.string().optional(),type:import_zod19.z.enum(["string","number","boolean","enum"]),options:import_zod19.z.array(import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.boolean()])).optional(),default:import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.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:import_zod19.z.array(import_zod19.z.object({id:import_zod19.z.string().optional(),tool:import_zod19.z.string().describe("Tool id: network | storage | impersonate | route-events | query | time-machine"),action:import_zod19.z.string().describe("Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."),params:import_zod19.z.unknown().optional(),label:import_zod19.z.string().optional()})).describe("Steps, in order."),undoSteps:import_zod19.z.array(import_zod19.z.object({id:import_zod19.z.string().optional(),tool:import_zod19.z.string(),action:import_zod19.z.string(),params:import_zod19.z.unknown().optional(),label:import_zod19.z.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:import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(o,i)=>n.callAction(SCENARIOS_TOOL_ID,o,i,r);switch(t.action){case"preview":{if(!t.id)return fail4("preview requires `id` (from get_scenarios).");const o=await s("preview",{id:t.id,params:t.params??{}});const i=[`**Dry run \u2014 \`${t.id}\`** (nothing has been changed)`,`variables: ${Object.entries(o.values).map(([a,c])=>`${a}=${String(c)}`).join(", ")||"none"}`,`**Would do:**
212
- ${o.willDo.map(a=>` \xB7 ${a.label} [${a.durability}]`).join("\n")}`];if(o.expectedOutcome)i.push(`**Expected outcome:** ${o.expectedOutcome}`);if(o.conflict){i.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)i.push(`**Warnings:**
213
- ${o.warnings.map(a=>` \xB7 ${a}`).join("\n")}`);i.push(o.ok?"Pre-flight checks passed. Review the intended effects before running.":`\u274C **Cannot run:**
214
- ${o.errors.map(a=>` \xB7 ${a}`).join("\n")}`);return text3(i.join("\n\n"))}case"run":{if(!t.id)return fail4("run requires `id` (from get_scenarios).");const o=await s("run",{id:t.id,params:t.params??{}});if(o.preflightErrors?.length){return fail4(`Pre-flight refused to run \`${t.id}\` \u2014 nothing was changed:
215
- ${o.preflightErrors.map(a=>` \xB7 ${a}`).join("\n")}`)}const i=(o.steps??[]).map(a=>{const c=a.skipped?"\u2013":a.ok?"\u2713":"\u2717";const u=a.error?`
216
- ${a.error}`:"";return` ${c} ${a.label} (${a.ms}ms)${u}`}).join("\n");if(!o.ok){return fail4(`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
217
- ${i}
288
+ ${e.effects.map(t=>` \xB7 ${t.label} [${t.kind}]`).join("\n")}`].filter(Boolean).join("\n")}function registerScenariosTools(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:import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const{payload:s}=await n.getSnapshot(SCENARIOS_TOOL_ID,r);const o=s??{};const a=o.code??[];const i=o.device??[];const c=o.drafts??[];const u=o.folders??[];const l=[];l.push(o.active?`\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
289
+ ${formatActive(o.active)}`:"**No scenario active** \u2014 the app is showing real data.");if(u.length){l.push(`**Folders:** ${u.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.`)}l.push(renderSection("Shipped with this app",a,u));l.push(renderSection("Saved on this device",i,u));if(c.length){l.push(`**Drafts awaiting review on the device (${c.length}):** these are INERT and cannot run until a human accepts them on the device.
290
+ ${c.map(summarize).join("\n")}`)}return text3(l.join("\n\n"))}catch(r){return fail4(errMsg4(r))}});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:import_zod19.z.enum(["preview","run","deactivate","save","setFolder","delete"]).describe("What to do."),folder:import_zod19.z.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:import_zod19.z.string().optional().describe("Scenario id (required for preview/run/delete; from get_scenarios)."),params:import_zod19.z.record(import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.boolean()])).optional().describe("Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."),scenario:import_zod19.z.object({id:import_zod19.z.string().describe("Stable kebab-case id."),name:import_zod19.z.string().describe("Human name; may contain {{variable}} placeholders."),description:import_zod19.z.string().optional(),expectedOutcome:import_zod19.z.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:import_zod19.z.string().optional(),authoredAgainst:import_zod19.z.string().optional().describe("App version this was written against."),folder:import_zod19.z.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:import_zod19.z.array(import_zod19.z.string()).optional(),vars:import_zod19.z.array(import_zod19.z.object({key:import_zod19.z.string(),label:import_zod19.z.string().optional(),type:import_zod19.z.enum(["string","number","boolean","enum"]),options:import_zod19.z.array(import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.boolean()])).optional(),default:import_zod19.z.union([import_zod19.z.string(),import_zod19.z.number(),import_zod19.z.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:import_zod19.z.array(import_zod19.z.object({id:import_zod19.z.string().optional(),tool:import_zod19.z.string().describe("Tool id: network | storage | impersonate | route-events | query | time-machine"),action:import_zod19.z.string().describe("Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."),params:import_zod19.z.unknown().optional(),label:import_zod19.z.string().optional()})).describe("Steps, in order."),undoSteps:import_zod19.z.array(import_zod19.z.object({id:import_zod19.z.string().optional(),tool:import_zod19.z.string(),action:import_zod19.z.string(),params:import_zod19.z.unknown().optional(),label:import_zod19.z.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:import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=(o,a)=>n.callAction(SCENARIOS_TOOL_ID,o,a,r);switch(t.action){case"preview":{if(!t.id)return fail4("preview requires `id` (from get_scenarios).");const o=await s("preview",{id:t.id,params:t.params??{}});const a=[`**Dry run \u2014 \`${t.id}\`** (nothing has been changed)`,`variables: ${Object.entries(o.values).map(([i,c])=>`${i}=${String(c)}`).join(", ")||"none"}`,`**Would do:**
291
+ ${o.willDo.map(i=>` \xB7 ${i.label} [${i.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:**
292
+ ${o.warnings.map(i=>` \xB7 ${i}`).join("\n")}`);a.push(o.ok?"Pre-flight checks passed. Review the intended effects before running.":`\u274C **Cannot run:**
293
+ ${o.errors.map(i=>` \xB7 ${i}`).join("\n")}`);return text3(a.join("\n\n"))}case"run":{if(!t.id)return fail4("run requires `id` (from get_scenarios).");const o=await s("run",{id:t.id,params:t.params??{}});if(o.preflightErrors?.length){return fail4(`Pre-flight refused to run \`${t.id}\` \u2014 nothing was changed:
294
+ ${o.preflightErrors.map(i=>` \xB7 ${i}`).join("\n")}`)}const a=(o.steps??[]).map(i=>{const c=i.skipped?"\u2013":i.ok?"\u2713":"\u2717";const u=i.error?`
295
+ ${i.error}`:"";return` ${c} ${i.label} (${i.ms}ms)${u}`}).join("\n");if(!o.ok){return fail4(`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
296
+ ${a}
218
297
 
219
- The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`)}return text3([`\u2705 **Scenario active \u2014 ${o.name??t.id}**`,i,`**Reported effects:**
220
- ${(o.effects??[]).map(a=>` \xB7 ${a.label} [${a.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 s("deactivate",{});const i=o.report;const a=[`Removed ${i.removedRules} override rule(s)`,i.stoppedImpersonation?"Ended impersonation":null,i.undoStepsRan?`Ran ${i.undoStepsRan} undo step(s)`:null].filter(Boolean);const c=i.unrestoredState??[];const u=(i.unrestoredStorageKeys.length?`
298
+ The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`)}return text3([`\u2705 **Scenario active \u2014 ${o.name??t.id}**`,a,`**Reported effects:**
299
+ ${(o.effects??[]).map(i=>` \xB7 ${i.label} [${i.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 s("deactivate",{});const a=o.report;const i=[`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 u=(a.unrestoredStorageKeys.length?`
221
300
 
222
- \u26A0\uFE0F **NOT restored:** ${i.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.`:"")+(c.length?`
301
+ \u26A0\uFE0F **NOT restored:** ${a.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.`:"")+(c.length?`
223
302
 
224
- \u26A0\uFE0F **Stores NOT restored:** ${c.join(", ")} \u2014 a state write has no automatic inverse. These stores still hold what the scenario wrote.`:"");const l=i.errors.length?`
303
+ \u26A0\uFE0F **Stores NOT restored:** ${c.join(", ")} \u2014 a state write has no automatic inverse. These stores still hold what the scenario wrote.`:"");const l=a.errors.length?`
225
304
 
226
- Errors: ${i.errors.join("; ")}`:"";return text3(`**Deactivated.**
227
- ${a.map(d=>` \xB7 ${d}`).join("\n")}${u}${l}`)}case"save":{if(!t.scenario)return fail4("save requires `scenario`.");const o=await s("save",{scenario:t.scenario});if(!o.ok){return fail4(`Save rejected:
228
- ${(o.errors??[o.error??"unknown error"]).map(i=>` \xB7 ${i}`).join("\n")}`)}return text3(`Saved \`${o.id}\` **as a draft**.
305
+ Errors: ${a.errors.join("; ")}`:"";return text3(`**Deactivated.**
306
+ ${i.map(h=>` \xB7 ${h}`).join("\n")}${u}${l}`)}case"save":{if(!t.scenario)return fail4("save requires `scenario`.");const o=await s("save",{scenario:t.scenario});if(!o.ok){return fail4(`Save rejected:
307
+ ${(o.errors??[o.error??"unknown error"]).map(a=>` \xB7 ${a}`).join("\n")}`)}return text3(`Saved \`${o.id}\` **as a draft**.
229
308
 
230
- 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 fail4("setFolder requires `id` (from get_scenarios).");const o=await s("setFolder",{id:t.id,folder:t.folder});return text3(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 fail4("delete requires `id`.");await s("delete",{id:t.id});return text3(`Deleted \`${t.id}\` from the device library.`)}}}catch(r){return fail4(errMsg4(r))}})}var import_zod20=require("zod");var import_node_child_process4=require("child_process");var import_node_util4=require("util");var import_node_fs3=require("fs");var import_node_os3=__toESM(require("os"));var import_node_path3=__toESM(require("path"));var import_node_crypto3=require("crypto");var execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process4.execFile);var clamp2=(e,n,t)=>Math.min(Math.max(e,n),t);function humanizeSimctlError(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 pngSize(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,r)=>e[r]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}async function captureIosFramebuffer(){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const e=import_node_path3.default.join(import_node_os3.default.tmpdir(),`buoy-mcp-sim-${(0,import_node_crypto3.randomUUID)()}.png`);try{await execFileAsync4("xcrun",["simctl","io","booted","screenshot","--type=png",e],{timeout:15e3})}catch(n){return{ok:false,error:humanizeSimctlError(n)}}try{const n=await import_node_fs3.promises.readFile(e);const t=pngSize(n);if(!t)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:e,width:t.width,height:t.height}}catch(n){return{ok:false,error:`Failed to read screenshot: ${n.message}`}}}async function cropToBuffer(e,n,t){const r=clamp2(Math.round(t.x),0,Math.max(0,n.width-1));const s=clamp2(Math.round(t.y),0,Math.max(0,n.height-1));const o=clamp2(Math.round(t.width),1,n.width-r);const i=clamp2(Math.round(t.height),1,n.height-s);const a=import_node_path3.default.join(import_node_os3.default.tmpdir(),`buoy-mcp-crop-${(0,import_node_crypto3.randomUUID)()}.png`);try{await execFileAsync4("sips",["-c",String(i),String(o),"--cropOffset",String(s),String(r),e,"--out",a],{timeout:1e4});const c=await import_node_fs3.promises.readFile(a);return{ok:true,buffer:c,width:o,height:i}}catch(c){const u=c;return{ok:false,error:`Crop failed: ${u?.stderr||u?.message||c}`}}finally{await import_node_fs3.promises.unlink(a).catch(()=>{});await import_node_fs3.promises.unlink(e).catch(()=>{})}}function pickDescribeScreenMatch(e,n,t){if(t!=null){const o=e.find(i=>i.nativeTag===t);if(o)return o}const r=(n??"").trim().toLowerCase();if(!r)return void 0;let s;for(const o of e){const i=[o.testID,o.label,o.text,o.name].filter(Boolean).map(u=>u.toLowerCase());let a=0;for(const u of i){if(u===r)a=Math.max(a,100);else if(u.startsWith(r))a=Math.max(a,50);else if(u.includes(r))a=Math.max(a,25)}const c=o.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)a+=20;else if(c)a-=40;if(a<=0)continue;if(!s||a>s.score)s={el:o,score:a}}return s?.el}function frameToRect(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var TOOL_ID="highlight-updates";var MARGIN_PT=12;var CAPTURE_PADDING_PT=6;var fail5=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg5=e=>e instanceof Error?e.message:String(e);function reasonMessage(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 candidatesText(e){if(!e||e.length===0)return"";const n=e.map(t=>{const r=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${r?` (${r})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
309
+ 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 fail4("setFolder requires `id` (from get_scenarios).");const o=await s("setFolder",{id:t.id,folder:t.folder});return text3(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 fail4("delete requires `id`.");await s("delete",{id:t.id});return text3(`Deleted \`${t.id}\` from the device library.`)}}}catch(r){return fail4(errMsg4(r))}})}var import_zod20=require("zod");var import_node_child_process4=require("child_process");var import_node_util4=require("util");var import_node_fs3=require("fs");var import_node_os3=__toESM(require("os"));var import_node_path3=__toESM(require("path"));var import_node_crypto3=require("crypto");var execFileAsync4=(0,import_node_util4.promisify)(import_node_child_process4.execFile);var clamp2=(e,n,t)=>Math.min(Math.max(e,n),t);function humanizeSimctlError(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 pngSize(e){const n=[137,80,78,71,13,10,26,10];if(e.length<24||!n.every((t,r)=>e[r]===t))return null;return{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}}async function captureIosFramebuffer(){if(process.platform!=="darwin"){return{ok:false,error:"Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."}}const e=import_node_path3.default.join(import_node_os3.default.tmpdir(),`buoy-mcp-sim-${(0,import_node_crypto3.randomUUID)()}.png`);try{await execFileAsync4("xcrun",["simctl","io","booted","screenshot","--type=png",e],{timeout:15e3})}catch(n){return{ok:false,error:humanizeSimctlError(n)}}try{const n=await import_node_fs3.promises.readFile(e);const t=pngSize(n);if(!t)return{ok:false,error:"Captured file wasn't a valid PNG."};return{ok:true,srcPath:e,width:t.width,height:t.height}}catch(n){return{ok:false,error:`Failed to read screenshot: ${n.message}`}}}async function cropToBuffer(e,n,t){const r=clamp2(Math.round(t.x),0,Math.max(0,n.width-1));const s=clamp2(Math.round(t.y),0,Math.max(0,n.height-1));const o=clamp2(Math.round(t.width),1,n.width-r);const a=clamp2(Math.round(t.height),1,n.height-s);const i=import_node_path3.default.join(import_node_os3.default.tmpdir(),`buoy-mcp-crop-${(0,import_node_crypto3.randomUUID)()}.png`);try{await execFileAsync4("sips",["-c",String(a),String(o),"--cropOffset",String(s),String(r),e,"--out",i],{timeout:1e4});const c=await import_node_fs3.promises.readFile(i);return{ok:true,buffer:c,width:o,height:a}}catch(c){const u=c;return{ok:false,error:`Crop failed: ${u?.stderr||u?.message||c}`}}finally{await import_node_fs3.promises.unlink(i).catch(()=>{});await import_node_fs3.promises.unlink(e).catch(()=>{})}}function pickDescribeScreenMatch(e,n,t){if(t!=null){const o=e.find(a=>a.nativeTag===t);if(o)return o}const r=(n??"").trim().toLowerCase();if(!r)return void 0;let s;for(const o of e){const a=[o.testID,o.label,o.text,o.name].filter(Boolean).map(u=>u.toLowerCase());let i=0;for(const u of a){if(u===r)i=Math.max(i,100);else if(u.startsWith(r))i=Math.max(i,50);else if(u.includes(r))i=Math.max(i,25)}const c=o.tap;if(c&&c.x>=0&&c.x<=1&&c.y>=0&&c.y<=1)i+=20;else if(c)i-=40;if(i<=0)continue;if(!s||i>s.score)s={el:o,score:i}}return s?.el}function frameToRect(e,n){return{x:e.x*n.width,y:e.y*n.height,width:e.width*n.width,height:e.height*n.height}}var TOOL_ID="highlight-updates";var MARGIN_PT=12;var CAPTURE_PADDING_PT=6;var fail5=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg5=e=>e instanceof Error?e.message:String(e);function reasonMessage(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 candidatesText(e){if(!e||e.length===0)return"";const n=e.map(t=>{const r=t.testID||t.nativeID;return`- ${t.componentName||t.viewType}${r?` (${r})`:""} \u2014 nativeTag ${t.nativeTag}, rendered \xD7${t.renderCount}`});return`
231
310
 
232
311
  Closest matches (retry with a more precise query, or pass nativeTag directly):
233
- ${n.join("\n")}`}function registerScreenshotTools(e,n){e.registerTool("screenshot_component",{description:"Locate a supported React Native component and attempt a cropped iOS Simulator screenshot. Select by query or an exact nativeTag. Uses measured bounds or an available fiber-walk fallback and can attempt scrolling. Inspect the match and crop before treating it as visual evidence. Requires Buoy Pro, a compatible highlight-updates integration, and the MCP server on the same Mac as the booted iOS Simulator.",inputSchema:{query:import_zod20.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:import_zod20.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:import_zod20.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(TOOL_ID,"setSilentTracking",{enabled:true},r).catch(()=>{});const s=await n.callAction(TOOL_ID,"locateComponent",{query:t.query,nativeTag:t.nativeTag,scrollIntoView:t.scrollIntoView??true,margin:MARGIN_PT},r);let o=s;if(!o.matched&&(o.reason==="no-renders"||o.reason==="no-match")){const m=await n.callAction(TOOL_ID,"describeScreen",{},r).catch(()=>null);const g=m?.elements?pickDescribeScreenMatch(m.elements,t.query,t.nativeTag):void 0;if(g?.frame&&m?.screen){o={matched:true,live:true,rect:frameToRect(g.frame,m.screen),screen:m.screen,nativeTag:g.nativeTag??void 0,componentName:g.name,testID:g.testID}}else if(o.reason==="no-renders"){o={matched:false,reason:"no-match",candidates:o.candidates}}}if(!o.matched||!o.rect||!o.screen){return fail5(reasonMessage(o,t.query)+candidatesText(o.candidates))}const i=await captureIosFramebuffer();if(!i.ok||!i.srcPath||!i.width||!i.height){return fail5(i.error??"Screenshot capture failed.")}const a=i.width/o.screen.width;const c=o.rect;const u=CAPTURE_PADDING_PT;const l=await cropToBuffer(i.srcPath,{width:i.width,height:i.height},{x:(c.x-u)*a,y:(c.y-u)*a,width:(c.width+u*2)*a,height:(c.height+u*2)*a});if(!l.ok||!l.buffer){return fail5(l.error??"Crop failed.")}const d=o.componentName||"component";const h=o.testID?` (testID: "${o.testID}")`:"";const y=o.inView===false?"\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge.":"";const v=`Captured "${d}"${h} \u2014 ${l.width}\xD7${l.height}px.${y}`;return{content:[{type:"image",data:l.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:v}]}}catch(r){return fail5(errMsg5(r))}})}var import_zod21=require("zod");var import_node_child_process5=require("child_process");var import_node_util5=require("util");var import_node_fs4=require("fs");var import_node_os4=__toESM(require("os"));var import_node_path4=__toESM(require("path"));var execFileAsync5=(0,import_node_util5.promisify)(import_node_child_process5.execFile);function candidates(){const e=process.env.BUOY_SIMCAM_DIR;const n=[...e?[import_node_path4.default.join(e,"buoycam")]:[],"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",import_node_path4.default.join(import_node_os4.default.homedir(),"Applications/Buoy.app/Contents/Resources/simcam/buoycam")];let t=__dirname;for(let r=0;r<8;r++){n.push(import_node_path4.default.join(t,"apps/desktop/resources/simcam/buoycam"));const s=import_node_path4.default.dirname(t);if(s===t)break;t=s}return n}var resolved=null;function cliPath(){if(resolved)return resolved;resolved=candidates().find(e=>(0,import_node_fs4.existsSync)(e))??null;return resolved}async function run2(e){const n=cliPath();if(!n){return{ok:false,error:"The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."}}try{const{stdout:t}=await execFileAsync5(n,[...e,"--json"],{timeout:3e4});return{ok:true,data:JSON.parse(t)}}catch(t){const r=t.stdout??"";try{return{ok:false,data:JSON.parse(r)}}catch{return{ok:false,error:t.message||"the simulator camera CLI failed"}}}}function describeDiagnosis(e){const n=e.filter(o=>o.level==="fail");const t=e.filter(o=>o.level==="warn");if(n.length===0&&t.length===0){return"The simulator camera is healthy \u2014 every check passed."}const r=[];for(const o of[...n,...t]){r.push(`- **${o.name}** (${o.level}): ${o.detail}`);if(o.fix)r.push(` ${o.fix}`)}const s=n.length>0?`${n.length} problem${n.length===1?"":"s"} found.`:`No hard failures, but ${t.length} thing${t.length===1?"":"s"} worth knowing.`;return[s,"",...r].join("\n")}function describeDevices(e,n){if(e.length===0){return"No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._"}const t=[e.length===1?"1 booted simulator:":`${e.length} booted simulators:`,...e.map(r=>`- **${r.name}** (${r.runtime}) \u2014 \`${r.udid}\``)];if(e.length>1&&n===null){t.push("","_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._");return t.join("\n")}if(n===null){t.push("","_Could not read the installed apps._");return t.join("\n")}if(n.length===0){t.push("","No apps installed on it.");return t.join("\n")}t.push("",`${n.length} installed app${n.length===1?"":"s"}:`,...n.map(r=>`- ${r.name} \u2014 \`${r.bundleId}\``),"","_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._");return t.join("\n")}function describeInputs(e,n){const t=[];t.push(e.length?'**Mac cameras** (for `kind: "camera"`):':"**Mac cameras**: none found.");if(e.length){t.push(...e.map(r=>`- ${r.name} \u2014 \`${r.uniqueId}\``))}else{t.push("_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._")}t.push("");t.push(n.length?'**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):':"**Capturable windows**: none found.");if(n.length){t.push(...n.map(r=>`- ${r.app}${r.title?` \u2014 ${r.title}`:""} \u2014 \`window:${r.id}\``))}else{t.push("_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._")}return t.join("\n")}function entitlementPaths(){const e=process.env.BUOY_ENTITLEMENT_FILE;if(e)return[e];return[import_node_path4.default.join(import_node_os4.default.homedir(),"Library/Application Support/Buoy/entitlement.json"),import_node_path4.default.join(import_node_os4.default.homedir(),"Library/Application Support/buoy-desktop/entitlement.json")]}function readEntitlement(){for(const e of entitlementPaths()){try{if(!(0,import_node_fs4.existsSync)(e))continue;const n=JSON.parse((0,import_node_fs4.readFileSync)(e,"utf8"));return{tier:n.tier==="pro"||n.tier==="free"?n.tier:"locked",isPro:n.isPro===true&&n.hasDesktopAccess===true&&typeof n.validUntil==="number"&&Number.isFinite(n.validUntil)&&Date.now()<n.validUntil&&typeof n.proValidUntil==="number"&&Number.isFinite(n.proValidUntil)&&Date.now()<n.proValidUntil}}catch{}}return{tier:"locked",isPro:false}}function proRequired(e){return`\`${e}\` is a Buoy Pro feature.
234
-
235
- The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel with a Free account. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
312
+ ${n.join("\n")}`}function registerScreenshotTools(e,n){e.registerTool("screenshot_component",{description:"Locate a supported React Native component and attempt a cropped iOS Simulator screenshot. Select by query or an exact nativeTag. Uses measured bounds or an available fiber-walk fallback and can attempt scrolling. Inspect the match and crop before treating it as visual evidence. Requires Buoy Pro, a compatible highlight-updates integration, and the MCP server on the same Mac as the booted iOS Simulator.",inputSchema:{query:import_zod20.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),nativeTag:import_zod20.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),scrollIntoView:import_zod20.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);await n.callAction(TOOL_ID,"setSilentTracking",{enabled:true},r).catch(()=>{});const s=await n.callAction(TOOL_ID,"locateComponent",{query:t.query,nativeTag:t.nativeTag,scrollIntoView:t.scrollIntoView??true,margin:MARGIN_PT},r);let o=s;if(!o.matched&&(o.reason==="no-renders"||o.reason==="no-match")){const g=await n.callAction(TOOL_ID,"describeScreen",{},r).catch(()=>null);const b=g?.elements?pickDescribeScreenMatch(g.elements,t.query,t.nativeTag):void 0;if(b?.frame&&g?.screen){o={matched:true,live:true,rect:frameToRect(b.frame,g.screen),screen:g.screen,nativeTag:b.nativeTag??void 0,componentName:b.name,testID:b.testID}}else if(o.reason==="no-renders"){o={matched:false,reason:"no-match",candidates:o.candidates}}}if(!o.matched||!o.rect||!o.screen){return fail5(reasonMessage(o,t.query)+candidatesText(o.candidates))}const a=await captureIosFramebuffer();if(!a.ok||!a.srcPath||!a.width||!a.height){return fail5(a.error??"Screenshot capture failed.")}const i=a.width/o.screen.width;const c=o.rect;const u=CAPTURE_PADDING_PT;const l=await cropToBuffer(a.srcPath,{width:a.width,height:a.height},{x:(c.x-u)*i,y:(c.y-u)*i,width:(c.width+u*2)*i,height:(c.height+u*2)*i});if(!l.ok||!l.buffer){return fail5(l.error??"Crop failed.")}const h=o.componentName||"component";const d=o.testID?` (testID: "${o.testID}")`:"";const m=o.inView===false?"\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge.":"";const w=`Captured "${h}"${d} \u2014 ${l.width}\xD7${l.height}px.${m}`;return{content:[{type:"image",data:l.buffer.toString("base64"),mimeType:"image/png"},{type:"text",text:w}]}}catch(r){return fail5(errMsg5(r))}})}var import_zod21=require("zod");init_camera();var text4=e=>({content:[{type:"text",text:e}]});var gate=e=>readEntitlement().isPro?null:fail6(proRequired(e));var fail6=e=>({isError:true,content:[{type:"text",text:e}]});function registerCameraTools(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:import_zod21.z.enum(["barcode","image","video","camera","screen","pattern"]).describe("What the camera should show."),value:import_zod21.z.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:import_zod21.z.enum(["qr","pdf417","aztec","code128"]).optional().describe("Barcode symbology; defaults to qr.")}},async n=>{const t=gate("camera_source");if(t)return t;const r=["source",n.kind];if(n.value)r.push(n.value);if(n.format)r.push("--format",n.format);const s=await run2(r);if(!s.ok){return fail6(s.error??s.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 text4(`The simulator camera is now showing ${o}, at ${s.data?.fps??0} fps.
236
313
 
237
- _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`}var text4=e=>({content:[{type:"text",text:e}]});var gate=e=>readEntitlement().isPro?null:fail6(proRequired(e));var fail6=e=>({isError:true,content:[{type:"text",text:e}]});function registerCameraTools(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:import_zod21.z.enum(["barcode","image","video","camera","screen","pattern"]).describe("What the camera should show."),value:import_zod21.z.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:import_zod21.z.enum(["qr","pdf417","aztec","code128"]).optional().describe("Barcode symbology; defaults to qr.")}},async n=>{const t=gate("camera_source");if(t)return t;const r=["source",n.kind];if(n.value)r.push(n.value);if(n.format)r.push("--format",n.format);const s=await run2(r);if(!s.ok){return fail6(s.error??s.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 text4(`The simulator camera is now showing ${o}, at ${s.data?.fps??0} fps.
238
-
239
- _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=gate("camera_status");if(n)return n;const t=await run2(["status"]);if(!t.ok||!t.data){return fail6(t.error??"could not read the simulator camera status")}const{publishing:r,fps:s,codes:o}=t.data;if(!r){return text4("The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._")}return text4([`Publishing at ${s} fps.`,o.length?`Currently decoding: ${o.map(i=>`\`${i}\``).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:import_zod21.z.string().describe("The app's bundle identifier."),udid:import_zod21.z.string().optional().describe("Simulator udid, if more than one is booted.")}},async n=>{const t=gate("camera_launch");if(t)return t;const r=["launch",n.bundleId];if(n.udid)r.push("--udid",n.udid);const s=await run2(r);if(!s.ok){return fail6(s.error??s.data?.error??"could not launch the app")}return text4(`Relaunched \`${n.bundleId}\` with the camera attached${s.data?.pid?` (pid ${s.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:import_zod21.z.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 r=await run2(t);if(!r.data?.checks){return fail6(r.error??"could not run the simulator camera diagnosis")}const s=readEntitlement();const o=s.isPro?"":"\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 text4(describeDiagnosis(r.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=gate("camera_stop");if(n)return n;const t=await run2(["stop"]);if(!t.ok){return fail6(t.error??t.data?.error??"could not stop the camera")}return text4("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:import_zod21.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")}},async n=>{const t=gate("camera_devices");if(t)return t;const r=await run2(["devices"]);if(!r.ok||!r.data){return fail6(r.error??"could not list booted simulators")}const s=r.data.devices??[];let o=null;if(s.length===1||n.udid){const i=await run2(["apps",...n.udid?["--udid",n.udid]:[]]);o=i.ok?i.data?.apps??[]:null}return text4(describeDevices(s,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=gate("camera_inputs");if(n)return n;const[t,r]=await Promise.all([run2(["cameras"]),run2(["windows"])]);if(!t.ok&&!r.ok){return fail6(t.error??r.error??"could not list the Mac's cameras or windows")}return text4(describeInputs(t.data?.cameras??[],r.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:import_zod21.z.enum(["on","off","status"]).describe("Turn it on, off, or report the current state."),udid:import_zod21.z.string().optional().describe("Which simulator. Only needed when several are booted.")}},async n=>{const t=gate("camera_zero_setup");if(t)return t;const r=await run2(["zero-setup",n.action,...n.udid?["--udid",n.udid]:[]]);if(!r.ok){return fail6(r.error??r.data?.error??"could not change zero-setup mode")}const s=r.data??{};if(n.action==="status"){if(!s.enabled)return text4("Zero-setup mode is **off**.");return text4(s.foreign?`Zero-setup is on, but **another tool owns it** (\`${s.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 text4("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 text4("Zero-setup mode is **off**.")})}var import_zod22=require("zod");init_webBench();var text5=e=>({content:[{type:"text",text:e}]});var fail7=e=>({isError:true,content:[{type:"text",text:e}]});var PRO_REQUIRED="`run_web_benchmark` is a Buoy Pro feature. Running benchmarks from an agent needs Pro.\n\n_Tell the user to sign in to Buoy Desktop with a Pro account, or see https://buoy.gg/pricing. Don't try to work around it._";function registerWebBenchTools(e){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:import_zod22.z.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:import_zod22.z.string().optional().describe("Default path for cases, e.g. /bench/lights"),cases:import_zod22.z.array(import_zod22.z.object({name:import_zod22.z.string(),path:import_zod22.z.string().optional(),params:import_zod22.z.record(import_zod22.z.string()).optional()})).min(1).describe("Variants to compare. The first is the baseline."),runsPerCase:import_zod22.z.number().optional().describe("Measured runs per case (1\u201310). Default 3."),perCaseDurationMs:import_zod22.z.number().optional().describe("Recording length per run. Default 5000."),settleMs:import_zod22.z.number().optional().describe("Wait after the page is ready, before recording. Default 1000."),cpuSlowdown:import_zod22.z.array(import_zod22.z.number()).optional().describe("CPU throttling rates; 1 = none. Default [1]."),viewport:import_zod22.z.object({width:import_zod22.z.number(),height:import_zod22.z.number(),deviceScaleFactor:import_zod22.z.number().optional()}).optional().describe("Fixed viewport. Default 1280\xD7800 at 1x."),readySelector:import_zod22.z.string().optional().describe("CSS selector that must appear before a run counts as ready."),traceDir:import_zod22.z.string().optional().describe("Save one Chrome trace per case here (extra, unranked runs)."),headed:import_zod22.z.boolean().optional().describe("Show the browser window. Default false.")}},async n=>{if(!readEntitlement().isPro)return fail7(PRO_REQUIRED);try{const t=await runWebBench({url:n.url,path:n.path,cases:n.cases,runsPerCase:n.runsPerCase,durationMs:n.perCaseDurationMs,settleMs:n.settleMs,cpuSlowdown:n.cpuSlowdown,viewport:n.viewport,readySelector:n.readySelector,traceDir:n.traceDir,headed:n.headed},s=>process.stderr.write(`[buoy-mcp] web-bench: ${s}
240
- `));const r={browser:t.browser,viewport:t.viewport,results:t.results.map(s=>({caseName:s.caseName,cpu:s.cpu,validRuns:s.validRuns,fpsMin:s.fpsMin,fpsMax:s.fpsMax,domGrowth:s.domGrowth,flags:s.flags,median:s.median,failures:s.runs.filter(o=>o.failed).map(o=>o.failed)})),traces:t.traces,warnings:t.warnings};return text5(`${formatWebBenchMarkdown(t)}
314
+ _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=gate("camera_status");if(n)return n;const t=await run2(["status"]);if(!t.ok||!t.data){return fail6(t.error??"could not read the simulator camera status")}const{publishing:r,fps:s,codes:o}=t.data;if(!r){return text4("The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._")}return text4([`Publishing at ${s} 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:import_zod21.z.string().describe("The app's bundle identifier."),udid:import_zod21.z.string().optional().describe("Simulator udid, if more than one is booted.")}},async n=>{const t=gate("camera_launch");if(t)return t;const r=["launch",n.bundleId];if(n.udid)r.push("--udid",n.udid);const s=await run2(r);if(!s.ok){return fail6(s.error??s.data?.error??"could not launch the app")}return text4(`Relaunched \`${n.bundleId}\` with the camera attached${s.data?.pid?` (pid ${s.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:import_zod21.z.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 r=await run2(t);if(!r.data?.checks){return fail6(r.error??"could not run the simulator camera diagnosis")}const s=readEntitlement();const o=s.isPro?"":"\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 text4(describeDiagnosis(r.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=gate("camera_stop");if(n)return n;const t=await run2(["stop"]);if(!t.ok){return fail6(t.error??t.data?.error??"could not stop the camera")}return text4("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:import_zod21.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")}},async n=>{const t=gate("camera_devices");if(t)return t;const r=await run2(["devices"]);if(!r.ok||!r.data){return fail6(r.error??"could not list booted simulators")}const s=r.data.devices??[];let o=null;if(s.length===1||n.udid){const a=await run2(["apps",...n.udid?["--udid",n.udid]:[]]);o=a.ok?a.data?.apps??[]:null}return text4(describeDevices(s,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=gate("camera_inputs");if(n)return n;const[t,r]=await Promise.all([run2(["cameras"]),run2(["windows"])]);if(!t.ok&&!r.ok){return fail6(t.error??r.error??"could not list the Mac's cameras or windows")}return text4(describeInputs(t.data?.cameras??[],r.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:import_zod21.z.enum(["on","off","status"]).describe("Turn it on, off, or report the current state."),udid:import_zod21.z.string().optional().describe("Which simulator. Only needed when several are booted.")}},async n=>{const t=gate("camera_zero_setup");if(t)return t;const r=await run2(["zero-setup",n.action,...n.udid?["--udid",n.udid]:[]]);if(!r.ok){return fail6(r.error??r.data?.error??"could not change zero-setup mode")}const s=r.data??{};if(n.action==="status"){if(!s.enabled)return text4("Zero-setup mode is **off**.");return text4(s.foreign?`Zero-setup is on, but **another tool owns it** (\`${s.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 text4("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 text4("Zero-setup mode is **off**.")})}var import_zod22=require("zod");init_camera();init_webBench();var text5=e=>({content:[{type:"text",text:e}]});var fail7=e=>({isError:true,content:[{type:"text",text:e}]});var PRO_REQUIRED="`run_web_benchmark` is a Buoy Pro feature. Running benchmarks from an agent needs Pro.\n\n_Tell the user to sign in to Buoy Desktop with a Pro account, or open an app signed in to Pro. See https://buoy.gg/pricing. Don't try to work around it._";function hasPro(e){if(readEntitlement().isPro)return true;return e.connectedDevices().some(n=>n.tier==="pro")}function registerWebBenchTools(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:import_zod22.z.string().describe("Origin of the running app, e.g. http://localhost:5173"),path:import_zod22.z.string().optional().describe("Default path for cases, e.g. /bench/lights"),cases:import_zod22.z.array(import_zod22.z.object({name:import_zod22.z.string(),path:import_zod22.z.string().optional(),params:import_zod22.z.record(import_zod22.z.string()).optional(),control:import_zod22.z.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:import_zod22.z.number().optional().describe("Measured runs per case (1\u201310). Default 3."),perCaseDurationMs:import_zod22.z.number().optional().describe("Recording length per run. Default 5000."),settleMs:import_zod22.z.number().optional().describe("Wait after the page is ready, before recording. Default 1000."),cpuSlowdown:import_zod22.z.array(import_zod22.z.number()).optional().describe("CPU throttling rates; 1 = none. Default [1]."),viewport:import_zod22.z.object({width:import_zod22.z.number(),height:import_zod22.z.number(),deviceScaleFactor:import_zod22.z.number().optional()}).optional().describe("Fixed viewport. Default 1280\xD7800 at 1x."),readySelector:import_zod22.z.string().optional().describe("CSS selector that must appear before a run counts as ready."),steps:import_zod22.z.array(import_zod22.z.union([import_zod22.z.object({click:import_zod22.z.string()}),import_zod22.z.object({press:import_zod22.z.string()}),import_zod22.z.object({type:import_zod22.z.object({selector:import_zod22.z.string(),text:import_zod22.z.string()})}),import_zod22.z.object({scroll:import_zod22.z.number()}),import_zod22.z.object({wait:import_zod22.z.number()}),import_zod22.z.object({expect:import_zod22.z.object({selector:import_zod22.z.string(),text:import_zod22.z.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:import_zod22.z.string().optional().describe("Save one Chrome trace per case here (extra, unranked runs)."),headed:import_zod22.z.boolean().optional().describe("Show the browser window. Default false."),allowPageErrors:import_zod22.z.boolean().optional().describe("Keep runs whose page threw an uncaught error. Default false: such runs fail."),projectRoot:import_zod22.z.string().optional().describe("Folder to load Playwright from. Default: the server's working directory."),outFile:import_zod22.z.string().optional().describe("Also save the full result and options as JSON here, to read again later."),extraRoundsWhenClose:import_zod22.z.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:import_zod22.z.number().optional().describe("Stop starting new runs after this long. Default 30 minutes."),budgets:import_zod22.z.object({droppedPct:import_zod22.z.number().optional(),inputMs:import_zod22.z.number().optional(),freezeMs:import_zod22.z.number().optional(),mainThreadPct:import_zod22.z.number().optional(),jsHeapMb:import_zod22.z.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(!hasPro(n))return fail7(PRO_REQUIRED);try{const r=await runWebBench({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},a=>process.stderr.write(`[buoy-mcp] web-bench: ${a}
315
+ `));const s={status:r.status,browser:r.browser,viewport:r.viewport,results:r.results.map(a=>({caseName:a.caseName,cpu:a.cpu,validRuns:a.validRuns,fpsMin:a.fpsMin,fpsMax:a.fpsMax,plannedRuns:a.plannedRuns,isBaseline:a.isBaseline,control:a.control===true,deltaDroppedPct:a.deltaDroppedPct,droppedRange:a.droppedMin==null?null:[a.droppedMin,a.droppedMax],flags:a.flags,budget:a.budget,median:a.median,failures:a.runs.filter(i=>i.failed).map(i=>i.failed)})),traces:r.traces,warnings:r.warnings,replay:r.replay};const o=`${formatWebBenchMarkdown(r)}
241
316
 
242
317
  \`\`\`json
243
- ${JSON.stringify(r,null,2)}
244
- \`\`\``)}catch(t){return fail7(t?.message??String(t))}})}var import_zod23=require("zod");var TOOL_ID2="highlight-updates";var text6=e=>({content:[{type:"text",text:e}]});var fail8=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg6=e=>e instanceof Error?e.message:String(e);function formatElement(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 tapCount=0;var FLOW_TIP_EVERY=3;var FLOW_TIP="\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 registerInteractTools(e,n){e.registerTool("describe_screen",{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. Requires Buoy Pro and the highlight-updates interaction integration.",inputSchema:{interactiveOnly:import_zod23.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),includeBuoy:import_zod23.z.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."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(TOOL_ID2,"describeScreen",{includeBuoy:t.includeBuoy},r);const o=s?.elements??[];const i=t.interactiveOnly?o.filter(u=>u.interactive):o;if(i.length===0){return text6("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 a=s.hiddenBuoy?` Buoy's own overlay (${s.hiddenBuoy} element${s.hiddenBuoy===1?"":"s"}) is left out \u2014 pass includeBuoy:true to see it.`:"";const c=`Screen ${s.screen.width}\xD7${s.screen.height} \u2014 ${i.length} element${i.length===1?"":"s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).${a}`;return text6(`${c}
318
+ ${JSON.stringify(s,null,2)}
319
+ \`\`\``;return r.status==="failed"?fail7(`Every case failed.
320
+
321
+ ${o}`):text5(o)}catch(r){return fail7(r?.message??String(r))}})}var import_zod23=require("zod");var TOOL_ID2="highlight-updates";var text6=e=>({content:[{type:"text",text:e}]});var fail8=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg6=e=>e instanceof Error?e.message:String(e);function formatElement(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 tapCount=0;var FLOW_TIP_EVERY=3;var FLOW_TIP="\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 registerInteractTools(e,n){e.registerTool("describe_screen",{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. Requires Buoy Pro and the highlight-updates interaction integration.",inputSchema:{interactiveOnly:import_zod23.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),includeBuoy:import_zod23.z.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."),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=await n.callAction(TOOL_ID2,"describeScreen",{includeBuoy:t.includeBuoy},r);const o=s?.elements??[];const a=t.interactiveOnly?o.filter(u=>u.interactive):o;if(a.length===0){return text6("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 i=s.hiddenBuoy?` Buoy's own overlay (${s.hiddenBuoy} element${s.hiddenBuoy===1?"":"s"}) is left out \u2014 pass includeBuoy:true to see it.`:"";const c=`Screen ${s.screen.width}\xD7${s.screen.height} \u2014 ${a.length} element${a.length===1?"":"s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).${i}`;return text6(`${c}
245
322
 
246
- ${i.map(formatElement).join("\n")}`)}catch(r){return fail8(errMsg6(r))}});e.registerTool("tap_element",{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. Use describe_screen for current controls and run_flow for sequences. Requires Buoy Pro and the app interaction integration.",inputSchema:{nativeTag:import_zod23.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),testID:import_zod23.z.string().optional().describe("Exact testID of the element."),query:import_zod23.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),value:import_zod23.z.union([import_zod23.z.number(),import_zod23.z.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:import_zod23.z.string().optional().describe("For a text input: the string to set via onChangeText."),longPress:import_zod23.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),scrollIntoView:import_zod23.z.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),includeBuoy:import_zod23.z.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),deviceId:deviceIdInput}},async t=>{try{if(t.nativeTag==null&&!t.testID&&!t.query){return fail8("Provide one of: nativeTag, testID, or query (from describe_screen).")}const r=n.requireProDevice(t.deviceId);const s=await n.callAction(TOOL_ID2,"tapElement",{nativeTag:t.nativeTag,testID:t.testID,query:t.query,value:t.value,text:t.text,longPress:t.longPress,scrollIntoView:t.scrollIntoView,includeBuoy:t.includeBuoy},r);tapCount+=1;const o=tapCount>=FLOW_TIP_EVERY&&tapCount%FLOW_TIP_EVERY===0?FLOW_TIP:"";if(s?.tapped&&s.matched){const a=s.matched;const c=a.text??a.label??a.name;const u=a.testID?` [testID=${a.testID}]`:a.nativeTag!=null?` #${a.nativeTag}`:"";const l=s.scrolled?" (scrolled into view)":"";const d=a.via==="onValueChange"?`Set "${c}"${u} \u2192 ${JSON.stringify(a.value)}`:a.via==="onChangeText"?`Typed ${JSON.stringify(a.value)} into "${c}"${u}`:a.via==="onLongPress"?`Long-pressed "${c}"${u}`:`Tapped "${c}"${u}`;const h=s.effect;const y=!h?"":h.commits>0?` ${h.commits} re-render${h.commits===1?"":"s"} followed.`:` No re-render followed within ${h.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 v=s.alsoMatched?.length?`
323
+ ${a.map(formatElement).join("\n")}`)}catch(r){return fail8(errMsg6(r))}});e.registerTool("tap_element",{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. Use describe_screen for current controls and run_flow for sequences. Requires Buoy Pro and the app interaction integration.",inputSchema:{nativeTag:import_zod23.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),testID:import_zod23.z.string().optional().describe("Exact testID of the element."),query:import_zod23.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),value:import_zod23.z.union([import_zod23.z.number(),import_zod23.z.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:import_zod23.z.string().optional().describe("For a text input: the string to set via onChangeText."),longPress:import_zod23.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),scrollIntoView:import_zod23.z.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),includeBuoy:import_zod23.z.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),deviceId:deviceIdInput}},async t=>{try{if(t.nativeTag==null&&!t.testID&&!t.query){return fail8("Provide one of: nativeTag, testID, or query (from describe_screen).")}const r=n.requireProDevice(t.deviceId);const s=await n.callAction(TOOL_ID2,"tapElement",{nativeTag:t.nativeTag,testID:t.testID,query:t.query,value:t.value,text:t.text,longPress:t.longPress,scrollIntoView:t.scrollIntoView,includeBuoy:t.includeBuoy},r);tapCount+=1;const o=tapCount>=FLOW_TIP_EVERY&&tapCount%FLOW_TIP_EVERY===0?FLOW_TIP:"";if(s?.tapped&&s.matched){const i=s.matched;const c=i.text??i.label??i.name;const u=i.testID?` [testID=${i.testID}]`:i.nativeTag!=null?` #${i.nativeTag}`:"";const l=s.scrolled?" (scrolled into view)":"";const h=i.via==="onValueChange"?`Set "${c}"${u} \u2192 ${JSON.stringify(i.value)}`:i.via==="onChangeText"?`Typed ${JSON.stringify(i.value)} into "${c}"${u}`:i.via==="onLongPress"?`Long-pressed "${c}"${u}`:`Tapped "${c}"${u}`;const d=s.effect;const m=!d?"":d.commits>0?` ${d.commits} re-render${d.commits===1?"":"s"} followed.`:` No re-render followed within ${d.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 w=s.alsoMatched?.length?`
247
324
  Also matched equally well (the choice was a coin toss \u2014 re-tap by #nativeTag if this was wrong):
248
- `+s.alsoMatched.map(m=>`- ${m.name}${m.text?` "${m.text}"`:""}${m.testID?` [testID=${m.testID}]`:""}${m.nativeTag!=null?` #${m.nativeTag}`:""}`).join("\n"):"";return text6(`${d}${l}.${y}${v}${o}`)}let i=s?.reason??"Tap failed.";if(s?.candidates?.length){i+="\n\nTappable candidates:\n"+s.candidates.map(a=>`- ${a.name}${a.text?` "${a.text}"`:""}${a.testID?` [testID=${a.testID}]`:""}${a.nativeTag!=null?` #${a.nativeTag}`:""}`).join("\n")}return fail8(i)}catch(r){return fail8(errMsg6(r))}})}var import_zod24=require("zod");var JS_TOP_TOOL_ID="js-top";var SPARK_CHARS="\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";function sparkline(e){const n=Math.max(1,...e);return e.map(t=>SPARK_CHARS[Math.min(7,Math.floor(t/n*7.999))]).join("")}function formatTopMarkdown(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((r,s)=>{const o=r.freezes>0?`\u26A0 ${r.freezes} (worst ${r.worstFreezeMs}ms)`:"\u2013";t.push(`| ${s+1} | ${r.label} | ${r.api} | ${r.windowMs.toFixed(1)} | ${r.pctOfBusy.toFixed(1)} | ${r.calls} | ${r.avgMs.toFixed(2)} | ${r.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 formatDetailMarkdown(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): \`${sparkline(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 registerJsThreadTools(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 and 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:deviceIdInput,durationMs:import_zod24.z.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),clearFirst:import_zod24.z.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")}},async({deviceId:t,durationMs:r,clearFirst:s})=>{try{const o=n.requireProDevice(t);const i=Math.min(Math.max(r??3e3,250),3e4);const a=await n.callAction(JS_TOP_TOOL_ID,"sample",{durationMs:i,clearFirst:s===true},o);if(!a)return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");const c=formatTopMarkdown(a,i);const u={busyPct:Math.round(a.busyPct*10)/10,freezeSummary:a.freezeSummary,tiers:a.tiers,rows:a.rows.slice(0,15).map(l=>({key:l.key,api:l.api,windowMs:Math.round(l.windowMs*10)/10,pctOfBusy:Math.round(l.pctOfBusy*10)/10,calls:l.calls,avgMs:Math.round(l.avgMs*100)/100,maxMs:Math.round(l.maxMs*10)/10,totalMs:Math.round(l.totalMs),freezes:l.freezes,worstFreezeMs:l.worstFreezeMs}))};return text(`${c}
325
+ `+s.alsoMatched.map(g=>`- ${g.name}${g.text?` "${g.text}"`:""}${g.testID?` [testID=${g.testID}]`:""}${g.nativeTag!=null?` #${g.nativeTag}`:""}`).join("\n"):"";return text6(`${h}${l}.${m}${w}${o}`)}let a=s?.reason??"Tap failed.";if(s?.candidates?.length){a+="\n\nTappable candidates:\n"+s.candidates.map(i=>`- ${i.name}${i.text?` "${i.text}"`:""}${i.testID?` [testID=${i.testID}]`:""}${i.nativeTag!=null?` #${i.nativeTag}`:""}`).join("\n")}return fail8(a)}catch(r){return fail8(errMsg6(r))}})}var import_zod24=require("zod");var JS_TOP_TOOL_ID="js-top";var SPARK_CHARS="\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";function sparkline(e){const n=Math.max(1,...e);return e.map(t=>SPARK_CHARS[Math.min(7,Math.floor(t/n*7.999))]).join("")}function formatTopMarkdown(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((r,s)=>{const o=r.freezes>0?`\u26A0 ${r.freezes} (worst ${r.worstFreezeMs}ms)`:"\u2013";t.push(`| ${s+1} | ${r.label} | ${r.api} | ${r.windowMs.toFixed(1)} | ${r.pctOfBusy.toFixed(1)} | ${r.calls} | ${r.avgMs.toFixed(2)} | ${r.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 formatDetailMarkdown(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): \`${sparkline(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 registerJsThreadTools(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 and 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:deviceIdInput,durationMs:import_zod24.z.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),clearFirst:import_zod24.z.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")}},async({deviceId:t,durationMs:r,clearFirst:s})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(r??3e3,250),3e4);const i=await n.callAction(JS_TOP_TOOL_ID,"sample",{durationMs:a,clearFirst:s===true},o);if(!i)return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");const c=formatTopMarkdown(i,a);const u={busyPct:Math.round(i.busyPct*10)/10,freezeSummary:i.freezeSummary,tiers:i.tiers,rows:i.rows.slice(0,15).map(l=>({key:l.key,api:l.api,windowMs:Math.round(l.windowMs*10)/10,pctOfBusy:Math.round(l.pctOfBusy*10)/10,calls:l.calls,avgMs:Math.round(l.avgMs*100)/100,maxMs:Math.round(l.maxMs*10)/10,totalMs:Math.round(l.totalMs),freezes:l.freezes,worstFreezeMs:l.worstFreezeMs}))};return text(`${c}
249
326
 
250
327
  \`\`\`json
251
328
  ${JSON.stringify(u)}
252
- \`\`\``)}catch(o){return fail2(errMsg2(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:deviceIdInput,key:import_zod24.z.string().describe("Origin key from get_js_thread_top's JSON rows.")}},async({deviceId:t,key:r})=>{try{const s=n.requireProDevice(t);const o=await n.callAction(JS_TOP_TOOL_ID,"getOriginDetail",{key:r},s);if(!o){return fail2(`Origin "${r}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`)}const i=formatDetailMarkdown(o);return text(`${i}
329
+ \`\`\``)}catch(o){return fail2(errMsg2(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:deviceIdInput,key:import_zod24.z.string().describe("Origin key from get_js_thread_top's JSON rows.")}},async({deviceId:t,key:r})=>{try{const s=n.requireProDevice(t);const o=await n.callAction(JS_TOP_TOOL_ID,"getOriginDetail",{key:r},s);if(!o){return fail2(`Origin "${r}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`)}const a=formatDetailMarkdown(o);return text(`${a}
253
330
 
254
331
  \`\`\`json
255
332
  ${JSON.stringify(o)}
256
- \`\`\``)}catch(s){return fail2(errMsg2(s))}});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:deviceIdInput}},async({deviceId:t})=>{try{const r=n.requireProDevice(t);await n.callAction(JS_TOP_TOOL_ID,"clear",{},r);return text("Cleared js-top stats.")}catch(r){return fail2(errMsg2(r))}})}var import_zod25=require("zod");var IMAGES_TOOL_ID="images";function uriTail(e){if(!e)return"(no source)";const n=e.match(/[?&]unstable_path=([^&?]+)/);const t=n?decodeURIComponent(n[1]):e;const r=t.split("?")[0]??t;const s=r.split("/").filter(Boolean);return s[s.length-1]??t}function verdictLabel(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 cacheLabel(e){if(e.cache)return e.cache;if(e.progressSeen)return"network";return"?"}function mb(e){return`${(e/(1024*1024)).toFixed(1)}MB`}function formatListMarkdown(e,n){const t=[];const r=e.stats;t.push(`## Images registry (${e.total} records, showing ${Math.min(n,e.records.length)})`);t.push("");t.push(`${r.loaded} loaded \xB7 ${r.errors} failed \xB7 ${r.loading} in flight \xB7 \u2248${mb(r.estDecodedBytes)} decoded`+(r.estWastedBytes>0?` \xB7 **${mb(r.estWastedBytes)} wasted on oversized sources**`:""));const s=e.insights;if(s){const i=[];if(s.duplicates.length>0)i.push(`${s.duplicates.length} duplicate URL(s): ${s.duplicates.slice(0,3).map(a=>`${uriTail(a.uri)}\xD7${a.count}`).join(", ")}`);if(s.retryStorms.length>0)i.push(`${s.retryStorms.length} retry storm(s): ${s.retryStorms.slice(0,3).map(a=>`#${a.id}(${a.loadCount} loads)`).join(", ")}`);if(s.queueSaturated)i.push("iOS image queue saturated (>4 concurrent loads)");if(s.missingAlt>0)i.push(`${s.missingAlt} image(s) missing alt/accessibilityLabel`);if(s.layoutShifters.length>0)i.push(`${s.layoutShifters.length} layout-shifting image(s): ${s.layoutShifters.slice(0,3).map(a=>`#${a.id}`).join(", ")} \u2014 reserve explicit dimensions`);if(i.length>0){t.push("");t.push(`**Insights:** ${i.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 i of e.records){const a=i.intrinsic?`${i.intrinsic.width}\xD7${i.intrinsic.height}`:"\u2013";const c=i.layout?`${i.layout.width}\xD7${i.layout.height}`:"\u2013";const u=i.status==="error"?`**error**${i.errorCode?` (${i.errorCode})`:""}`:i.status;t.push(`| ${i.id} | ${uriTail(i.uri)} | ${i.lib} | ${u}${i.mounted?"":" (unmounted)"} | ${cacheLabel(i)} | ${i.ms??"\u2013"} | ${a} | ${c} | ${verdictLabel(i)} | ${i.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 ACTION_MAP={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 registerImagesTools(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:deviceIdInput,limit:import_zod25.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),status:import_zod25.z.enum(["pending","loading","loaded","error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")}},async({deviceId:t,limit:r,status:s})=>{try{const o=n.requireProDevice(t);const i=Math.min(Math.max(r??30,1),200);const a=await n.callAction(IMAGES_TOOL_ID,"list",{limit:i,status:s},o);if(!a)return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");const c=formatListMarkdown(a,i);const u={stats:a.stats,insights:a.insights,captureStatus:a.captureStatus,globalModes:a.globalModes,records:a.records.map(l=>({id:l.id,uri:l.uri,lib:l.lib,status:l.status,mounted:l.mounted,cache:l.cache,ms:l.ms,intrinsic:l.intrinsic,layout:l.layout,oversizeFactor:l.oversizeFactor,wastedKB:l.wastedKB,error:l.error,errorCode:l.errorCode,overrideLabel:l.overrideLabel,loadCount:l.loadCount,hasAltText:l.hasAltText,layoutShifts:l.layoutShifts}))};return text(`${c}
333
+ \`\`\``)}catch(s){return fail2(errMsg2(s))}});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:deviceIdInput}},async({deviceId:t})=>{try{const r=n.requireProDevice(t);await n.callAction(JS_TOP_TOOL_ID,"clear",{},r);return text("Cleared js-top stats.")}catch(r){return fail2(errMsg2(r))}})}var import_zod25=require("zod");var IMAGES_TOOL_ID="images";function uriTail(e){if(!e)return"(no source)";const n=e.match(/[?&]unstable_path=([^&?]+)/);const t=n?decodeURIComponent(n[1]):e;const r=t.split("?")[0]??t;const s=r.split("/").filter(Boolean);return s[s.length-1]??t}function verdictLabel(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 cacheLabel(e){if(e.cache)return e.cache;if(e.progressSeen)return"network";return"?"}function mb(e){return`${(e/(1024*1024)).toFixed(1)}MB`}function formatListMarkdown(e,n){const t=[];const r=e.stats;t.push(`## Images registry (${e.total} records, showing ${Math.min(n,e.records.length)})`);t.push("");t.push(`${r.loaded} loaded \xB7 ${r.errors} failed \xB7 ${r.loading} in flight \xB7 \u2248${mb(r.estDecodedBytes)} decoded`+(r.estWastedBytes>0?` \xB7 **${mb(r.estWastedBytes)} wasted on oversized sources**`:""));const s=e.insights;if(s){const a=[];if(s.duplicates.length>0)a.push(`${s.duplicates.length} duplicate URL(s): ${s.duplicates.slice(0,3).map(i=>`${uriTail(i.uri)}\xD7${i.count}`).join(", ")}`);if(s.retryStorms.length>0)a.push(`${s.retryStorms.length} retry storm(s): ${s.retryStorms.slice(0,3).map(i=>`#${i.id}(${i.loadCount} loads)`).join(", ")}`);if(s.queueSaturated)a.push("iOS image queue saturated (>4 concurrent loads)");if(s.missingAlt>0)a.push(`${s.missingAlt} image(s) missing alt/accessibilityLabel`);if(s.layoutShifters.length>0)a.push(`${s.layoutShifters.length} layout-shifting image(s): ${s.layoutShifters.slice(0,3).map(i=>`#${i.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 i=a.intrinsic?`${a.intrinsic.width}\xD7${a.intrinsic.height}`:"\u2013";const c=a.layout?`${a.layout.width}\xD7${a.layout.height}`:"\u2013";const u=a.status==="error"?`**error**${a.errorCode?` (${a.errorCode})`:""}`:a.status;t.push(`| ${a.id} | ${uriTail(a.uri)} | ${a.lib} | ${u}${a.mounted?"":" (unmounted)"} | ${cacheLabel(a)} | ${a.ms??"\u2013"} | ${i} | ${c} | ${verdictLabel(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 ACTION_MAP={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 registerImagesTools(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:deviceIdInput,limit:import_zod25.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),status:import_zod25.z.enum(["pending","loading","loaded","error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")}},async({deviceId:t,limit:r,status:s})=>{try{const o=n.requireProDevice(t);const a=Math.min(Math.max(r??30,1),200);const i=await n.callAction(IMAGES_TOOL_ID,"list",{limit:a,status:s},o);if(!i)return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");const c=formatListMarkdown(i,a);const u={stats:i.stats,insights:i.insights,captureStatus:i.captureStatus,globalModes:i.globalModes,records:i.records.map(l=>({id:l.id,uri:l.uri,lib:l.lib,status:l.status,mounted:l.mounted,cache:l.cache,ms:l.ms,intrinsic:l.intrinsic,layout:l.layout,oversizeFactor:l.oversizeFactor,wastedKB:l.wastedKB,error:l.error,errorCode:l.errorCode,overrideLabel:l.overrideLabel,loadCount:l.loadCount,hasAltText:l.hasAltText,layoutShifts:l.layoutShifts}))};return text(`${c}
257
334
 
258
335
  \`\`\`json
259
336
  ${JSON.stringify(u)}
260
- \`\`\``)}catch(o){return fail2(errMsg2(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:deviceIdInput,id:import_zod25.z.number().describe("Record id from get_images."),action:import_zod25.z.enum(["hard_reload","retry","evict_disk","flash","prove_savings","force_error","force_loading","force_blank","swap_url","restore"]).describe("What to do."),url:import_zod25.z.string().optional().describe("Replacement URL (swap_url only).")}},async({deviceId:t,id:r,action:s,url:o})=>{try{const i=n.requireProDevice(t);if(s==="prove_savings"){const h=await n.callAction(IMAGES_TOOL_ID,"proveSavings",{id:r},i);if(!h)return fail2("Device returned nothing for proveSavings.");if(!h.ok)return fail2(h.message);const y=[`**prove_savings** on #${r}: ${h.message}`];if(h.originalBytes!=null)y.push(`Original: ${Math.round(h.originalBytes/1024)}KB (${h.originalBytesSource})`);if(h.optimizedBytes!=null)y.push(`Optimized: ${Math.round(h.optimizedBytes/1024)}KB \u2014 ${h.optimizedDims?.width}\xD7${h.optimizedDims?.height} WebP`);if(h.savedBytes!=null)y.push(`**Saved: ${Math.round(h.savedBytes/1024)}KB (${h.savedPct}%)**`);if(h.optimizedUri)y.push(`Preview in place: image_action {id:${r}, action:"swap_url", url:"${h.optimizedUri}"}`);return text(y.join("\n"))}const a=ACTION_MAP[s];if(!a)return fail2(`Unknown action: ${s}`);if(s==="swap_url"&&!o)return fail2("swap_url requires the `url` param.");const c=await n.callAction(IMAGES_TOOL_ID,a.action,{id:r,...a.params?.(o)??{}},i);const u=s==="flash"?0:1500;if(u>0)await new Promise(h=>setTimeout(h,u));const l=await n.callAction(IMAGES_TOOL_ID,"getDetail",{id:r},i).catch(()=>null);const d=[`**${s}** on #${r}: ${c?.message??(c?.ok===false?"failed":"done")}`];if(l){d.push("");d.push(`Now: status=${l.status}${l.errorCode?` (${l.errorCode})`:""} \xB7 cache=${cacheLabel(l)} \xB7 ms=${l.ms??"\u2013"} \xB7 loads=${l.loadCount}${l.overrideLabel?` \xB7 sim="${l.overrideLabel}"`:""}${l.mounted?"":" \xB7 UNMOUNTED"}`);if(l.error)d.push(`Error: ${l.error}`)}return text(d.join("\n"))}catch(i){return fail2(errMsg2(i))}});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:deviceIdInput,network:import_zod25.z.enum(["normal","offline","cold"]).optional().describe("Network simulation mode."),blankImages:import_zod25.z.boolean().optional().describe("Render integrated images without a source."),massAction:import_zod25.z.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:import_zod25.z.boolean().optional().describe("Clear the captured image log."),clearExpoCaches:import_zod25.z.boolean().optional().describe("Clear expo-image memory + disk caches.")}},async({deviceId:t,network:r,blankImages:s,massAction:o,clearRegistry:i,clearExpoCaches:a})=>{try{const c=n.requireProDevice(t);const u=[];if(o){const l=await n.callAction(IMAGES_TOOL_ID,"massAction",{kind:o},c);u.push(l?.message??`mass ${o} applied`)}if(r){await n.callAction(IMAGES_TOOL_ID,"setNetworkMode",{mode:r},c);u.push(`network=${r}`)}if(s!=null){await n.callAction(IMAGES_TOOL_ID,"setBlankImages",{enabled:s},c);u.push(`blankImages=${s}`)}if(i){await n.callAction(IMAGES_TOOL_ID,"clearRecords",{},c);u.push("registry cleared")}if(a){const l=await n.callAction(IMAGES_TOOL_ID,"clearExpoCaches",{},c);u.push(l?.message??"expo caches cleared")}if(u.length===0)return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");return text(`Applied: ${u.join(" \xB7 ")}`)}catch(c){return fail2(errMsg2(c))}})}var import_zod26=require("zod");function graphUnavailableNote(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 ASSETS_TOOL_ID="assets";function kb2(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 sizeLabel(e){if(e.sizeBytes==null)return"\u2013";return e.sizeSource==="measured"?kb2(e.sizeBytes):`~${kb2(e.sizeBytes)}`}function scalesLabel(e){if(!e.length)return"\u2013";return e.map(n=>`@${n}x`).join(" ")}function flagsLabel(e,n){const t=[];if(!e.loaded)t.push("UNUSED");if(n.overDecode.includes(e.id))t.push("OVERSIZE");if(n.duplicates.some(r=>r.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 formatListMarkdown2(e,n){const t=[];const r=e.stats;const s=e.scanStatus;t.push(`## Asset inventory (${e.total} assets, showing ${Math.min(n,e.records.length)}, largest first)`);t.push("");const o=Object.entries(r.byKind).filter(([,l])=>l.count>0).map(([l,d])=>`${d.count} ${l}${d.bytes>0?` (${kb2(d.bytes)})`:""}`);t.push(`${r.loaded} loaded \xB7 ${r.neverLoaded} never loaded \xB7 `+(r.totalBytes>0?`${kb2(r.totalBytes)} measured (${r.measuredCount}/${r.total})`:"sizes not measured")+` \xB7 ${o.join(" \xB7 ")}`);if(s.fontFamilies.length>0){t.push(`Loaded font families: ${s.fontFamilies.join(", ")}`)}const i=graphUnavailableNote(s);if(i)t.push(i);if(s.measuring)t.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");const a=e.insights;const c=[];if(a.neverLoaded.length)c.push(`${a.neverLoaded.length} never loaded`);if(a.duplicates.length){const l=a.duplicates.reduce((d,h)=>d+(h.wastedBytes??0),0);c.push(`${a.duplicates.length} duplicate group(s)${l>0?` (${kb2(l)} wasted)`:""}`)}if(a.overDecode.length)c.push(`${a.overDecode.length} decode larger than 2\xD7 the screen`);if(a.webpCandidates.length)c.push(`${a.webpCandidates.length} WebP candidate(s)`);if(a.heavyGifs.length)c.push(`${a.heavyGifs.length} heavy GIF(s)`);if(a.heavyFonts.length)c.push(`${a.heavyFonts.length} heavy font(s)`);if(a.scaleGaps.length)c.push(`${a.scaleGaps.length} scale gap(s)`);if(a.scaleAnomalies.length)c.push(`${a.scaleAnomalies.length} scale anomaly(ies)`);if(c.length){t.push("");t.push(`**Findings:** ${c.join(" \xB7 ")}`)}const u=e.diff;if(u){const l=u.netBytes==null?"net unknown":Math.abs(u.netBytes)<1024?"\xB10 net":`${u.netBytes>=0?"+":"\u2212"}${kb2(Math.abs(u.netBytes))} net`;t.push("");t.push(`**Since baseline** (${new Date(u.baselineSavedAt).toISOString()}): ${u.added.length} added \xB7 ${u.removed.length} removed \xB7 ${u.grown.length} grew \xB7 ${l}`+(u.removed.length?` \u2014 removed: ${u.removed.slice(0,5).map(d=>`${d.name}.${d.type}`).join(", ")}${u.removed.length>5?"\u2026":""}`:""))}t.push("");t.push("| id | asset | kind | dims | scales | size | flags |");t.push("|---|---|---|---|---|---|---|");for(const l of e.records){const d=l.width!=null&&l.height!=null?`${Math.round(l.width)}\xD7${Math.round(l.height)}`:"\u2013";t.push(`| ${l.id} | ${l.name}.${l.type} | ${l.kind} | ${d} | ${scalesLabel(l.scales)} | ${sizeLabel(l)} | ${flagsLabel(l,a)} |`)}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 registerAssetsTools(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:deviceIdInput,limit:import_zod26.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),kind:import_zod26.z.enum(["image","font","video","audio","data","other"]).optional().describe("Only assets of this kind."),unusedOnly:import_zod26.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")}},async({deviceId:t,limit:r,kind:s,unusedOnly:o})=>{try{const i=n.requireProDevice(t);const a=Math.min(Math.max(r??40,1),500);const c=await n.callAction(ASSETS_TOOL_ID,"list",{limit:o?500:a,kind:s,loadedOnly:false},i);if(!c)return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");if(o){c.records=c.records.filter(d=>!d.loaded).slice(0,a)}const u=formatListMarkdown2(c,a);const l={stats:c.stats,scanStatus:c.scanStatus,insights:c.insights,diff:c.diff,records:c.records.map(d=>({id:d.id,name:d.name,type:d.type,kind:d.kind,hash:d.hash,width:d.width,height:d.height,scales:d.scales,loaded:d.loaded,sizeBytes:d.sizeBytes,sizeSource:d.sizeSource}))};return text(`${u}
337
+ \`\`\``)}catch(o){return fail2(errMsg2(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:deviceIdInput,id:import_zod25.z.number().describe("Record id from get_images."),action:import_zod25.z.enum(["hard_reload","retry","evict_disk","flash","prove_savings","force_error","force_loading","force_blank","swap_url","restore"]).describe("What to do."),url:import_zod25.z.string().optional().describe("Replacement URL (swap_url only).")}},async({deviceId:t,id:r,action:s,url:o})=>{try{const a=n.requireProDevice(t);if(s==="prove_savings"){const d=await n.callAction(IMAGES_TOOL_ID,"proveSavings",{id:r},a);if(!d)return fail2("Device returned nothing for proveSavings.");if(!d.ok)return fail2(d.message);const m=[`**prove_savings** on #${r}: ${d.message}`];if(d.originalBytes!=null)m.push(`Original: ${Math.round(d.originalBytes/1024)}KB (${d.originalBytesSource})`);if(d.optimizedBytes!=null)m.push(`Optimized: ${Math.round(d.optimizedBytes/1024)}KB \u2014 ${d.optimizedDims?.width}\xD7${d.optimizedDims?.height} WebP`);if(d.savedBytes!=null)m.push(`**Saved: ${Math.round(d.savedBytes/1024)}KB (${d.savedPct}%)**`);if(d.optimizedUri)m.push(`Preview in place: image_action {id:${r}, action:"swap_url", url:"${d.optimizedUri}"}`);return text(m.join("\n"))}const i=ACTION_MAP[s];if(!i)return fail2(`Unknown action: ${s}`);if(s==="swap_url"&&!o)return fail2("swap_url requires the `url` param.");const c=await n.callAction(IMAGES_TOOL_ID,i.action,{id:r,...i.params?.(o)??{}},a);const u=s==="flash"?0:1500;if(u>0)await new Promise(d=>setTimeout(d,u));const l=await n.callAction(IMAGES_TOOL_ID,"getDetail",{id:r},a).catch(()=>null);const h=[`**${s}** on #${r}: ${c?.message??(c?.ok===false?"failed":"done")}`];if(l){h.push("");h.push(`Now: status=${l.status}${l.errorCode?` (${l.errorCode})`:""} \xB7 cache=${cacheLabel(l)} \xB7 ms=${l.ms??"\u2013"} \xB7 loads=${l.loadCount}${l.overrideLabel?` \xB7 sim="${l.overrideLabel}"`:""}${l.mounted?"":" \xB7 UNMOUNTED"}`);if(l.error)h.push(`Error: ${l.error}`)}return text(h.join("\n"))}catch(a){return fail2(errMsg2(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:deviceIdInput,network:import_zod25.z.enum(["normal","offline","cold"]).optional().describe("Network simulation mode."),blankImages:import_zod25.z.boolean().optional().describe("Render integrated images without a source."),massAction:import_zod25.z.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:import_zod25.z.boolean().optional().describe("Clear the captured image log."),clearExpoCaches:import_zod25.z.boolean().optional().describe("Clear expo-image memory + disk caches.")}},async({deviceId:t,network:r,blankImages:s,massAction:o,clearRegistry:a,clearExpoCaches:i})=>{try{const c=n.requireProDevice(t);const u=[];if(o){const l=await n.callAction(IMAGES_TOOL_ID,"massAction",{kind:o},c);u.push(l?.message??`mass ${o} applied`)}if(r){await n.callAction(IMAGES_TOOL_ID,"setNetworkMode",{mode:r},c);u.push(`network=${r}`)}if(s!=null){await n.callAction(IMAGES_TOOL_ID,"setBlankImages",{enabled:s},c);u.push(`blankImages=${s}`)}if(a){await n.callAction(IMAGES_TOOL_ID,"clearRecords",{},c);u.push("registry cleared")}if(i){const l=await n.callAction(IMAGES_TOOL_ID,"clearExpoCaches",{},c);u.push(l?.message??"expo caches cleared")}if(u.length===0)return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");return text(`Applied: ${u.join(" \xB7 ")}`)}catch(c){return fail2(errMsg2(c))}})}var import_zod26=require("zod");function graphUnavailableNote(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 ASSETS_TOOL_ID="assets";function kb2(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 sizeLabel(e){if(e.sizeBytes==null)return"\u2013";return e.sizeSource==="measured"?kb2(e.sizeBytes):`~${kb2(e.sizeBytes)}`}function scalesLabel(e){if(!e.length)return"\u2013";return e.map(n=>`@${n}x`).join(" ")}function flagsLabel(e,n){const t=[];if(!e.loaded)t.push("UNUSED");if(n.overDecode.includes(e.id))t.push("OVERSIZE");if(n.duplicates.some(r=>r.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 formatListMarkdown2(e,n){const t=[];const r=e.stats;const s=e.scanStatus;t.push(`## Asset inventory (${e.total} assets, showing ${Math.min(n,e.records.length)}, largest first)`);t.push("");const o=Object.entries(r.byKind).filter(([,l])=>l.count>0).map(([l,h])=>`${h.count} ${l}${h.bytes>0?` (${kb2(h.bytes)})`:""}`);t.push(`${r.loaded} loaded \xB7 ${r.neverLoaded} never loaded \xB7 `+(r.totalBytes>0?`${kb2(r.totalBytes)} measured (${r.measuredCount}/${r.total})`:"sizes not measured")+` \xB7 ${o.join(" \xB7 ")}`);if(s.fontFamilies.length>0){t.push(`Loaded font families: ${s.fontFamilies.join(", ")}`)}const a=graphUnavailableNote(s);if(a)t.push(a);if(s.measuring)t.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");const i=e.insights;const c=[];if(i.neverLoaded.length)c.push(`${i.neverLoaded.length} never loaded`);if(i.duplicates.length){const l=i.duplicates.reduce((h,d)=>h+(d.wastedBytes??0),0);c.push(`${i.duplicates.length} duplicate group(s)${l>0?` (${kb2(l)} wasted)`:""}`)}if(i.overDecode.length)c.push(`${i.overDecode.length} decode larger than 2\xD7 the screen`);if(i.webpCandidates.length)c.push(`${i.webpCandidates.length} WebP candidate(s)`);if(i.heavyGifs.length)c.push(`${i.heavyGifs.length} heavy GIF(s)`);if(i.heavyFonts.length)c.push(`${i.heavyFonts.length} heavy font(s)`);if(i.scaleGaps.length)c.push(`${i.scaleGaps.length} scale gap(s)`);if(i.scaleAnomalies.length)c.push(`${i.scaleAnomalies.length} scale anomaly(ies)`);if(c.length){t.push("");t.push(`**Findings:** ${c.join(" \xB7 ")}`)}const u=e.diff;if(u){const l=u.netBytes==null?"net unknown":Math.abs(u.netBytes)<1024?"\xB10 net":`${u.netBytes>=0?"+":"\u2212"}${kb2(Math.abs(u.netBytes))} net`;t.push("");t.push(`**Since baseline** (${new Date(u.baselineSavedAt).toISOString()}): ${u.added.length} added \xB7 ${u.removed.length} removed \xB7 ${u.grown.length} grew \xB7 ${l}`+(u.removed.length?` \u2014 removed: ${u.removed.slice(0,5).map(h=>`${h.name}.${h.type}`).join(", ")}${u.removed.length>5?"\u2026":""}`:""))}t.push("");t.push("| id | asset | kind | dims | scales | size | flags |");t.push("|---|---|---|---|---|---|---|");for(const l of e.records){const h=l.width!=null&&l.height!=null?`${Math.round(l.width)}\xD7${Math.round(l.height)}`:"\u2013";t.push(`| ${l.id} | ${l.name}.${l.type} | ${l.kind} | ${h} | ${scalesLabel(l.scales)} | ${sizeLabel(l)} | ${flagsLabel(l,i)} |`)}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 registerAssetsTools(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:deviceIdInput,limit:import_zod26.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),kind:import_zod26.z.enum(["image","font","video","audio","data","other"]).optional().describe("Only assets of this kind."),unusedOnly:import_zod26.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")}},async({deviceId:t,limit:r,kind:s,unusedOnly:o})=>{try{const a=n.requireProDevice(t);const i=Math.min(Math.max(r??40,1),500);const c=await n.callAction(ASSETS_TOOL_ID,"list",{limit:o?500:i,kind:s,loadedOnly:false},a);if(!c)return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");if(o){c.records=c.records.filter(h=>!h.loaded).slice(0,i)}const u=formatListMarkdown2(c,i);const l={stats:c.stats,scanStatus:c.scanStatus,insights:c.insights,diff:c.diff,records:c.records.map(h=>({id:h.id,name:h.name,type:h.type,kind:h.kind,hash:h.hash,width:h.width,height:h.height,scales:h.scales,loaded:h.loaded,sizeBytes:h.sizeBytes,sizeSource:h.sizeSource}))};return text(`${u}
261
338
 
262
339
  \`\`\`json
263
340
  ${JSON.stringify(l)}
264
- \`\`\``)}catch(i){return fail2(errMsg2(i))}});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:deviceIdInput,action:import_zod26.z.enum(["rescan","measure_sizes","detail","save_baseline","clear_baseline","clear_records"]).describe("What to do."),id:import_zod26.z.number().optional().describe("Asset id from get_assets (detail only).")}},async({deviceId:t,action:r,id:s})=>{try{const o=n.requireProDevice(t);switch(r){case"rescan":{const i=await n.callAction(ASSETS_TOOL_ID,"rescan",{},o);const a=i?.scanStatus;if(!a)return fail2("Device returned nothing for rescan.");return text(`Rescan done: ${a.registryCount} registered at runtime`+(a.graphCount!=null?` \xB7 ${a.graphCount} in the bundle graph`:" \xB7 no bundle graph")+(a.graphError?` (graph: ${a.graphError})`:"")+` \xB7 ${a.fontFamilies.length} font families`+(a.measuring?" \xB7 size measurement running \u2014 get_assets in a moment":""))}case"measure_sizes":{const i=await n.callAction(ASSETS_TOOL_ID,"measureSizes",{},o);return text(`Measured ${i?.measured??0} of ${i?.total??0} assets.`)}case"detail":{if(s==null)return fail2("detail requires the `id` param (from get_assets).");const i=await n.callAction(ASSETS_TOOL_ID,"getDetail",{id:s},o);if(!i)return fail2(`Asset ${s} not found \u2014 re-run get_assets for current ids.`);const a=[`## ${i.name}.${i.type} (#${i.id})`,"",`- Kind: ${i.kind} \xB7 ${i.loaded?"loaded at runtime":"in bundle, NEVER loaded"} (${i.origin})`];if(i.width!=null)a.push(`- Dimensions: ${i.width}\xD7${i.height} logical \xB7 scales ${scalesLabel(i.scales)}`);if(i.sizesByScale){for(const[c,u]of Object.entries(i.sizesByScale)){a.push(`- Size @${c}x: ${kb2(u)}`)}}if(i.sizeBytes!=null)a.push(`- Total: ${sizeLabel(i)}`);if(i.estDecodedBytes)a.push(`- Decoded memory: ~${kb2(i.estDecodedBytes)} (RGBA)`);if(i.hash)a.push(`- Hash: ${i.hash}`);if(i.fileSystemLocation)a.push(`- Source dir: ${i.fileSystemLocation}`);if(i.files?.length)a.push(`- Files: ${i.files.join(", ")}`);if(i.uri)a.push(`- Resolved URI: ${i.uri}`);return text(a.join("\n"))}case"save_baseline":{const i=await n.callAction(ASSETS_TOOL_ID,"saveBaseline",{},o);return text(`Baseline saved (${i?.assets??0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`)}case"clear_baseline":{await n.callAction(ASSETS_TOOL_ID,"clearBaseline",{},o);return text("Baseline cleared.")}case"clear_records":{const i=await n.callAction(ASSETS_TOOL_ID,"clearRecords",{},o);return text(i?.message??"Inventory cleared.")}}}catch(o){return fail2(errMsg2(o))}})}var import_zod27=require("zod");var import_ws=require("ws");var CONNECT_TIMEOUT_MS=3e3;function messageSocketUrl(e){return`${e.replace(/^http/,"ws").replace(/\/+$/,"")}/message`}function reloadViaDevServer(e){return new Promise(n=>{let t=false;const r=a=>{if(t)return;t=true;n(a)};let s;try{s=new import_ws.WebSocket(messageSocketUrl(e))}catch(a){r({ok:false,error:a.message});return}const o=setTimeout(()=>{try{s.close()}catch{}r({ok:false,error:`dev server at ${e} did not accept a connection`})},CONNECT_TIMEOUT_MS);let i=false;s.on("open",()=>{try{s.send(JSON.stringify({version:2,method:"reload"}));i=true;clearTimeout(o);setTimeout(()=>{try{s.close()}catch{}r({ok:true})},250)}catch(a){clearTimeout(o);r({ok:false,error:a.message})}});s.on("error",a=>{clearTimeout(o);if(i)return;r({ok:false,error:a.message})})})}var APP_TOOL_ID="app";var RELOAD_ACTION="reloadApp";var DEFAULT_WAIT_MS=3e4;var TEARDOWN_GRACE_MS=1200;var PROBE_TIMEOUT_MS=1500;var PROBE_INTERVAL_MS=400;var delay3=e=>new Promise(n=>setTimeout(n,e));async function ping(e,n,t){try{const r=await e.callAction(APP_TOOL_ID,"ping",{},n,{timeoutMs:t,silent:true});e.noteDevServerOrigin(n,r?.devServerOrigin);return typeof r?.startedAt==="number"?r.startedAt:null}catch{return null}}function assertReloadSupported(e,n){if(n===ALL_DEVICES)return;const t=e.toolActions(APP_TOOL_ID).find(r=>r.deviceId===n);if(!t?.actions.includes(RELOAD_ACTION)){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 waitForAppBack(e,n,t,r){const s=Date.now();await delay3(TEARDOWN_GRACE_MS);while(Date.now()-s<t){const o=await ping(e,n,PROBE_TIMEOUT_MS);if(o!==null&&o!==r)return Date.now()-s;await delay3(PROBE_INTERVAL_MS)}return null}function registerReloadTools(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. Requires configured @buoy-gg/core support and Buoy Pro.",inputSchema:{strategy:import_zod27.z.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:import_zod27.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),timeoutMs:import_zod27.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),deviceId:import_zod27.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')}},async t=>{try{const r=n.requireProDevice(t.deviceId);assertReloadSupported(n,r);const s=t.wait===false?null:await ping(n,r,PROBE_TIMEOUT_MS);let o=null;let i=false;const a=async l=>{const d=n.devServerOrigin(r);if(!d){if(l)throw l;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 reloadViaDevServer(d);if(!h.ok){throw new Error(`The app is not answering (it looks crashed) and the dev server at ${d} could not be reached either: ${h.error}. Reload the app by hand.`)}i=true};if(n.isUnresponsive(r)&&n.devServerOrigin(r)){await a()}else{try{o=await n.callAction(APP_TOOL_ID,RELOAD_ACTION,{strategy:t.strategy??"auto"},r)}catch(l){await a(l)}}const c=i?` 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 text(`Reload requested${c}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`)}const u=await waitForAppBack(n,r,t.timeoutMs??DEFAULT_WAIT_MS,s);if(u==null){return fail2(`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 text(`Reloaded the app${c} \u2014 back and connected after ${(u/1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod28=require("zod");var text7=e=>({content:[{type:"text",text:e}]});var fail9=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg7=e=>e instanceof Error?e.message:String(e);var PROBE_TIMEOUT_MS2=5e3;var MAX_STORAGE_KEYS=20;var MAX_STORAGE_VALUE=400;var STORAGE_VALUE_BUDGET=1600;var MAX_LINE=160;var cap=(e,n=MAX_LINE)=>e.length>n?`${e.slice(0,n)}\u2026`:e;function hhmmss3(e){if(!e)return"--:--:--";const n=new Date(e);const t=r=>String(r).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}async function otherBackends(e,n,t){const r=[];try{const s=await e.callAction("storage","mmkv.snapshot",{},n,t);for(const o of s??[]){const i=o.entries?.length??0;if(i>0)r.push(`MMKV "${o.id}": ${i} key${i===1?"":"s"}`)}}catch{}try{const s=await e.callAction("storage","secure.keys",{},n,t);if(s?.length)r.push(`SecureStore: ${s.length} key${s.length===1?"":"s"}`)}catch{}return r.length?`
341
+ \`\`\``)}catch(a){return fail2(errMsg2(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:deviceIdInput,action:import_zod26.z.enum(["rescan","measure_sizes","detail","save_baseline","clear_baseline","clear_records"]).describe("What to do."),id:import_zod26.z.number().optional().describe("Asset id from get_assets (detail only).")}},async({deviceId:t,action:r,id:s})=>{try{const o=n.requireProDevice(t);switch(r){case"rescan":{const a=await n.callAction(ASSETS_TOOL_ID,"rescan",{},o);const i=a?.scanStatus;if(!i)return fail2("Device returned nothing for rescan.");return text(`Rescan done: ${i.registryCount} registered at runtime`+(i.graphCount!=null?` \xB7 ${i.graphCount} in the bundle graph`:" \xB7 no bundle graph")+(i.graphError?` (graph: ${i.graphError})`:"")+` \xB7 ${i.fontFamilies.length} font families`+(i.measuring?" \xB7 size measurement running \u2014 get_assets in a moment":""))}case"measure_sizes":{const a=await n.callAction(ASSETS_TOOL_ID,"measureSizes",{},o);return text(`Measured ${a?.measured??0} of ${a?.total??0} assets.`)}case"detail":{if(s==null)return fail2("detail requires the `id` param (from get_assets).");const a=await n.callAction(ASSETS_TOOL_ID,"getDetail",{id:s},o);if(!a)return fail2(`Asset ${s} not found \u2014 re-run get_assets for current ids.`);const i=[`## ${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)i.push(`- Dimensions: ${a.width}\xD7${a.height} logical \xB7 scales ${scalesLabel(a.scales)}`);if(a.sizesByScale){for(const[c,u]of Object.entries(a.sizesByScale)){i.push(`- Size @${c}x: ${kb2(u)}`)}}if(a.sizeBytes!=null)i.push(`- Total: ${sizeLabel(a)}`);if(a.estDecodedBytes)i.push(`- Decoded memory: ~${kb2(a.estDecodedBytes)} (RGBA)`);if(a.hash)i.push(`- Hash: ${a.hash}`);if(a.fileSystemLocation)i.push(`- Source dir: ${a.fileSystemLocation}`);if(a.files?.length)i.push(`- Files: ${a.files.join(", ")}`);if(a.uri)i.push(`- Resolved URI: ${a.uri}`);return text(i.join("\n"))}case"save_baseline":{const a=await n.callAction(ASSETS_TOOL_ID,"saveBaseline",{},o);return text(`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(ASSETS_TOOL_ID,"clearBaseline",{},o);return text("Baseline cleared.")}case"clear_records":{const a=await n.callAction(ASSETS_TOOL_ID,"clearRecords",{},o);return text(a?.message??"Inventory cleared.")}}}catch(o){return fail2(errMsg2(o))}})}var import_zod27=require("zod");var import_ws=require("ws");var CONNECT_TIMEOUT_MS=3e3;function messageSocketUrl(e){return`${e.replace(/^http/,"ws").replace(/\/+$/,"")}/message`}function reloadViaDevServer(e){return new Promise(n=>{let t=false;const r=i=>{if(t)return;t=true;n(i)};let s;try{s=new import_ws.WebSocket(messageSocketUrl(e))}catch(i){r({ok:false,error:i.message});return}const o=setTimeout(()=>{try{s.close()}catch{}r({ok:false,error:`dev server at ${e} did not accept a connection`})},CONNECT_TIMEOUT_MS);let a=false;s.on("open",()=>{try{s.send(JSON.stringify({version:2,method:"reload"}));a=true;clearTimeout(o);setTimeout(()=>{try{s.close()}catch{}r({ok:true})},250)}catch(i){clearTimeout(o);r({ok:false,error:i.message})}});s.on("error",i=>{clearTimeout(o);if(a)return;r({ok:false,error:i.message})})})}var APP_TOOL_ID="app";var RELOAD_ACTION="reloadApp";var DEFAULT_WAIT_MS=3e4;var TEARDOWN_GRACE_MS=1200;var PROBE_TIMEOUT_MS=1500;var PROBE_INTERVAL_MS=400;var delay3=e=>new Promise(n=>setTimeout(n,e));async function ping(e,n,t){try{const r=await e.callAction(APP_TOOL_ID,"ping",{},n,{timeoutMs:t,silent:true});e.noteDevServerOrigin(n,r?.devServerOrigin);return typeof r?.startedAt==="number"?r.startedAt:null}catch{return null}}function assertReloadSupported(e,n){if(n===ALL_DEVICES)return;const t=e.toolActions(APP_TOOL_ID).find(r=>r.deviceId===n);if(!t?.actions.includes(RELOAD_ACTION)){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 waitForAppBack(e,n,t,r){const s=Date.now();await delay3(TEARDOWN_GRACE_MS);while(Date.now()-s<t){const o=await ping(e,n,PROBE_TIMEOUT_MS);if(o!==null&&o!==r)return Date.now()-s;await delay3(PROBE_INTERVAL_MS)}return null}function registerReloadTools(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. Requires configured @buoy-gg/core support and Buoy Pro.",inputSchema:{strategy:import_zod27.z.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:import_zod27.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),timeoutMs:import_zod27.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),deviceId:import_zod27.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')}},async t=>{try{const r=n.requireProDevice(t.deviceId);assertReloadSupported(n,r);const s=t.wait===false?null:await ping(n,r,PROBE_TIMEOUT_MS);let o=null;let a=false;const i=async l=>{const h=n.devServerOrigin(r);if(!h){if(l)throw l;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 d=await reloadViaDevServer(h);if(!d.ok){throw new Error(`The app is not answering (it looks crashed) and the dev server at ${h} could not be reached either: ${d.error}. Reload the app by hand.`)}a=true};if(n.isUnresponsive(r)&&n.devServerOrigin(r)){await i()}else{try{o=await n.callAction(APP_TOOL_ID,RELOAD_ACTION,{strategy:t.strategy??"auto"},r)}catch(l){await i(l)}}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 text(`Reload requested${c}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`)}const u=await waitForAppBack(n,r,t.timeoutMs??DEFAULT_WAIT_MS,s);if(u==null){return fail2(`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 text(`Reloaded the app${c} \u2014 back and connected after ${(u/1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`)}catch(r){return fail2(errMsg2(r))}})}var import_zod28=require("zod");var text7=e=>({content:[{type:"text",text:e}]});var fail9=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg7=e=>e instanceof Error?e.message:String(e);var PROBE_TIMEOUT_MS2=5e3;var MAX_STORAGE_KEYS=20;var MAX_STORAGE_VALUE=400;var STORAGE_VALUE_BUDGET=1600;var MAX_LINE=160;var cap=(e,n=MAX_LINE)=>e.length>n?`${e.slice(0,n)}\u2026`:e;function hhmmss3(e){if(!e)return"--:--:--";const n=new Date(e);const t=r=>String(r).padStart(2,"0");return`${t(n.getHours())}:${t(n.getMinutes())}:${t(n.getSeconds())}`}async function otherBackends(e,n,t){const r=[];try{const s=await e.callAction("storage","mmkv.snapshot",{},n,t);for(const o of s??[]){const a=o.entries?.length??0;if(a>0)r.push(`MMKV "${o.id}": ${a} key${a===1?"":"s"}`)}}catch{}try{const s=await e.callAction("storage","secure.keys",{},n,t);if(s?.length)r.push(`SecureStore: ${s.length} key${s.length===1?"":"s"}`)}catch{}return r.length?`
265
342
  ${r.join(" \xB7 ")} \u2014 read with get_storage`:""}async function section(e,n){try{const t=await n();return t?`${e}
266
343
  ${t}`:null}catch(t){const r=errMsg7(t);if(/not responding|timed out|got no answer/i.test(r)){return`${e}
267
344
  (no answer \u2014 see the note above)`}return`${e}
268
- (unavailable: ${cap(r,120)})`}}function registerTriageTools(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. Requires Buoy Pro.",inputSchema:{deviceId:import_zod28.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=n.connectedDevices().find(m=>m.deviceId===r);const o=new Set((s?.tools??[]).map(m=>m.toolId));const i=`**${s?.deviceName??r}** (${s?.platform??"?"}, id \`${r}\`) \u2014 tools: ${[...o].join(", ")||"none"}`;const a={timeoutMs:PROBE_TIMEOUT_MS2};const[c,u,l,d]=await Promise.all([section("**Console \u2014 recent errors/warnings:**",async()=>{if(!o.has("console"))return null;const{payload:m}=await n.getSnapshot("console",r,a);const g=m;const p=Array.isArray(g)?g:g?.entries??[];const f=p.filter(b=>b.level==="error"||b.level==="warning");const w=f.slice(-8);if(w.length===0){return`(none \u2014 ${p.length} entries captured)`}return w.map(b=>`${hhmmss3(b.timestamp)} [${b.level==="error"?"ERR":"warn"}] ${cap((b.message??"").split("\n").map(_=>_.trim()).filter(Boolean).slice(0,3).join(" \u23CE "),220)}`).join("\n")}),section("**Network \u2014 recent requests:**",async()=>{if(!o.has("network"))return null;const{payload:m}=await n.getSnapshot("network",r,a);const g=Array.isArray(m)?m:m?.events??[];if(g.length===0)return"(no requests captured yet)";const p=g.slice(-8);return p.map(f=>{const w=f.error||(f.status??0)>=400?"\u274C":"\u2705";const b=f.path??f.url??"?";const _=f.error??f.status??"pending";return`${w} ${f.method??"GET"} ${cap(String(b),80)} \u2192 ${_}${f.duration?` (${Math.round(f.duration)}ms)`:""}`}).join("\n")}),section("**Storage \u2014 persisted values:**",async()=>{if(!o.has("storage"))return null;const m=await n.callAction("storage","async.getAllKeys",{},r,a);if(!Array.isArray(m)||m.length===0){return"(no app keys)"}const g=m.slice(0,MAX_STORAGE_KEYS);let p=[];try{p=await n.callAction("storage","async.multiGet",{keys:g},r,a)??[]}catch{return`${g.map($=>`\`${$}\``).join(", ")}
269
- _Values: get_storage includeValues:true_`}const f=new Map(p);let w=STORAGE_VALUE_BUDGET;const b=g.map($=>{const A=f.get($);if(A==null)return`- \`${$}\` = (null)`;if(A.length>MAX_STORAGE_VALUE){return`- \`${$}\` = (${(A.length/1024).toFixed(1)} KB \u2014 read with get_storage)`}if(w<=0)return`- \`${$}\` = (not shown \u2014 triage budget spent; read with get_storage)`;w-=A.length;return`- \`${$}\` = ${A}`});const _=m.length-g.length;const k=await otherBackends(n,r,a);return`${b.join("\n")}${_>0?`
270
- \u2026+${_} more keys`:""}${k}`}),section("**Screen \u2014 what's visible now:**",async()=>{if(!o.has("highlight-updates"))return null;const m=await n.callAction("highlight-updates","describeScreen",{},r,a);const g=(m?.elements??[]).filter(p=>p.interactive);if(g.length===0)return"(nothing interactive on screen)";return g.slice(0,20).map(p=>`\u2022 ${p.name}${p.text||p.label?` "${cap(p.text??p.label??"",60)}"`:""}${p.testID?` [testID=${p.testID}]`:""}`).join("\n")})]);const h=n.isUnresponsive(r);const y=h?`
345
+ (unavailable: ${cap(r,120)})`}}function registerTriageTools(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. Requires Buoy Pro.",inputSchema:{deviceId:import_zod28.z.string().optional().describe("Target device id (auto-selected if one is connected).")}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=n.connectedDevices().find(g=>g.deviceId===r);const o=new Set((s?.tools??[]).map(g=>g.toolId));const a=`**${s?.deviceName??r}** (${s?.platform??"?"}, id \`${r}\`) \u2014 tools: ${[...o].join(", ")||"none"}`;const i={timeoutMs:PROBE_TIMEOUT_MS2};const[c,u,l,h]=await Promise.all([section("**Console \u2014 recent errors/warnings:**",async()=>{if(!o.has("console"))return null;const{payload:g}=await n.getSnapshot("console",r,i);const b=g;const p=Array.isArray(b)?b:b?.entries??[];const f=p.filter(y=>y.level==="error"||y.level==="warning");const v=f.slice(-8);if(v.length===0){return`(none \u2014 ${p.length} entries captured)`}return v.map(y=>`${hhmmss3(y.timestamp)} [${y.level==="error"?"ERR":"warn"}] ${cap((y.message??"").split("\n").map(k=>k.trim()).filter(Boolean).slice(0,3).join(" \u23CE "),220)}`).join("\n")}),section("**Network \u2014 recent requests:**",async()=>{if(!o.has("network"))return null;const{payload:g}=await n.getSnapshot("network",r,i);const b=Array.isArray(g)?g:g?.events??[];if(b.length===0)return"(no requests captured yet)";const p=b.slice(-8);return p.map(f=>{const v=f.error||(f.status??0)>=400?"\u274C":"\u2705";const y=f.path??f.url??"?";const k=f.error??f.status??"pending";return`${v} ${f.method??"GET"} ${cap(String(y),80)} \u2192 ${k}${f.duration?` (${Math.round(f.duration)}ms)`:""}`}).join("\n")}),section("**Storage \u2014 persisted values:**",async()=>{if(!o.has("storage"))return null;const g=await n.callAction("storage","async.getAllKeys",{},r,i);if(!Array.isArray(g)||g.length===0){return"(no app keys)"}const b=g.slice(0,MAX_STORAGE_KEYS);let p=[];try{p=await n.callAction("storage","async.multiGet",{keys:b},r,i)??[]}catch{return`${b.map(_=>`\`${_}\``).join(", ")}
346
+ _Values: get_storage includeValues:true_`}const f=new Map(p);let v=STORAGE_VALUE_BUDGET;const y=b.map(_=>{const M=f.get(_);if(M==null)return`- \`${_}\` = (null)`;if(M.length>MAX_STORAGE_VALUE){return`- \`${_}\` = (${(M.length/1024).toFixed(1)} KB \u2014 read with get_storage)`}if(v<=0)return`- \`${_}\` = (not shown \u2014 triage budget spent; read with get_storage)`;v-=M.length;return`- \`${_}\` = ${M}`});const k=g.length-b.length;const S=await otherBackends(n,r,i);return`${y.join("\n")}${k>0?`
347
+ \u2026+${k} more keys`:""}${S}`}),section("**Screen \u2014 what's visible now:**",async()=>{if(!o.has("highlight-updates"))return null;const g=await n.callAction("highlight-updates","describeScreen",{},r,i);const b=(g?.elements??[]).filter(p=>p.interactive);if(b.length===0)return"(nothing interactive on screen)";return b.slice(0,20).map(p=>`\u2022 ${p.name}${p.text||p.label?` "${cap(p.text??p.label??"",60)}"`:""}${p.testID?` [testID=${p.testID}]`:""}`).join("\n")})]);const d=n.isUnresponsive(r);const m=d?`
271
348
 
272
- \u{1F6D1} ${n.unresponsiveNote(r)}`:"";const v=[c,u,l,d].filter(Boolean).join("\n\n");return text7(`${i}${y}
349
+ \u{1F6D1} ${n.unresponsiveNote(r)}`:"";const w=[c,u,l,h].filter(Boolean).join("\n\n");return text7(`${a}${m}
273
350
 
274
- ${v}
351
+ ${w}
275
352
 
276
- _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`)}catch(r){return fail9(errMsg7(r))}})}var import_zod29=require("zod");var UI_TOOL_ID="highlight-updates";var APP_TOOL_ID2="app";var text8=e=>({content:[{type:"text",text:e}]});var fail10=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg8=e=>e instanceof Error?e.message:String(e);var delay4=e=>new Promise(n=>setTimeout(n,e));var POLL_MS=250;var DEFAULT_WAIT_MS2=5e3;var ASSERT_SETTLE_MS=2e3;var TEARDOWN_GRACE_MS2=1200;var REALM_PROBE_MS=1500;var MAX_FLOW_MS=12e4;function matches(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(r=>String(r).toLowerCase().includes(t))}function formatElement2(e){const n=e.text??e.label;return`${e.interactive?"\u2022 ":" "}${e.name}${n?` "${n}"`:""}${e.testID?` [testID=${e.testID}]`:""}`}var stepSchema=import_zod29.z.object({tap:import_zod29.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:import_zod29.z.boolean().optional().describe("Make `tap` a long-press instead."),type:import_zod29.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod29.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:import_zod29.z.union([import_zod29.z.number(),import_zod29.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),reload:import_zod29.z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:import_zod29.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),waitFor:import_zod29.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:import_zod29.z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:import_zod29.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:import_zod29.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),timeoutMs:import_zod29.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 \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");function registerFlowTools(e,n){e.registerTool("run_flow",{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. Actions use exposed app handlers, not physical gestures. Review failures and check the expected behavior. Requires Buoy Pro and the app interaction integration.",inputSchema:{steps:import_zod29.z.array(stepSchema).min(1).describe("Steps to run, in order."),include:import_zod29.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."),continueOnFailure:import_zod29.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),deviceId:deviceIdInput}},async t=>{const r=Date.now();try{const s=n.requireProDevice(t.deviceId);const o=t.include??"all";const i=async()=>{const w=await n.callAction(UI_TOOL_ID,"describeScreen",{},s);return w?.elements??[]};const a=async()=>{try{const w=await n.callAction(APP_TOOL_ID2,"ping",{},s,{timeoutMs:REALM_PROBE_MS,silent:true});return typeof w?.startedAt==="number"?w.startedAt:null}catch{return null}};const c=async(w,b)=>{const _=Date.now();while(Date.now()-_<b){const k=await a();if(k!==null&&k!==w)return Date.now()-_;await delay4(POLL_MS)}return null};const u=new Set;const l=new Set;const d=new Set((n.connectedDevices().find(w=>w.deviceId===s)?.tools??[]).map(w=>w.toolId));if(o==="all"){if(d.has("console")){const{payload:w}=await n.getSnapshot("console",s).catch(()=>({payload:null}));const b=w?.entries??[];for(const _ of b)u.add(_.id)}if(d.has("network")){const{payload:w}=await n.getSnapshot("network",s).catch(()=>({payload:null}));const b=Array.isArray(w)?w:w?.events??[];for(const _ of b)l.add(_.id)}}const h=async(w,b)=>{const _=Date.now();for(;;){if(Date.now()-_>=b||Date.now()-r>MAX_FLOW_MS){return{ok:false,ms:Date.now()-_}}let k=[];try{k=await i()}catch{}if(w(k))return{ok:true,ms:Date.now()-_};await delay4(POLL_MS)}};const y=[];let v=null;let m=0;for(let w=0;w<t.steps.length;w++){const b=t.steps[w];const _=`${w+1}`;const k=b.timeoutMs??DEFAULT_WAIT_MS2;if(Date.now()-r>MAX_FLOW_MS){y.push(`${_} \u2717 flow exceeded ${MAX_FLOW_MS/1e3}s \u2014 stopped here`);v=w;break}const $=[];const A=[];const R=b.reload||b.tap||b.type!==void 0||b.waitMs||b.waitFor||b.waitGone||b.expect||b.expectGone;if(!R){y.push(`${w+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${b.set!==void 0?" (`set` needs `tap` naming the control)":""}`);v=v??w;if(!t.continueOnFailure)break;continue}try{if(b.reload){const x=await a();await n.callAction(APP_TOOL_ID2,"reloadApp",{strategy:"auto"},s);await delay4(TEARDOWN_GRACE_MS2);const S=await c(x,Math.max(k,2e4));if(S==null){$.push("app did not come back after reload")}else{A.push(`reloaded (${S}ms)`);await h(T=>T.length>0,1e4)}}if(b.type!==void 0){const x=b.into??b.tap;if(!x){$.push("`type` needs `into` (which input to type into)")}else{const S=await n.callAction(UI_TOOL_ID,"tapElement",{...selector(x),text:b.type},s);if(!S?.tapped){$.push(S?.reason??`could not type into "${x}"`)}else if(S.matched?.via&&S.matched.via!=="onChangeText"){$.push(`"${x}" is not a text input \u2014 it ran ${S.matched.via} instead, and the text was not entered`)}else{A.push(`typed ${JSON.stringify(b.type)} into "${x}"`)}}}else if(b.tap){const x={...selector(b.tap)};if(b.longPress)x.longPress=true;if(b.set!==void 0)x.value=b.set;const S=await n.callAction(UI_TOOL_ID,"tapElement",x,s);if(!S?.tapped){$.push(S?.reason??`no element matched "${b.tap}"`)}else{const T=S.matched;const M=T?.text??T?.label??T?.name??b.tap;if(b.set!==void 0&&T?.via&&T.via!=="onValueChange"){$.push(`"${M}" is not a toggle/slider \u2014 it ran ${T.via} instead, so it was not set to ${JSON.stringify(b.set)}`)}else{A.push(b.set!==void 0?`set "${M}" \u2192 ${JSON.stringify(T?.value??b.set)}`:`${b.longPress?"long-pressed":"tapped"} "${M}"`)}}}if(!$.length&&b.waitMs){await delay4(b.waitMs);A.push(`waited ${b.waitMs}ms`)}if(!$.length&&b.waitFor){const x=await h(S=>S.some(T=>matches(T,b.waitFor)),k);if(x.ok)A.push(`"${b.waitFor}" appeared (${x.ms}ms)`);else $.push(`"${b.waitFor}" never appeared within ${k}ms`)}if(!$.length&&b.waitGone){const x=await h(S=>!S.some(T=>matches(T,b.waitGone)),k);if(x.ok)A.push(`"${b.waitGone}" gone (${x.ms}ms)`);else $.push(`"${b.waitGone}" was still there after ${k}ms`)}if(!$.length&&b.expect){const x=await h(S=>S.some(T=>matches(T,b.expect)),b.timeoutMs??ASSERT_SETTLE_MS);if(x.ok)A.push(`"${b.expect}" is on screen`);else $.push(`expected "${b.expect}" on screen \u2014 not found`)}if(!$.length&&b.expectGone){const x=await h(S=>!S.some(T=>matches(T,b.expectGone)),b.timeoutMs??ASSERT_SETTLE_MS);if(x.ok)A.push(`"${b.expectGone}" is gone`);else $.push(`expected "${b.expectGone}" to be gone \u2014 still on screen`)}}catch(x){$.push(errMsg8(x))}if($.length){y.push(`${_} \u2717 ${describeStep(b)} \u2014 ${$.join("; ")}`);v=v??w;if(!t.continueOnFailure)break}else{m++;y.push(`${_} \u2713 ${A.join(", ")||describeStep(b)}`)}}const g=((Date.now()-r)/1e3).toFixed(1);const p=v===null?`**Flow: ${m}/${t.steps.length} steps OK (${g}s)**`:`**Flow FAILED at step ${v+1} \u2014 ${m}/${t.steps.length} steps OK (${g}s)**`;const f=[p,y.join("\n")];if(o!=="none"){try{const w=await i();const b=w.filter(_=>_.interactive).slice(0,30);f.push(`**Screen now (${b.length} interactive of ${w.length}):**
277
- ${b.map(formatElement2).join("\n")}`)}catch(w){f.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(w))}`)}}if(o==="all"){if(d.has("network")){const{payload:w}=await n.getSnapshot("network",s).catch(()=>({payload:null}));const b=Array.isArray(w)?w:w?.events??[];const _=b.filter(k=>!l.has(k.id)).slice(-15);if(_.length){f.push(`**Network during this flow (${_.length}):**
278
- `+_.map(k=>`${k.error||(k.status??0)>=400?"\u274C":"\u2705"} ${k.method??"GET"} ${k.path??k.url??"?"} \u2192 ${k.error??k.status??"pending"}`).join("\n"))}}if(d.has("console")){const{payload:w}=await n.getSnapshot("console",s).catch(()=>({payload:null}));const b=w?.entries??[];const _=b.filter(k=>!u.has(k.id)&&(k.level==="error"||k.level==="warning")).slice(-10);if(_.length){f.push(`**Console during this flow (${_.length}):**
279
- `+_.map(k=>`[${k.level==="error"?"ERR":"warn"}] ${cap2(firstLines(k.message??""),220)}`).join("\n"))}}}return text8(f.join("\n\n"))}catch(s){return fail10(errMsg8(s))}})}function selector(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function describeStep(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.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;return"step"}var cap2=(e,n=160)=>e.length>n?`${e.slice(0,n)}\u2026`:e;var firstLines=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ");var import_zod30=require("zod");var UI_TOOL_ID2="highlight-updates";var text9=e=>({content:[{type:"text",text:e}]});var fail11=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg9=e=>e instanceof Error?e.message:String(e);var delay5=e=>new Promise(n=>setTimeout(n,e));var DEFAULT_IDLE_MS=3e3;var SETTLE_MS=400;var DEFAULT_TOP=12;var lastCapture=new Map;var stepSchema2=import_zod30.z.object({tap:import_zod30.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),type:import_zod30.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod30.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),waitMs:import_zod30.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),repeat:import_zod30.z.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 selector2(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function dominant(e){if(!e)return"unknown";let n="unknown";let t=-1;for(const[r,s]of Object.entries(e)){if((s??0)>t){t=s??0;n=r}}return n}var CAUSE_HINT={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 formatCapture(e,n){const t=e.summary;const r=t.wastedRenders??0;const s=t.totalRenders>0?Math.round(r/t.totalRenders*100):0;const o=[];const i=t.totalRenderMs??0;o.push(`**${t.totalRenders} component renders across ${t.totalCommits} commits in ${(e.windowMs/1e3).toFixed(1)}s**`+(i>0?` \xB7 ${i.toFixed(1)}ms of render work`:"")+` \xB7 ${r} wasted (${s}%)`);const a=Object.entries(t.causes??{}).filter(([,d])=>(d??0)>0).sort((d,h)=>(h[1]??0)-(d[1]??0)).map(([d,h])=>`${d} ${h}`).join(" \xB7 ");if(a)o.push(`causes: ${a}`);o.push("");o.push("| component | self ms | renders | cause | dragged along by | changed props |");o.push("|---|---:|---:|---|---|---|");const c=[...t.topComponents].sort((d,h)=>(h.totalMs??0)-(d.totalMs??0)||h.renders-d.renders).slice(0,n);for(const d of c){o.push(`| ${d.name} | ${(d.totalMs??0).toFixed(1)} | ${d.renders} | ${dominant(d.causes)} | ${d.parentName??"\u2014"} | ${(d.changedProps??[]).join(", ")||"\u2014"} |`)}if(t.topComponents.length>n){o.push(`_\u2026+${t.topComponents.length-n} more components_`)}const u=new Set(c.map(d=>dominant(d.causes)));const l=[...u].filter(d=>CAUSE_HINT[d]).map(d=>`\`${d}\` = ${CAUSE_HINT[d]}`);if(l.length){o.push("");o.push(l.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 formatDelta(e,n){const t=(l,d)=>{const h=d-l;return`${l} \u2192 ${d} (${h>0?"+":""}${h})`};const r=e.summary;const s=n.summary;const o=[`**vs "${e.label}"**`];o.push(`- total renders: ${t(r.totalRenders,s.totalRenders)}`);o.push(`- wasted: ${t(r.wastedRenders??0,s.wastedRenders??0)}`);const i=new Map(r.topComponents.map(l=>[l.name,l.renders]));const a=[];for(const l of s.topComponents.slice(0,20)){const d=i.get(l.name)??0;if(d!==l.renders)a.push(`${l.name} ${t(d,l.renders)}`);i.delete(l.name)}for(const[l,d]of i){if(d>0)a.push(`${l} ${t(d,0)}`)}if(a.length)o.push(`- per component: ${a.slice(0,10).join(" \xB7 ")}`);const c=s.totalRenders<r.totalRenders;const u=s.totalRenders>r.totalRenders;o.push(u?"\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 registerRenderTools(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:import_zod30.z.array(stepSchema2).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:import_zod30.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),label:import_zod30.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),compareToPrevious:import_zod30.z.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),top:import_zod30.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=n.connectedDevices().find(g=>g.deviceId===r);const o=(s?.tools??[]).find(g=>g.toolId===UI_TOOL_ID2);if(!o){return fail11("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 fail11("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 i=await n.callAction(UI_TOOL_ID2,"beginMeasurement",{},r);if(i&&i.ok===false){return fail11(`Can't measure renders: ${i.reason??"capture unavailable"}`)}await delay5(SETTLE_MS);const a=Date.now();const c=[];if(t.steps?.length){for(const g of t.steps){const p=g.repeat??1;for(let f=0;f<p;f++){if(g.type!==void 0){const w=g.into??g.tap;if(!w){c.push("a `type` step needs `into`");continue}const b=await n.callAction(UI_TOOL_ID2,"tapElement",{...selector2(w),text:g.type},r);if(!b?.tapped)c.push(b?.reason??`could not type into "${w}"`);else if(b.matched?.via&&b.matched.via!=="onChangeText"){c.push(`"${w}" is not a text input \u2014 it ran ${b.matched.via}, nothing was typed`)}}else if(g.tap){const w=await n.callAction(UI_TOOL_ID2,"tapElement",selector2(g.tap),r);if(!w?.tapped)c.push(w?.reason??`no element matched "${g.tap}"`)}if(g.waitMs)await delay5(g.waitMs)}}await delay5(SETTLE_MS)}else{await delay5(t.idleMs??DEFAULT_IDLE_MS)}const u=Date.now()-a;const l=await n.callAction(UI_TOOL_ID2,"endMeasurement",{},r);const d=l?.summary;if(!d){return fail11("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:u,summary:d};const y=lastCapture.get(r);lastCapture.set(r,h);const v=t.steps?.length?t.steps.map(g=>(g.type!==void 0?`type "${g.type}"`:`tap "${g.tap}"`)+(g.repeat?` \xD7${g.repeat}`:"")).join(", "):`idle ${(u/1e3).toFixed(1)}s`;const m=[`**measure_renders \u2014 ${v}**`,"",formatCapture(h,t.top??DEFAULT_TOP)];if(t.compareToPrevious){m.push("",y?formatDelta(y,h):"_No previous measurement on this device to compare against._")}if(c.length)m.push("",`\u26A0\uFE0F ${c.join("; ")}`);if(d.totalRenders===0){m.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){m.push("","_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._")}return text9(m.join("\n"))}catch(r){return fail11(errMsg9(r))}})}var import_node_fs6=__toESM(require("fs"));var import_node_path6=__toESM(require("path"));var WATCHED_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".json"]);var IGNORED_DIRS=new Set(["node_modules",".git",".expo","ios","android","dist","lib","build",".next","coverage","__snapshots__"]);var DEBOUNCE_MS=400;function resolveMode(e){const n=(e??"").trim().toLowerCase();if(n==="always"||n==="never"||n==="auto")return n;return"auto"}var EditWatcher=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=import_node_fs6.default.watch(e,{recursive:true},(t,r)=>{if(!r)return;const s=String(r);if(s.split(import_node_path6.default.sep).some(o=>IGNORED_DIRS.has(o)))return;if(!WATCHED_EXT.has(import_node_path6.default.extname(s)))return;this.noteEdit(s)});this.watcher.on("error",()=>this.stop())}catch{this.stop()}}noteEdit(e){const n=Date.now();if(n-this.lastEditAt>DEBOUNCE_MS)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`
353
+ _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`)}catch(r){return fail9(errMsg7(r))}})}var import_zod29=require("zod");var UI_TOOL_ID="highlight-updates";var APP_TOOL_ID2="app";var text8=e=>({content:[{type:"text",text:e}]});var fail10=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg8=e=>e instanceof Error?e.message:String(e);var delay4=e=>new Promise(n=>setTimeout(n,e));var POLL_MS=250;var DEFAULT_WAIT_MS2=5e3;var ASSERT_SETTLE_MS=2e3;var TEARDOWN_GRACE_MS2=1200;var REALM_PROBE_MS=1500;var MAX_FLOW_MS=12e4;function matches(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(r=>String(r).toLowerCase().includes(t))}function formatElement2(e){const n=e.text??e.label;return`${e.interactive?"\u2022 ":" "}${e.name}${n?` "${n}"`:""}${e.testID?` [testID=${e.testID}]`:""}`}var stepSchema=import_zod29.z.object({tap:import_zod29.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),longPress:import_zod29.z.boolean().optional().describe("Make `tap` a long-press instead."),type:import_zod29.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod29.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),set:import_zod29.z.union([import_zod29.z.number(),import_zod29.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),reload:import_zod29.z.boolean().optional().describe("Reload the JS bundle before this step's action."),waitMs:import_zod29.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),waitFor:import_zod29.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),waitGone:import_zod29.z.string().optional().describe("Wait until no element matches this (default 5s)."),expect:import_zod29.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),expectGone:import_zod29.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),timeoutMs:import_zod29.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 \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");function registerFlowTools(e,n){e.registerTool("run_flow",{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. Actions use exposed app handlers, not physical gestures. Review failures and check the expected behavior. Requires Buoy Pro and the app interaction integration.",inputSchema:{steps:import_zod29.z.array(stepSchema).min(1).describe("Steps to run, in order."),include:import_zod29.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."),continueOnFailure:import_zod29.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),deviceId:deviceIdInput}},async t=>{const r=Date.now();try{const s=n.requireProDevice(t.deviceId);const o=t.include??"all";const a=async()=>{const v=await n.callAction(UI_TOOL_ID,"describeScreen",{},s);return v?.elements??[]};const i=async()=>{try{const v=await n.callAction(APP_TOOL_ID2,"ping",{},s,{timeoutMs:REALM_PROBE_MS,silent:true});return typeof v?.startedAt==="number"?v.startedAt:null}catch{return null}};const c=async(v,y)=>{const k=Date.now();while(Date.now()-k<y){const S=await i();if(S!==null&&S!==v)return Date.now()-k;await delay4(POLL_MS)}return null};const u=new Set;const l=new Set;const h=new Set((n.connectedDevices().find(v=>v.deviceId===s)?.tools??[]).map(v=>v.toolId));if(o==="all"){if(h.has("console")){const{payload:v}=await n.getSnapshot("console",s).catch(()=>({payload:null}));const y=v?.entries??[];for(const k of y)u.add(k.id)}if(h.has("network")){const{payload:v}=await n.getSnapshot("network",s).catch(()=>({payload:null}));const y=Array.isArray(v)?v:v?.events??[];for(const k of y)l.add(k.id)}}const d=async(v,y)=>{const k=Date.now();for(;;){if(Date.now()-k>=y||Date.now()-r>MAX_FLOW_MS){return{ok:false,ms:Date.now()-k}}let S=[];try{S=await a()}catch{}if(v(S))return{ok:true,ms:Date.now()-k};await delay4(POLL_MS)}};const m=[];let w=null;let g=0;for(let v=0;v<t.steps.length;v++){const y=t.steps[v];const k=`${v+1}`;const S=y.timeoutMs??DEFAULT_WAIT_MS2;if(Date.now()-r>MAX_FLOW_MS){m.push(`${k} \u2717 flow exceeded ${MAX_FLOW_MS/1e3}s \u2014 stopped here`);w=v;break}const _=[];const M=[];const B=y.reload||y.tap||y.type!==void 0||y.waitMs||y.waitFor||y.waitGone||y.expect||y.expectGone;if(!B){m.push(`${v+1} \u2717 step does nothing \u2014 give it a tap/type/wait/expect${y.set!==void 0?" (`set` needs `tap` naming the control)":""}`);w=w??v;if(!t.continueOnFailure)break;continue}try{if(y.reload){const I=await i();await n.callAction(APP_TOOL_ID2,"reloadApp",{strategy:"auto"},s);await delay4(TEARDOWN_GRACE_MS2);const x=await c(I,Math.max(S,2e4));if(x==null){_.push("app did not come back after reload")}else{M.push(`reloaded (${x}ms)`);await d($=>$.length>0,1e4)}}if(y.type!==void 0){const I=y.into??y.tap;if(!I){_.push("`type` needs `into` (which input to type into)")}else{const x=await n.callAction(UI_TOOL_ID,"tapElement",{...selector(I),text:y.type},s);if(!x?.tapped){_.push(x?.reason??`could not type into "${I}"`)}else if(x.matched?.via&&x.matched.via!=="onChangeText"){_.push(`"${I}" is not a text input \u2014 it ran ${x.matched.via} instead, and the text was not entered`)}else{M.push(`typed ${JSON.stringify(y.type)} into "${I}"`)}}}else if(y.tap){const I={...selector(y.tap)};if(y.longPress)I.longPress=true;if(y.set!==void 0)I.value=y.set;const x=await n.callAction(UI_TOOL_ID,"tapElement",I,s);if(!x?.tapped){_.push(x?.reason??`no element matched "${y.tap}"`)}else{const $=x.matched;const T=$?.text??$?.label??$?.name??y.tap;if(y.set!==void 0&&$?.via&&$.via!=="onValueChange"){_.push(`"${T}" is not a toggle/slider \u2014 it ran ${$.via} instead, so it was not set to ${JSON.stringify(y.set)}`)}else{M.push(y.set!==void 0?`set "${T}" \u2192 ${JSON.stringify($?.value??y.set)}`:`${y.longPress?"long-pressed":"tapped"} "${T}"`)}}}if(!_.length&&y.waitMs){await delay4(y.waitMs);M.push(`waited ${y.waitMs}ms`)}if(!_.length&&y.waitFor){const I=await d(x=>x.some($=>matches($,y.waitFor)),S);if(I.ok)M.push(`"${y.waitFor}" appeared (${I.ms}ms)`);else _.push(`"${y.waitFor}" never appeared within ${S}ms`)}if(!_.length&&y.waitGone){const I=await d(x=>!x.some($=>matches($,y.waitGone)),S);if(I.ok)M.push(`"${y.waitGone}" gone (${I.ms}ms)`);else _.push(`"${y.waitGone}" was still there after ${S}ms`)}if(!_.length&&y.expect){const I=await d(x=>x.some($=>matches($,y.expect)),y.timeoutMs??ASSERT_SETTLE_MS);if(I.ok)M.push(`"${y.expect}" is on screen`);else _.push(`expected "${y.expect}" on screen \u2014 not found`)}if(!_.length&&y.expectGone){const I=await d(x=>!x.some($=>matches($,y.expectGone)),y.timeoutMs??ASSERT_SETTLE_MS);if(I.ok)M.push(`"${y.expectGone}" is gone`);else _.push(`expected "${y.expectGone}" to be gone \u2014 still on screen`)}}catch(I){_.push(errMsg8(I))}if(_.length){m.push(`${k} \u2717 ${describeStep(y)} \u2014 ${_.join("; ")}`);w=w??v;if(!t.continueOnFailure)break}else{g++;m.push(`${k} \u2713 ${M.join(", ")||describeStep(y)}`)}}const b=((Date.now()-r)/1e3).toFixed(1);const p=w===null?`**Flow: ${g}/${t.steps.length} steps OK (${b}s)**`:`**Flow FAILED at step ${w+1} \u2014 ${g}/${t.steps.length} steps OK (${b}s)**`;const f=[p,m.join("\n")];if(o!=="none"){try{const v=await a();const y=v.filter(k=>k.interactive).slice(0,30);f.push(`**Screen now (${y.length} interactive of ${v.length}):**
354
+ ${y.map(formatElement2).join("\n")}`)}catch(v){f.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(v))}`)}}if(o==="all"){if(h.has("network")){const{payload:v}=await n.getSnapshot("network",s).catch(()=>({payload:null}));const y=Array.isArray(v)?v:v?.events??[];const k=y.filter(S=>!l.has(S.id)).slice(-15);if(k.length){f.push(`**Network during this flow (${k.length}):**
355
+ `+k.map(S=>`${S.error||(S.status??0)>=400?"\u274C":"\u2705"} ${S.method??"GET"} ${S.path??S.url??"?"} \u2192 ${S.error??S.status??"pending"}`).join("\n"))}}if(h.has("console")){const{payload:v}=await n.getSnapshot("console",s).catch(()=>({payload:null}));const y=v?.entries??[];const k=y.filter(S=>!u.has(S.id)&&(S.level==="error"||S.level==="warning")).slice(-10);if(k.length){f.push(`**Console during this flow (${k.length}):**
356
+ `+k.map(S=>`[${S.level==="error"?"ERR":"warn"}] ${cap2(firstLines(S.message??""),220)}`).join("\n"))}}}return text8(f.join("\n\n"))}catch(s){return fail10(errMsg8(s))}})}function selector(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function describeStep(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.waitFor)return`waitFor "${e.waitFor}"`;if(e.waitGone)return`waitGone "${e.waitGone}"`;if(e.expect)return`expect "${e.expect}"`;return"step"}var cap2=(e,n=160)=>e.length>n?`${e.slice(0,n)}\u2026`:e;var firstLines=(e,n=3)=>e.split("\n").map(t=>t.trim()).filter(Boolean).slice(0,n).join(" \u23CE ");var import_zod30=require("zod");var UI_TOOL_ID2="highlight-updates";var text9=e=>({content:[{type:"text",text:e}]});var fail11=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg9=e=>e instanceof Error?e.message:String(e);var delay5=e=>new Promise(n=>setTimeout(n,e));var DEFAULT_IDLE_MS=3e3;var SETTLE_MS=400;var DEFAULT_TOP=12;var lastCapture=new Map;var stepSchema2=import_zod30.z.object({tap:import_zod30.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),type:import_zod30.z.string().optional().describe("Text to type (needs `into`)."),into:import_zod30.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),waitMs:import_zod30.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),repeat:import_zod30.z.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 selector2(e){return/\s/.test(e)?{query:e}:{testID:e,query:e}}function dominant(e){if(!e)return"unknown";let n="unknown";let t=-1;for(const[r,s]of Object.entries(e)){if((s??0)>t){t=s??0;n=r}}return n}var CAUSE_HINT={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 formatCapture(e,n){const t=e.summary;const r=t.wastedRenders??0;const s=t.totalRenders>0?Math.round(r/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 ${r} wasted (${s}%)`);const i=Object.entries(t.causes??{}).filter(([,h])=>(h??0)>0).sort((h,d)=>(d[1]??0)-(h[1]??0)).map(([h,d])=>`${h} ${d}`).join(" \xB7 ");if(i)o.push(`causes: ${i}`);o.push("");o.push("| component | self ms | renders | cause | dragged along by | changed props |");o.push("|---|---:|---:|---|---|---|");const c=[...t.topComponents].sort((h,d)=>(d.totalMs??0)-(h.totalMs??0)||d.renders-h.renders).slice(0,n);for(const h of c){o.push(`| ${h.name} | ${(h.totalMs??0).toFixed(1)} | ${h.renders} | ${dominant(h.causes)} | ${h.parentName??"\u2014"} | ${(h.changedProps??[]).join(", ")||"\u2014"} |`)}if(t.topComponents.length>n){o.push(`_\u2026+${t.topComponents.length-n} more components_`)}const u=new Set(c.map(h=>dominant(h.causes)));const l=[...u].filter(h=>CAUSE_HINT[h]).map(h=>`\`${h}\` = ${CAUSE_HINT[h]}`);if(l.length){o.push("");o.push(l.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 formatDelta(e,n){const t=(l,h)=>{const d=h-l;return`${l} \u2192 ${h} (${d>0?"+":""}${d})`};const r=e.summary;const s=n.summary;const o=[`**vs "${e.label}"**`];o.push(`- total renders: ${t(r.totalRenders,s.totalRenders)}`);o.push(`- wasted: ${t(r.wastedRenders??0,s.wastedRenders??0)}`);const a=new Map(r.topComponents.map(l=>[l.name,l.renders]));const i=[];for(const l of s.topComponents.slice(0,20)){const h=a.get(l.name)??0;if(h!==l.renders)i.push(`${l.name} ${t(h,l.renders)}`);a.delete(l.name)}for(const[l,h]of a){if(h>0)i.push(`${l} ${t(h,0)}`)}if(i.length)o.push(`- per component: ${i.slice(0,10).join(" \xB7 ")}`);const c=s.totalRenders<r.totalRenders;const u=s.totalRenders>r.totalRenders;o.push(u?"\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 registerRenderTools(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:import_zod30.z.array(stepSchema2).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:import_zod30.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),label:import_zod30.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),compareToPrevious:import_zod30.z.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),top:import_zod30.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),deviceId:deviceIdInput}},async t=>{try{const r=n.requireProDevice(t.deviceId);const s=n.connectedDevices().find(b=>b.deviceId===r);const o=(s?.tools??[]).find(b=>b.toolId===UI_TOOL_ID2);if(!o){return fail11("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 fail11("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(UI_TOOL_ID2,"beginMeasurement",{},r);if(a&&a.ok===false){return fail11(`Can't measure renders: ${a.reason??"capture unavailable"}`)}await delay5(SETTLE_MS);const i=Date.now();const c=[];if(t.steps?.length){for(const b of t.steps){const p=b.repeat??1;for(let f=0;f<p;f++){if(b.type!==void 0){const v=b.into??b.tap;if(!v){c.push("a `type` step needs `into`");continue}const y=await n.callAction(UI_TOOL_ID2,"tapElement",{...selector2(v),text:b.type},r);if(!y?.tapped)c.push(y?.reason??`could not type into "${v}"`);else if(y.matched?.via&&y.matched.via!=="onChangeText"){c.push(`"${v}" is not a text input \u2014 it ran ${y.matched.via}, nothing was typed`)}}else if(b.tap){const v=await n.callAction(UI_TOOL_ID2,"tapElement",selector2(b.tap),r);if(!v?.tapped)c.push(v?.reason??`no element matched "${b.tap}"`)}if(b.waitMs)await delay5(b.waitMs)}}await delay5(SETTLE_MS)}else{await delay5(t.idleMs??DEFAULT_IDLE_MS)}const u=Date.now()-i;const l=await n.callAction(UI_TOOL_ID2,"endMeasurement",{},r);const h=l?.summary;if(!h){return fail11("The capture window closed without returning anything. If the app reloaded mid-measurement, try again.")}const d={label:t.label??(t.steps?.length?"interaction":"idle"),windowMs:u,summary:h};const m=lastCapture.get(r);lastCapture.set(r,d);const w=t.steps?.length?t.steps.map(b=>(b.type!==void 0?`type "${b.type}"`:`tap "${b.tap}"`)+(b.repeat?` \xD7${b.repeat}`:"")).join(", "):`idle ${(u/1e3).toFixed(1)}s`;const g=[`**measure_renders \u2014 ${w}**`,"",formatCapture(d,t.top??DEFAULT_TOP)];if(t.compareToPrevious){g.push("",m?formatDelta(m,d):"_No previous measurement on this device to compare against._")}if(c.length)g.push("",`\u26A0\uFE0F ${c.join("; ")}`);if(h.totalRenders===0){g.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){g.push("","_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._")}return text9(g.join("\n"))}catch(r){return fail11(errMsg9(r))}})}var import_node_fs6=__toESM(require("fs"));var import_node_path6=__toESM(require("path"));var WATCHED_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".json"]);var IGNORED_DIRS=new Set(["node_modules",".git",".expo","ios","android","dist","lib","build",".next","coverage","__snapshots__"]);var DEBOUNCE_MS=400;function resolveMode(e){const n=(e??"").trim().toLowerCase();if(n==="always"||n==="never"||n==="auto")return n;return"auto"}var EditWatcher=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=import_node_fs6.default.watch(e,{recursive:true},(t,r)=>{if(!r)return;const s=String(r);if(s.split(import_node_path6.default.sep).some(o=>IGNORED_DIRS.has(o)))return;if(!WATCHED_EXT.has(import_node_path6.default.extname(s)))return;this.noteEdit(s)});this.watcher.on("error",()=>this.stop())}catch{this.stop()}}noteEdit(e){const n=Date.now();if(n-this.lastEditAt>DEBOUNCE_MS)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`
280
357
 
281
358
  ---
282
359
  \u26A0\uFE0F **${e.length} source file${e.length===1?"":"s"} changed since anything was last checked on the device** (${n}${t}).
@@ -384,7 +461,7 @@ After a behavior change, repeat the affected interaction and check the expected
384
461
  Include restart or persistence checks when relevant to the change. Use appropriate static
385
462
  checks for documentation-only work. Report actual checks, observations and remaining limits.
386
463
  BUOY_VERIFY reminders track selected edits and tool calls; clearing a reminder is not a
387
- test result. Stay within the user's authorized task and preserve existing project work.`;var VERIFYING_TOOLS=new Set(["measure_renders","scenario_action","run_flow","run_benchmark_batch","get_triage","tap_element","describe_screen","get_console","get_network_requests","get_events","get_snapshot"]);function createMcpServer(e){const n=new import_mcp.McpServer({name:"buoy",version:VERSION},{capabilities:{tools:{}},instructions:SERVER_INSTRUCTIONS});editWatcher.start(process.cwd(),resolveMode(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((r,s,o)=>t(r,s,(async(...i)=>{const a=await o(...i);const c=a?.isError===true;if(VERIFYING_TOOLS.has(r)&&!c){editWatcher.markVerified();return a}const u=editWatcher.nudge();if(!u)return a;const l=a;if(Array.isArray(l?.content)){return{...l,content:[...l.content,{type:"text",text:u}]}}return a})));registerTriageTools(n,e);registerFlowTools(n,e);registerRenderTools(n,e);registerGenericTools(n,e);registerPerfMonitorTools(n,e);registerEventsTools(n,e);registerNetworkTools(n,e);registerReactQueryTools(n,e);registerZustandTools(n,e);registerJotaiTools(n,e);registerReduxTools(n,e);registerStorageTools(n,e);registerRoutesTools(n,e);registerConsoleTools(n,e);registerSentryTools(n,e);registerTimeMachineTools(n,e);registerClockTools(n,e);registerPermissionsTools(n,e);registerLifecycleTools(n,e);registerLocationTools(n,e);registerScenariosTools(n,e);registerScreenshotTools(n,e);registerCameraTools(n);registerWebBenchTools(n);registerInteractTools(n,e);registerJsThreadTools(n,e);registerImagesTools(n,e);registerAssetsTools(n,e);registerReloadTools(n,e);return n}var deviceIdField={deviceId:import_zod31.z.string().optional().describe('Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.')};var import_node_child_process6=require("child_process");var import_node_fs7=__toESM(require("fs"));var import_node_os5=__toESM(require("os"));var import_node_path7=__toESM(require("path"));function defaultEnv(){return{BUOY_VERIFY:"auto"}}var PUBLIC_REGISTRY="https://registry.npmjs.org";var PROBE_TIMEOUT_MS3=3e3;function runInit(e={}){const n=e.cwd??process.cwd();const t=e.skillSrcDir??import_node_path7.default.resolve(__dirname,"../skill");const r=e.log??(i=>process.stdout.write(i+"\n"));const s=resolveEntry(e,r);const o=[];upsertMcpConfig(import_node_path7.default.join(n,".mcp.json"),"mcpServers",s,n,o);import_node_fs7.default.mkdirSync(import_node_path7.default.join(n,".cursor"),{recursive:true});upsertMcpConfig(import_node_path7.default.join(n,".cursor","mcp.json"),"mcpServers",s,n,o);if(import_node_fs7.default.existsSync(import_node_path7.default.join(n,".vscode"))){upsertMcpConfig(import_node_path7.default.join(n,".vscode","mcp.json"),"servers",s,n,o)}installSkills(t,n,o);appendDebuggingGuide(import_node_path7.default.join(n,"CLAUDE.md"),n,o);appendDebuggingGuide(import_node_path7.default.join(n,"AGENTS.md"),n,o);r("Buoy MCP configuration written:");for(const i of o)r(" \u2022 "+i);r("");r("Next steps:");r(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");r(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");r(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");if(s.command==="npx"){r("");r(`The editor launches the server with \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);r(`Registry access may be needed. Re-run \`npx -y ${PACKAGE_NAME}@latest init\` to refresh configuration.`)}else if(!e.cliPath){r("");r(`This uses a pinned local install (no network on startup). To update it later,`);r(`To update the local package, run: npx -y ${s.args[0]?.replace(/dist[/\\]cli\.js$/,"")??""}@latest init --local`)}}function resolveEntry(e,n){if(e.cliPath){return{type:"stdio",command:"node",args:[e.cliPath],env:defaultEnv()}}const t=e.mode??"auto";if(t==="npx"){return npxEntry()}if(t==="local"){return localEntry(e,n)}const r=probeRegistry();if(r.reachable){return npxEntry()}n(`\u26A0 npm registry ${r.registry?`(${r.registry}) `:""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`);return localEntry(e,n)}function npxEntry(){return{type:"stdio",command:"npx",args:["-y",`${PACKAGE_NAME}@latest`],env:defaultEnv()}}function localEntry(e,n){const t=e.installDir??process.env.BUOY_MCP_HOME??import_node_path7.default.join(import_node_os5.default.homedir(),".buoy-mcp");const r=e.registry??PUBLIC_REGISTRY;const s=installLocalCopy(t,r,n);if(s){return{type:"stdio",command:"node",args:[s],env:defaultEnv()}}n("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");return npxEntry()}function installLocalCopy(e,n,t){const r=`${PACKAGE_NAME}@${VERSION==="0.0.0-dev"?"latest":VERSION}`;import_node_fs7.default.mkdirSync(e,{recursive:true});const s=process.platform==="win32"?"npm.cmd":"npm";t(`Installing ${r} into ${e} from ${n} \u2026`);const o=(0,import_node_child_process6.spawnSync)(s,["install",r,"--prefix",e,"--registry",n,`--@buoy-gg:registry=${n}`,"--no-audit","--no-fund","--loglevel=error"],{stdio:["ignore","ignore","inherit"],encoding:"utf8"});if(o.status!==0){t(`\u26A0 npm install exited with code ${o.status??"?"}${o.error?` (${o.error.message})`:""}.`);return null}const i=import_node_path7.default.join(e,"node_modules",PACKAGE_NAME,"dist","cli.js");if(!import_node_fs7.default.existsSync(i)){t(`\u26A0 install finished but ${i} is missing.`);return null}return i}function probeRegistry(){const e=effectiveRegistry();if(!e)return{reachable:false,registry:null};if(/^https?:\/\/registry\.npmjs\.org\/?$/i.test(e)){return{reachable:true,registry:e}}return{reachable:hostReachable(e),registry:e}}function effectiveRegistry(){const e=npmConfigGet("@buoy-gg:registry");if(e)return e;const n=npmConfigGet("registry");return n??PUBLIC_REGISTRY}function npmConfigGet(e){const n=process.platform==="win32"?"npm.cmd":"npm";const t=(0,import_node_child_process6.spawnSync)(n,["config","get",e],{encoding:"utf8",timeout:PROBE_TIMEOUT_MS3});if(t.status!==0||typeof t.stdout!=="string")return null;const r=t.stdout.trim();if(!r||r==="undefined"||r==="null")return null;return r}function hostReachable(e){const n=(0,import_node_child_process6.spawnSync)(process.execPath,["-e",REACHABILITY_PROBE,e,String(PROBE_TIMEOUT_MS3)],{encoding:"utf8",timeout:PROBE_TIMEOUT_MS3+2e3});return n.status===0}var REACHABILITY_PROBE=`
464
+ test result. Stay within the user's authorized task and preserve existing project work.`;var VERIFYING_TOOLS=new Set(["measure_renders","scenario_action","run_flow","run_benchmark_batch","get_triage","tap_element","describe_screen","get_console","get_network_requests","get_events","get_snapshot"]);function createMcpServer(e){const n=new import_mcp.McpServer({name:"buoy",version:VERSION},{capabilities:{tools:{}},instructions:SERVER_INSTRUCTIONS});editWatcher.start(process.cwd(),resolveMode(process.env.BUOY_VERIFY));const t=n.registerTool.bind(n);n.registerTool=((r,s,o)=>t(r,s,(async(...a)=>{const i=await o(...a);const c=i?.isError===true;if(VERIFYING_TOOLS.has(r)&&!c){editWatcher.markVerified();return i}const u=editWatcher.nudge();if(!u)return i;const l=i;if(Array.isArray(l?.content)){return{...l,content:[...l.content,{type:"text",text:u}]}}return i})));registerTriageTools(n,e);registerFlowTools(n,e);registerRenderTools(n,e);registerGenericTools(n,e);registerPerfMonitorTools(n,e);registerEventsTools(n,e);registerNetworkTools(n,e);registerReactQueryTools(n,e);registerZustandTools(n,e);registerJotaiTools(n,e);registerReduxTools(n,e);registerStorageTools(n,e);registerRoutesTools(n,e);registerConsoleTools(n,e);registerSentryTools(n,e);registerTimeMachineTools(n,e);registerClockTools(n,e);registerPermissionsTools(n,e);registerLifecycleTools(n,e);registerLocationTools(n,e);registerScenariosTools(n,e);registerScreenshotTools(n,e);registerCameraTools(n);registerWebBenchTools(n,e);registerInteractTools(n,e);registerJsThreadTools(n,e);registerImagesTools(n,e);registerAssetsTools(n,e);registerReloadTools(n,e);return n}var deviceIdField={deviceId:import_zod31.z.string().optional().describe('Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.')};var import_node_child_process7=require("child_process");var import_node_fs7=__toESM(require("fs"));var import_node_os6=__toESM(require("os"));var import_node_path7=__toESM(require("path"));function defaultEnv(){return{BUOY_VERIFY:"auto"}}var PUBLIC_REGISTRY="https://registry.npmjs.org";var PROBE_TIMEOUT_MS3=3e3;function runInit(e={}){const n=e.cwd??process.cwd();const t=e.skillSrcDir??import_node_path7.default.resolve(__dirname,"../skill");const r=e.log??(a=>process.stdout.write(a+"\n"));const s=resolveEntry(e,r);const o=[];upsertMcpConfig(import_node_path7.default.join(n,".mcp.json"),"mcpServers",s,n,o);import_node_fs7.default.mkdirSync(import_node_path7.default.join(n,".cursor"),{recursive:true});upsertMcpConfig(import_node_path7.default.join(n,".cursor","mcp.json"),"mcpServers",s,n,o);if(import_node_fs7.default.existsSync(import_node_path7.default.join(n,".vscode"))){upsertMcpConfig(import_node_path7.default.join(n,".vscode","mcp.json"),"servers",s,n,o)}installSkills(t,n,o);appendDebuggingGuide(import_node_path7.default.join(n,"CLAUDE.md"),n,o);appendDebuggingGuide(import_node_path7.default.join(n,"AGENTS.md"),n,o);r("Buoy MCP configuration written:");for(const a of o)r(" \u2022 "+a);r("");r("Next steps:");r(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");r(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");r(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");if(s.command==="npx"){r("");r(`The editor launches the server with \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);r(`Registry access may be needed. Re-run \`npx -y ${PACKAGE_NAME}@latest init\` to refresh configuration.`)}else if(!e.cliPath){r("");r(`This uses a pinned local install (no network on startup). To update it later,`);r(`To update the local package, run: npx -y ${s.args[0]?.replace(/dist[/\\]cli\.js$/,"")??""}@latest init --local`)}}function resolveEntry(e,n){if(e.cliPath){return{type:"stdio",command:"node",args:[e.cliPath],env:defaultEnv()}}const t=e.mode??"auto";if(t==="npx"){return npxEntry()}if(t==="local"){return localEntry(e,n)}const r=probeRegistry();if(r.reachable){return npxEntry()}n(`\u26A0 npm registry ${r.registry?`(${r.registry}) `:""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`);return localEntry(e,n)}function npxEntry(){return{type:"stdio",command:"npx",args:["-y",`${PACKAGE_NAME}@latest`],env:defaultEnv()}}function localEntry(e,n){const t=e.installDir??process.env.BUOY_MCP_HOME??import_node_path7.default.join(import_node_os6.default.homedir(),".buoy-mcp");const r=e.registry??PUBLIC_REGISTRY;const s=installLocalCopy(t,r,n);if(s){return{type:"stdio",command:"node",args:[s],env:defaultEnv()}}n("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");return npxEntry()}function installLocalCopy(e,n,t){const r=`${PACKAGE_NAME}@${VERSION==="0.0.0-dev"?"latest":VERSION}`;import_node_fs7.default.mkdirSync(e,{recursive:true});const s=process.platform==="win32"?"npm.cmd":"npm";t(`Installing ${r} into ${e} from ${n} \u2026`);const o=(0,import_node_child_process7.spawnSync)(s,["install",r,"--prefix",e,"--registry",n,`--@buoy-gg:registry=${n}`,"--no-audit","--no-fund","--loglevel=error"],{stdio:["ignore","ignore","inherit"],encoding:"utf8"});if(o.status!==0){t(`\u26A0 npm install exited with code ${o.status??"?"}${o.error?` (${o.error.message})`:""}.`);return null}const a=import_node_path7.default.join(e,"node_modules",PACKAGE_NAME,"dist","cli.js");if(!import_node_fs7.default.existsSync(a)){t(`\u26A0 install finished but ${a} is missing.`);return null}return a}function probeRegistry(){const e=effectiveRegistry();if(!e)return{reachable:false,registry:null};if(/^https?:\/\/registry\.npmjs\.org\/?$/i.test(e)){return{reachable:true,registry:e}}return{reachable:hostReachable(e),registry:e}}function effectiveRegistry(){const e=npmConfigGet("@buoy-gg:registry");if(e)return e;const n=npmConfigGet("registry");return n??PUBLIC_REGISTRY}function npmConfigGet(e){const n=process.platform==="win32"?"npm.cmd":"npm";const t=(0,import_node_child_process7.spawnSync)(n,["config","get",e],{encoding:"utf8",timeout:PROBE_TIMEOUT_MS3});if(t.status!==0||typeof t.stdout!=="string")return null;const r=t.stdout.trim();if(!r||r==="undefined"||r==="null")return null;return r}function hostReachable(e){const n=(0,import_node_child_process7.spawnSync)(process.execPath,["-e",REACHABILITY_PROBE,e,String(PROBE_TIMEOUT_MS3)],{encoding:"utf8",timeout:PROBE_TIMEOUT_MS3+2e3});return n.status===0}var REACHABILITY_PROBE=`
388
465
  const url = process.argv[1];
389
466
  const ms = Number(process.argv[2]) || 3000;
390
467
  const c = new AbortController();
@@ -397,7 +474,7 @@ fetch(url, { method: 'HEAD', signal: c.signal })
397
474
  clearTimeout(t);
398
475
  process.exit(1);
399
476
  });
400
- `;function upsertMcpConfig(e,n,t,r,s){let o={};if(import_node_fs7.default.existsSync(e)){try{o=JSON.parse(import_node_fs7.default.readFileSync(e,"utf8"))}catch{import_node_fs7.default.copyFileSync(e,e+".bak");s.push(`existing ${rel(e,r)} was invalid JSON \u2014 backed up to ${rel(e,r)}.bak`);o={}}}else{import_node_fs7.default.mkdirSync(import_node_path7.default.dirname(e),{recursive:true})}const i=o[n]&&typeof o[n]==="object"?o[n]:{};const a="buoy"in i;i.buoy=t;o[n]=i;import_node_fs7.default.writeFileSync(e,JSON.stringify(o,null,2)+"\n");s.push(`${a?"updated":"added"} \`buoy\` in ${rel(e,r)}`)}var GUIDE_HEADING="# Debugging with Buoy";var DEBUGGING_GUIDE=`${GUIDE_HEADING}
477
+ `;function upsertMcpConfig(e,n,t,r,s){let o={};if(import_node_fs7.default.existsSync(e)){try{o=JSON.parse(import_node_fs7.default.readFileSync(e,"utf8"))}catch{import_node_fs7.default.copyFileSync(e,e+".bak");s.push(`existing ${rel(e,r)} was invalid JSON \u2014 backed up to ${rel(e,r)}.bak`);o={}}}else{import_node_fs7.default.mkdirSync(import_node_path7.default.dirname(e),{recursive:true})}const a=o[n]&&typeof o[n]==="object"?o[n]:{};const i="buoy"in a;a.buoy=t;o[n]=a;import_node_fs7.default.writeFileSync(e,JSON.stringify(o,null,2)+"\n");s.push(`${i?"updated":"added"} \`buoy\` in ${rel(e,r)}`)}var GUIDE_HEADING="# Debugging with Buoy";var DEBUGGING_GUIDE=`${GUIDE_HEADING}
401
478
 
402
479
  Use Buoy to inspect a connected app when runtime evidence helps the task.
403
480
  Complete account, tool integration and connection setup first. MCP data and
@@ -430,12 +507,12 @@ only source review or a build was performed.
430
507
  reminders, \`always\` repeats until checked, and \`never\` disables them.
431
508
  A reminder is not a test result. Choose checks appropriate to the change;
432
509
  prose-only edits do not require running device actions.
433
- `;function appendDebuggingGuide(e,n,t){let r="";if(import_node_fs7.default.existsSync(e)){try{r=import_node_fs7.default.readFileSync(e,"utf8")}catch{t.push(`\u26A0 could not read ${rel(e,n)} \u2014 skipped the debugging guide`);return}if(r.includes(GUIDE_HEADING)){t.push(`${rel(e,n)} already has the "${GUIDE_HEADING}" block; preserved existing text. Review it against the current bundled guidance before relying on older instructions.`);return}}const s=r===""?"":r.endsWith("\n")?"\n":"\n\n";try{import_node_fs7.default.writeFileSync(e,r+s+DEBUGGING_GUIDE);t.push(`${r?"appended":"created"} "${GUIDE_HEADING}" block in ${rel(e,n)}`)}catch{t.push(`\u26A0 could not write ${rel(e,n)} \u2014 skipped the debugging guide`)}}function installSkills(e,n,t){if(!import_node_fs7.default.existsSync(e)){t.push("\u26A0 no bundled skills found \u2014 skipped");return}const r=import_node_fs7.default.readdirSync(e,{withFileTypes:true}).filter(s=>s.isDirectory()&&import_node_fs7.default.existsSync(import_node_path7.default.join(e,s.name,"SKILL.md")));if(r.length===0){t.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");return}for(const s of r){const o=import_node_path7.default.join(e,s.name);const i=import_node_path7.default.join(n,".claude","skills",s.name);copyDir(o,i);t.push(`installed the ${s.name} skill \u2192 ${rel(import_node_path7.default.join(i,"SKILL.md"),n)}`)}}function copyDir(e,n){import_node_fs7.default.mkdirSync(n,{recursive:true});for(const t of import_node_fs7.default.readdirSync(e,{withFileTypes:true})){const r=import_node_path7.default.join(e,t.name);const s=import_node_path7.default.join(n,t.name);if(t.isDirectory())copyDir(r,s);else import_node_fs7.default.copyFileSync(r,s)}}function rel(e,n){const t=import_node_path7.default.relative(n,e);return t&&!t.startsWith("..")?t:e}console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function main(){const e=process.env.BUOY_BROKER_PORT;const n=new SyncClient({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const t=createMcpServer(n);const r=new import_stdio.StdioServerTransport;let s=false;const o=i=>{if(s)return;s=true;process.stderr.write(`[buoy-mcp] shutting down (${i})
510
+ `;function appendDebuggingGuide(e,n,t){let r="";if(import_node_fs7.default.existsSync(e)){try{r=import_node_fs7.default.readFileSync(e,"utf8")}catch{t.push(`\u26A0 could not read ${rel(e,n)} \u2014 skipped the debugging guide`);return}if(r.includes(GUIDE_HEADING)){t.push(`${rel(e,n)} already has the "${GUIDE_HEADING}" block; preserved existing text. Review it against the current bundled guidance before relying on older instructions.`);return}}const s=r===""?"":r.endsWith("\n")?"\n":"\n\n";try{import_node_fs7.default.writeFileSync(e,r+s+DEBUGGING_GUIDE);t.push(`${r?"appended":"created"} "${GUIDE_HEADING}" block in ${rel(e,n)}`)}catch{t.push(`\u26A0 could not write ${rel(e,n)} \u2014 skipped the debugging guide`)}}function installSkills(e,n,t){if(!import_node_fs7.default.existsSync(e)){t.push("\u26A0 no bundled skills found \u2014 skipped");return}const r=import_node_fs7.default.readdirSync(e,{withFileTypes:true}).filter(s=>s.isDirectory()&&import_node_fs7.default.existsSync(import_node_path7.default.join(e,s.name,"SKILL.md")));if(r.length===0){t.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");return}for(const s of r){const o=import_node_path7.default.join(e,s.name);const a=import_node_path7.default.join(n,".claude","skills",s.name);copyDir(o,a);t.push(`installed the ${s.name} skill \u2192 ${rel(import_node_path7.default.join(a,"SKILL.md"),n)}`)}}function copyDir(e,n){import_node_fs7.default.mkdirSync(n,{recursive:true});for(const t of import_node_fs7.default.readdirSync(e,{withFileTypes:true})){const r=import_node_path7.default.join(e,t.name);const s=import_node_path7.default.join(n,t.name);if(t.isDirectory())copyDir(r,s);else import_node_fs7.default.copyFileSync(r,s)}}function rel(e,n){const t=import_node_path7.default.relative(n,e);return t&&!t.startsWith("..")?t:e}console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function main(){const e=process.env.BUOY_BROKER_PORT;const n=new SyncClient({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const t=createMcpServer(n);const r=new import_stdio.StdioServerTransport;let s=false;const o=a=>{if(s)return;s=true;process.stderr.write(`[buoy-mcp] shutting down (${a})
434
511
  `);setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await n.stop().catch(()=>{});await t.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>o("stdin closed"));process.stdin.once("close",()=>o("stdin closed"));t.server.onclose=()=>o("transport closed");process.on("SIGINT",()=>o("SIGINT"));process.on("SIGTERM",()=>o("SIGTERM"));process.on("SIGHUP",()=>o("SIGHUP"));await t.connect(r);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
435
- `);n.start().then(i=>process.stderr.write(`[buoy-mcp] broker ${i}
436
- `)).catch(i=>process.stderr.write(`[buoy-mcp] broker connection failed: ${i?.message??i} \u2014 tools will report no devices until it recovers
437
- `))}if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const n=e.find(a=>!a.startsWith("--"));if(!n)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:t}=await import("fs");const{runWebBench:r,formatWebBenchMarkdown:s}=await Promise.resolve().then(()=>(init_webBench(),webBench_exports));const o=JSON.parse(t(n,"utf8"));const i=await r({...o,durationMs:o.durationMs??o.perCaseDurationMs},a=>process.stderr.write(`[web-bench] ${a}
438
- `));process.stdout.write((e.includes("--json")?JSON.stringify(i,null,2):s(i))+"\n");process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] web-bench failed: ${e?.message??e}
439
- `);process.exit(1)}})()}else if(process.argv[2]==="init"){try{const e=process.argv.slice(3);let n="auto";if(e.includes("--local"))n="local";else if(e.includes("--npx"))n="npx";const t=e.indexOf("--registry");const r=t!==-1?e[t+1]:void 0;runInit({mode:n,registry:r});process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] init failed: ${e?.message??e}
512
+ `);n.start().then(a=>process.stderr.write(`[buoy-mcp] broker ${a}
513
+ `)).catch(a=>process.stderr.write(`[buoy-mcp] broker connection failed: ${a?.message??a} \u2014 tools will report no devices until it recovers
514
+ `))}async function brokerHasProApp(){const e=process.env.BUOY_BROKER_PORT;const n=new SyncClient({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:false});try{const t=await Promise.race([n.start().then(()=>true,()=>false),new Promise(r=>setTimeout(()=>r(false),4e3))]);if(!t)return false;for(let r=0;r<10;r++){if(n.connectedDevices().some(s=>s.tier==="pro"))return true;await new Promise(s=>setTimeout(s,300))}return false}finally{await Promise.race([n.stop().catch(()=>{}),new Promise(t=>setTimeout(t,2e3))])}}if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const n=e.find(l=>!l.startsWith("--"));if(!n)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:t}=await import("fs");const{runWebBench:r,formatWebBenchMarkdown:s}=await Promise.resolve().then(()=>(init_webBench(),webBench_exports));const{readEntitlement:o}=await Promise.resolve().then(()=>(init_camera(),camera_exports));if(!o().isPro&&!await brokerHasProApp()){throw new Error("web-bench is a Buoy Pro feature. Sign in to Buoy Desktop with a Pro account, or open an app signed in to Pro. See https://buoy.gg/pricing.")}const a=JSON.parse(t(n,"utf8"));const i=await r({...a,durationMs:a.durationMs??a.perCaseDurationMs},l=>process.stderr.write(`[web-bench] ${l}
515
+ `));const c=i.status==="failed"||i.browserLeftRunning?1:0;const u=(e.includes("--json")?JSON.stringify(i,null,2):s(i))+"\n";setTimeout(()=>process.exit(c),5e3).unref();process.stdout.write(u,()=>process.exit(c))}catch(e){process.stderr.write(`[buoy-mcp] web-bench failed: ${e?.message??e}
516
+ `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="init"){try{const e=process.argv.slice(3);let n="auto";if(e.includes("--local"))n="local";else if(e.includes("--npx"))n="npx";const t=e.indexOf("--registry");const r=t!==-1?e[t+1]:void 0;runInit({mode:n,registry:r});process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] init failed: ${e?.message??e}
440
517
  `);process.exit(1)}}else{main().catch(e=>{process.stderr.write(`[buoy-mcp] fatal: ${e?.message??e}
441
518
  `);process.exit(1)})}