@buoy-gg/mcp 7.0.68 → 7.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,16 @@ Run it from your app's directory. It registers a `buoy` server in your MCP confi
22
22
 
23
23
  The [MCP guide](https://buoy.gg/buoy/latest/docs/mcp) covers setup, what it captures and its limits. New to Buoy? Start with the [Quick start](https://buoy.gg/buoy/latest/docs/quick-start).
24
24
 
25
+ ## Buoy Web
26
+
27
+ Let your AI use the web in its own Chrome tabs. It's free and needs no account.
28
+
29
+ ```bash
30
+ npx -y @buoy-gg/mcp@latest init --web
31
+ ```
32
+
33
+ This adds a `buoy-web` server for all your projects. Then add the Buoy Web Chrome extension and restart your editor. It only uses tabs it opened, and it never types passwords or card numbers. See the [Buoy Web guide](https://buoy.gg/buoy/latest/docs/buoy-web).
34
+
25
35
 
26
36
  ## Read screens
27
37
 
@@ -0,0 +1,4 @@
1
+ import{createRequire as q}from"module";const fe=q(import.meta.url);import I from"fs";import R from"os";import y from"path";var G='// Buoy Web helper. Chrome starts it for the Buoy Web extension (native messaging) and\n// ends it when Chrome closes. It passes messages between the extension and Buoy MCP\n// sessions, which connect over a Unix socket only this user can open.\n//\n// extension <-- stdin/stdout (native messaging) --> helper <-- ~/.buoy/web/chrome-<pid>.sock --> MCP sessions\nimport fs from "node:fs";\nimport net from "node:net";\nimport os from "node:os";\nimport path from "node:path";\n\nconst dir = process.env.BUOY_WEB_DIR ?? path.join(os.homedir(), ".buoy", "web");\nfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\nfs.chmodSync(dir, 0o700);\nconst sock = path.join(dir, `chrome-${process.pid}.sock`);\nconst info = path.join(dir, `chrome-${process.pid}.json`);\n\n// ---- native messaging: 4-byte little-endian length, then JSON -----------------\nlet inbuf = Buffer.alloc(0);\nprocess.stdin.on("data", (chunk) => {\n inbuf = Buffer.concat([inbuf, chunk]);\n while (inbuf.length >= 4) {\n const n = inbuf.readUInt32LE(0);\n if (inbuf.length < 4 + n) break;\n const msg = JSON.parse(inbuf.subarray(4, 4 + n).toString("utf8"));\n inbuf = inbuf.subarray(4 + n);\n fromExtension(msg);\n }\n});\nprocess.stdin.on("end", quit); // Chrome closed or the extension let go\nprocess.stdout.on("error", quit);\nfunction toExtension(msg) {\n const body = Buffer.from(JSON.stringify(msg), "utf8");\n if (body.length > 1024 * 1024) throw new Error("Message too big for Chrome (over 1 MB).");\n const head = Buffer.alloc(4);\n head.writeUInt32LE(body.length, 0);\n process.stdout.write(Buffer.concat([head, body]));\n}\n\n// ---- MCP sessions: one JSON message per line ------------------------------------\nconst clients = new Map(); // id -> socket\nlet nextClient = 1;\nfunction fromExtension(msg) {\n const s = clients.get(msg.c);\n if (!s) return;\n const { c, ...rest } = msg;\n s.write(JSON.stringify(rest) + "\\n");\n}\n\nconst server = net.createServer((s) => {\n const c = nextClient++;\n clients.set(c, s);\n let buf = "";\n s.setEncoding("utf8");\n s.on("data", (d) => {\n buf += d;\n let i;\n while ((i = buf.indexOf("\\n")) >= 0) {\n const line = buf.slice(0, i);\n buf = buf.slice(i + 1);\n if (!line) continue;\n let msg;\n try {\n msg = JSON.parse(line);\n toExtension({ ...msg, c });\n } catch (e) {\n if (msg?.id != null) s.write(JSON.stringify({ id: msg.id, error: String(e.message ?? e) }) + "\\n");\n }\n }\n });\n const gone = () => {\n if (!clients.delete(c)) return;\n try {\n toExtension({ c, method: "client.closed" });\n } catch {}\n };\n s.on("close", gone);\n s.on("error", gone);\n});\n\n// Clear out sockets left by helpers that died without cleaning up.\nfor (const f of fs.readdirSync(dir)) {\n const m = /^chrome-(\\d+)\\.(sock|json)$/.exec(f);\n if (!m || Number(m[1]) === process.pid) continue;\n try {\n process.kill(Number(m[1]), 0);\n } catch {\n fs.rmSync(path.join(dir, f), { force: true });\n }\n}\n\nconst old = process.umask(0o177); // the socket file is created 0600\nserver.listen(sock, () => {\n process.umask(old);\n fs.writeFileSync(info, JSON.stringify({ pid: process.pid, sock, started: Date.now() }), { mode: 0o600 });\n // Tells Buoy the extension is set up, so a closed Chrome isn\'t mistaken for "no extension".\n fs.writeFileSync(path.join(dir, "seen"), new Date().toISOString(), { mode: 0o600 });\n toExtension({ ready: true }); // the extension shows "connected" only after this\n});\n\nfunction quit() {\n for (const f of [sock, info]) fs.rmSync(f, { force: true });\n process.exit(0);\n}\nprocess.on("SIGTERM", quit);\nprocess.on("SIGINT", quit);\n';import V from"crypto";import N from"fs";import X from"http";import K from"net";import z from"os";import U from"path";import{WebSocketServer as Q}from"ws";var Z=process.env.BUOY_WEB_RELAYLOG==="1";var M=(...n)=>Z&&process.stderr.write(`[buoy-web relay] ${n.join(" ")}
2
+ `);var x=()=>process.env.BUOY_WEB_DIR??U.join(z.homedir(),".buoy","web");function ee(n=x()){let e=[];try{e=N.readdirSync(n)}catch{return[]}const i=[];for(const o of e){const a=/^chrome-(\d+)\.json$/.exec(o);if(!a)continue;try{process.kill(Number(a[1]),0);const b=JSON.parse(N.readFileSync(U.join(n,o),"utf8"));if(N.existsSync(b.sock))i.push(b)}catch{}}return i.sort((o,a)=>a.started-o.started)}function te(n){return new Promise((e,i)=>{const o=K.createConnection(n);const a=new Map;let b=1;let l="";let d=false;let v=false;const w={onEvent:()=>{},onClose:()=>{},call(m,S){if(v)return Promise.reject(new Error("Chrome's Buoy helper closed."));const c=b++;o.write(JSON.stringify({id:c,method:m,params:S})+"\n");return new Promise((O,B)=>a.set(c,{res:O,rej:B}))},close:()=>o.end()};o.setEncoding("utf8");o.on("connect",()=>{d=true;e(w)});o.on("error",m=>{if(!d)i(m)});o.on("data",m=>{l+=m;let S;while((S=l.indexOf("\n"))>=0){const c=JSON.parse(l.slice(0,S));l=l.slice(S+1);if(c.id!=null&&a.has(c.id)){const{res:O,rej:B}=a.get(c.id);a.delete(c.id);if(c.error)B(new Error(c.error));else O(c.result)}else if(c.event)w.onEvent(c.event,c.params)}});o.on("close",()=>{v=true;for(const{rej:m}of a.values())m(new Error("Chrome's Buoy helper closed."));a.clear();w.onClose()})})}async function ne({sock:n}){const e=await te(n);const i=await e.call("hello");const o=new Map;let a=1;let b=false;let l=null;const d=new Set;const v=t=>{if(l?.readyState===1)l.send(JSON.stringify(t))};const w=t=>[...o.values()].find(t);const m={url:"",hello:i,onDetach:()=>{},onClose:()=>{},idle:()=>Promise.all([...d]),close:()=>{}};async function S(t,{settle:r=false}={}){const s=o.get(t);if(s)return s;await e.call("debugger.attach",{tabId:t});if(r){const E=Date.now();while(Date.now()-E<5e3){const T=await e.call("debugger.send",{tabId:t,method:"Runtime.evaluate",params:{expression:"[location.href, document.readyState]",returnByValue:true}}).catch(()=>null);const[D,L]=T?.result?.value??[];if(D&&D!=="about:blank"&&L!=="loading")break;await new Promise(H=>setTimeout(H,50))}}const{targetInfo:u}=await e.call("debugger.send",{tabId:t,method:"Target.getTargetInfo"});const{openerId:g,openerFrameId:f,...p}=u;p.canAccessOpener=false;const h={tabId:t,sessionId:`buoy-tab-${a++}`,targetInfo:p,children:new Set,framesSent:false,held:[]};o.set(t,h);v({method:"Target.attachedToTarget",params:{sessionId:h.sessionId,targetInfo:{...p,attached:true},waitingForDebugger:false}});return h}function c(t){const r=o.get(t);if(!r)return;o.delete(t);v({method:"Target.detachedFromTarget",params:{sessionId:r.sessionId,targetId:r.targetInfo?.targetId}})}e.onEvent=(t,r)=>{if(t==="debugger.event"){const s=o.get(r.tabId);if(!s)return;const u=r.params?.sessionId;if(r.method==="Target.attachedToTarget"&&u)s.children.add(u);if(r.method==="Target.detachedFromTarget"&&u)s.children.delete(u);const g={sessionId:r.sessionId||s.sessionId,method:r.method,params:r.params};if(!s.framesSent)s.held.push(g);else v(g)}else if(t==="debugger.detached"){c(r.tabId);m.onDetach(r)}else if(t==="tabs.created"){if(b){const s=S(r.id,{settle:true}).catch(()=>{});d.add(s);void s.finally(()=>d.delete(s))}}else if(t==="tabs.removed")c(r.tabId)};e.onClose=()=>{for(const t of[...o.keys()])c(t);l?.close();m.onClose()};async function O(t,r,s){if(!s){switch(t){case"Browser.getVersion":return{protocolVersion:"1.3",product:"Chrome/BuoyWeb",userAgent:i.userAgent,jsVersion:""};case"Browser.setDownloadBehavior":case"Target.setDiscoverTargets":case"Browser.close":return{};case"Target.setAutoAttach":b=true;return{};case"Target.getTargetInfo":return{targetInfo:{targetId:"buoy-browser",type:"browser",title:"",url:"",attached:true,canAccessOpener:false}};case"Target.getBrowserContexts":return{browserContextIds:[]};case"Target.createTarget":{const p=await e.call("tabs.create",{url:r?.url});const h=await S(p.id);return{targetId:h.targetInfo.targetId}}case"Target.closeTarget":{const p=w(h=>h.targetInfo?.targetId===r?.targetId);if(!p)return{success:false};await e.call("tabs.remove",{tabId:p.tabId});return{success:true}}}const f=o.values().next().value;if(!f)throw new Error(`No Buoy tab to run ${t} on.`);return e.call("debugger.send",{tabId:f.tabId,method:t,params:r})}if(t==="Target.getTargetInfo"){const f=w(p=>p.sessionId===s);if(f)return{targetInfo:f.targetInfo}}let u=w(f=>f.sessionId===s);let g;if(!u){u=w(f=>f.children.has(s));g=s}if(!u)throw new Error(`No Buoy tab for session ${s}.`);return e.call("debugger.send",{tabId:u.tabId,sessionId:g,method:t,params:r})}const B=V.randomUUID();const _=X.createServer((t,r)=>r.writeHead(404).end());const C=new Q({server:_});C.on("connection",(t,r)=>{if(r.url!==`/${B}`||l)return t.close(4004,"No.");l=t;t.on("message",async s=>{const{id:u,sessionId:g,method:f,params:p}=JSON.parse(s.toString());try{const h=await O(f,p,g);M("ok ",g??"-",f);t.send(JSON.stringify({id:u,sessionId:g,result:h??{}}));if(f==="Page.getFrameTree"){const E=w(T=>T.sessionId===g);if(E&&!E.framesSent){E.framesSent=true;for(const T of E.held.splice(0))v(T)}}}catch(h){M("ERR",g??"-",f,h.message);t.send(JSON.stringify({id:u,sessionId:g,error:{message:String(h.message??h)}}))}});t.on("close",()=>{l=null;e.close()})});await new Promise(t=>_.listen(0,"127.0.0.1",t));m.url=`ws://127.0.0.1:${_.address().port}/${B}`;m.close=()=>{e.close();C.close();_.close()};return m}var J="gg.buoy.web";var $=["cfolfiajhlenfifknbomdogooagnlamf","kjbgbjimhjiocnfifnemmenkondjeifp",...(process.env.BUOY_WEB_EXTENSION_IDS??"").split(",").filter(Boolean)];var A=["Google/Chrome","Google/Chrome Beta","Google/Chrome Dev","Google/Chrome Canary","Chromium","BraveSoftware/Brave-Browser","Microsoft Edge","Vivaldi","Arc/User Data"];function P(){if(process.env.BUOY_WEB_NMH_DIRS)return process.env.BUOY_WEB_NMH_DIRS.split(y.delimiter).filter(Boolean);if(process.platform!=="darwin")return[];const n=y.join(R.homedir(),"Library","Application Support");return A.map(e=>y.join(n,e)).filter(e=>I.existsSync(e)).map(e=>y.join(e,"NativeMessagingHosts"))}var j=(n,e,i)=>{try{if(I.readFileSync(n,"utf8")===e)return}catch{}I.writeFileSync(n,e,{mode:i});I.chmodSync(n,i)};function oe(){try{const n=y.join(x(),"helper");I.mkdirSync(n,{recursive:true,mode:448});const e=y.join(n,"helper.mjs");const i=y.join(n,"helper.sh");j(e,G,384);const o=d=>`'${d.replace(/'/g,`'\\''`)}'`;const a=process.env.BUOY_WEB_DIR?`BUOY_WEB_DIR=${o(process.env.BUOY_WEB_DIR)} `:"";j(i,`#!/bin/sh
3
+ ${a}exec ${o(process.execPath)} ${o(e)} "$@"
4
+ `,448);const b=JSON.stringify({name:J,description:"Buoy Web helper: lets the Buoy MCP use Buoy's own tabs through the Buoy Web extension.",path:i,type:"stdio",allowed_origins:$.map(d=>`chrome-extension://${d}/`)},null,2);const l=[];for(const d of P()){try{I.mkdirSync(d,{recursive:true});j(y.join(d,`${J}.json`),b+"\n",420);l.push(d)}catch{}}return l}catch{return[]}}function re(){if(process.env.BUOY_WEB_NMH_DIRS)return false;if(process.platform!=="darwin")return false;const n=y.join(R.homedir(),"Library","Application Support");for(const e of A){let i=[];try{i=I.readdirSync(y.join(n,e))}catch{continue}for(const o of i){if(o!=="Default"&&!/^Profile \d+$/.test(o))continue;if($.some(a=>I.existsSync(y.join(n,e,o,"Extensions",a))))return true}}return false}import se from"crypto";import k from"fs";import W from"path";var ie=process.env.BUOY_TELEMETRY_URL??"https://buoy.gg/api/t";var ae=24*60*60*1e3;function Y(){const n=(process.env.BUOY_TELEMETRY??"").toLowerCase();return n==="0"||n==="false"||n==="off"||process.env.DO_NOT_TRACK==="1"}var F=false;function ce(){if(F||Y())return;F=true;try{const n=W.join(x(),"install.json");let e={};try{e=JSON.parse(k.readFileSync(n,"utf8"))}catch{}if(typeof e.installId!=="string")e={installId:se.randomUUID(),lastPingAt:0};if(Date.now()-(e.lastPingAt??0)<ae)return;e.lastPingAt=Date.now();k.mkdirSync(W.dirname(n),{recursive:true,mode:448});k.writeFileSync(n,JSON.stringify(e),{mode:384});const i=JSON.stringify({installId:e.installId,version:"7.0.69",platform:process.platform==="darwin"?"macos":process.platform==="win32"?"windows":"linux",tier:"locked",surface:"web-mcp"});void fetch(ie,{method:"POST",headers:{"content-type":"application/json"},body:i,signal:AbortSignal.timeout(5e3)}).catch(()=>{})}catch{}}export{x as webDir,ee as findHelpers,ne as startRelay,P as helperManifestDirs,oe as ensureHelper,re as extensionInstalled,Y as telemetryOff,ce as noteUse};
@@ -1,4 +1,4 @@
1
- import{createRequire as F}from"module";const J=F(import.meta.url);import{FarmError as k,agentLabel as q,connectFarm as B,detectAgent as U,findDataContainer as E,nodeLauncher as $,readBuoyInstallId as O}from"./chunk-D4P6PPYP.mjs";import{readEntitlement as L}from"./chunk-OUCB2Y5J.mjs";import j from"os";import C from"path";import{z as o}from"zod";function x(t){if(L().admitted)return true;if(!t)return false;if(t.isAdmitted?.())return true;return t.connectedDevices().some(r=>r.isPro===true||r.tier==="free")}function P(t){return`\`${t}\` needs a Buoy account. A free one works.
1
+ import{createRequire as F}from"module";const J=F(import.meta.url);import{FarmError as k,agentLabel as q,connectFarm as B,detectAgent as U,findDataContainer as E,nodeLauncher as $,readBuoyInstallId as O}from"./chunk-TKDN24C2.mjs";import{readEntitlement as L}from"./chunk-OUCB2Y5J.mjs";import j from"os";import C from"path";import{z as o}from"zod";function x(t){if(L().admitted)return true;if(!t)return false;if(t.isAdmitted?.())return true;return t.connectedDevices().some(r=>r.isPro===true||r.tier==="free")}function P(t){return`\`${t}\` needs a Buoy account. A free one works.
2
2
 
3
3
  Tell the user to sign in to Buoy Desktop, or run \`npx buoy login\` in the project. Then run this again.`}var G=t=>({content:[{type:"text",text:t}]});var w=t=>({isError:true,content:[{type:"text",text:t}]});var c=t=>G(JSON.stringify(t,null,2));var R;var I=t=>x(R)?null:w(P(t));var v=null;var M=()=>void 0;function _(){if(!v){v=U({clientName:M()}).catch(()=>void 0).then(t=>B({label:process.env.BUOY_AGENT_LABEL||(t?q(t):"Buoy MCP"),agent:t,launcher:$(process.argv[1],["farmd"])})).catch(t=>{v=null;throw t})}return v}async function u(t,r,e,n){return(await _()).call(t,r,{timeoutMs:e,requestId:n})}var h=[];function A(t){for(let r=h.length-1;r>=0;r--)if(t(h[r]))h.splice(r,1)}async function N(t,r){const e=()=>h.find(s=>s.buoyDeviceId&&t.endsWith(s.buoyDeviceId));let n=e();if(!n&&h.length){await z().catch(()=>void 0);n=e()}if(!n)return null;try{return{checked:true,...await u("fairness",{leaseId:n.leaseId,epoch:n.epoch,windows:r},3e4)}}catch(s){if(s instanceof k&&s.code==="BAD_REQUEST"&&/Unknown method/.test(s.message)){return{checked:false,why:"This farm is too old to check fairness. Restart Buoy Desktop when the farm is idle."}}return{checked:false,why:s instanceof k?`${s.code}: ${s.message}`:String(s?.message??s)}}}async function z(){const t=await u("status");const r=new Set(h.map(e=>e.leaseId));for(const e of t.sims){if(!e.leaseId||!r.has(e.leaseId))continue;let n=h.find(p=>p.udid===e.udid);if(!n){const p=h.find(b=>b.leaseId===e.leaseId);n={...p,udid:e.udid,buoyDeviceId:void 0};h.push(n)}if(e.buoyDeviceId){n.buoyDeviceId=e.buoyDeviceId;continue}const s=t.templates.find(p=>p.id===e.templateId)?.bundleId;const i=C.join(j.homedir(),"Library/Developer/CoreSimulator/Devices",e.udid,"data");const a=s?await E(i,s):null;const l=a?await O(a):null;if(l)n.buoyDeviceId=l}}function y(t){if(t instanceof k&&t.code==="RUN_STOPPED"){return w('RUN_STOPPED: The person stopped this run in Buoy Desktop (reason=user). Stop work on it. Tell them: "You stopped this run in Sims." Start a new run only if they ask.')}if(t instanceof k)return w(`${t.code}: ${t.message}${t.next?`
4
- Next: ${t.next}`:""}`);return w(`Sim Farm error: ${t?.message??t}`)}var D={leaseId:o.string().describe("The lease id from sim_farm_acquire."),epoch:o.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};var S=o.string().optional().describe("Any unique string for this call. Repeating the same call with the same requestId returns the first result instead of acting twice.");var T=o.string().describe("The runId from sim_farm_acquire.");async function W(t,r,e){const n=new Map;const s=Date.now()+e;for(;;){for(const i of r){if(n.has(i))continue;const a=t.connectedDevices().find(l=>l.deviceId.endsWith(i)&&l.tier!=="checking");if(a)n.set(i,a.deviceId)}if(n.size===r.length||Date.now()>=s)return n;await new Promise(i=>setTimeout(i,500))}}function H(t,r){M=()=>t.server.getClientVersion()?.name;R=r;t.registerTool("sim_farm_status",{description:"Show the Sim Farm: how many farm simulators exist, which are awake or in use and by whom, free memory and disk, how many more sims fit, templates, leases, and recent events. Farm sims are slim iOS simulators (about 1.5 GB each instead of 5) that the farm makes from a template. Apps launch as a real phone (an iPhone SE, 2nd gen, by default): they see its RAM and cores and get its memory warnings. Each sim's `phone` shows the app's peak memory, and `overLimit: true` means a real phone would have closed the app. A safety cap closes an app that keeps growing. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:o.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:e})=>{try{return c(await u("status",{outside:!!e}))}catch(n){return y(n)}});t.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and, optionally, a seed folder (signed-in app data); clones are made from it in about 4 s each. Dev-client apps (Expo) need `launchUrl` so they open on Metro instead of the launcher. `grow` / `shrink` change how many sims exist. Use `dryRun` first: it returns the plan, what slim mode turns off (push, StoreKit, app links, widgets, Spotlight\u2026) and whether the Mac has room. You own the pool: Buoy Desktop has no size control. sim_farm_acquire makes sims as needed; `shrink` the ones you no longer need so they stop using disk space.",inputSchema:{mode:o.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size."),templateId:o.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:o.number().int().min(1).max(64).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:o.string().optional().describe("For create: path to a simulator .app build."),seedDir:o.string().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:o.string().optional().describe("For create: URL opened after each launch, e.g. exp+my-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081."),deviceType:o.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:o.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:o.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:o.number().int().min(1).max(65535).optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:o.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId:S}},async e=>{const n=I("sim_farm_setup");if(n)return n;try{if(e.mode==="create"){const a={appPath:e.appPath,seedDir:e.seedDir,deviceType:e.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:e.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:e.profile??"slim",metroPort:e.metroPort??8802,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(!a.appPath)return w("create needs appPath.");if(a.seedDir===void 0)return w("create needs seedDir, or seedDir: null for an app with no seed.");if(e.dryRun)return c({wouldCreateTemplate:a,budget:await u("budget",{ramGB:6,diskGB:4})});return c(await u("setup",a,15*6e4,e.requestId))}let s=e.templateId;if(!s){const a=await u("status");s=a.templates.filter(l=>l.usable).pop()?.id;if(!s)return w("No usable template yet. Run sim_farm_setup with mode create first.")}const i=e.count??1;if(e.dryRun){const a=await u("budget",{ramGB:0,diskGB:e.mode==="grow"?2*i:0});return c({mode:e.mode,templateId:s,count:i,roomForMoreSims:a.roomFor,ok:a.ok,reason:a.reason})}return c(await u(e.mode,{templateId:s,count:i},30*6e4,e.requestId))}catch(s){return y(s)}});t.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. The person watches your work live in Buoy Desktop, so give a clear `goal` and a `labels` entry per sim. Returns a run (runId, deadlineAt), a lease (leaseId + epoch) and exact sims (UDID, name, and buoyDeviceId once the app ran: the 8-character tail that its list_devices id ends with; with launch, also deviceId, the full id to pass to other tools). Parked sims are booted and new ones cloned as needed, within the Mac's memory and disk limits. This MCP keeps the lease alive while it runs; release it with sim_farm_finish when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run. While working, send sim_farm_report when a sim starts, passes or fails. The run stops at deadlineAt unless you ask for more time with sim_farm_extend. If any call returns RUN_STOPPED, the person stopped you: stop and tell them.",inputSchema:{count:o.number().int().min(1).max(64).describe("How many sims. There is no fixed cap: the farm gives as many as this Mac's memory and disk allow. Use allowPartial to take what fits."),goal:o.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'. With runId, the run's goal is used if you leave this out."),labels:o.array(o.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:o.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:o.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:o.string().optional().describe("Old name for goal. Use goal."),templateId:o.string().optional().describe("Defaults to the newest usable template."),launch:o.boolean().optional().describe("Launch the app on each sim and wait (up to about 30 s) until it joins Buoy. Each sim then has deviceId for other tools."),allowPartial:o.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:o.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:o.number().int().min(1).max(1440).optional().describe("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId:S}},async e=>{const n=I("sim_farm_acquire");if(n)return n;try{let s=e.templateId;if(!s){const d=await u("status");s=d.templates.filter(f=>f.usable).pop()?.id;if(!s)return w("No usable template yet. Run sim_farm_setup with mode create first.")}let i=(e.goal??e.purpose??"").trim();if(!i&&e.runId)i=((await u("status")).runs??[]).find(d=>d.id===e.runId)?.goal??"";if(!i)return w("Give a goal: one short sentence about what you are doing.");const a=await _();const l=await a.acquire({templateId:s,count:e.count,purpose:i,goal:i,labels:e.labels,kind:e.kind,runId:e.runId,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});for(const d of l.sims){const f=h.findIndex(g=>g.udid===d.udid);if(f>=0)h.splice(f,1);h.push({leaseId:l.leaseId,epoch:l.epoch,runId:l.runId,udid:d.udid,buoyDeviceId:d.buoyDeviceId??void 0})}let p=l;if(e.launch&&r){const d=l.sims.map(m=>m.buoyDeviceId).filter(m=>!!m);const f=d.length?await W(r,d,3e4):new Map;p={...l,sims:l.sims.map(m=>({...m,deviceId:m.buoyDeviceId&&f.get(m.buoyDeviceId)||null}))};const g=l.sims.filter(m=>m.buoyDeviceId&&!f.has(m.buoyDeviceId));if(g.length){p.warning=`The app started on ${g.map(m=>m.name).join(", ")} but has not joined Buoy after 30 s. Run list_devices again in a moment, or take a screenshot with sim_farm_action to see the screen.`}}const b=e.launch?l.sims.filter(d=>!d.buoyDeviceId):[];if(b.length){const d=(await u("status").catch(()=>null))?.templates?.find(g=>g.id===s);const f=d?.metroPort?` on port ${d.metroPort}`:"";return c({...p,warning:`The app did not report a Buoy id on ${b.map(g=>g.name).join(", ")}. It may still be loading, or Metro${f} may be down or broken. Check Metro${f}, then take a screenshot with sim_farm_action to see the screen.`})}return c(p)}catch(s){return y(s)}});t.registerTool("sim_farm_release",{description:"Give one lease's sims back. `fast` (default) stops the app and restores its seeded data in about 2 s; `full` rebuilds each sim from the template. Use `full` after anything changed OS settings (permissions, keychain, location) or after a crash. Releasing your last lease ends the run, unless `keepRun` is true. To end a run with a result, use sim_farm_finish instead.",inputSchema:{...D,reset:o.enum(["fast","full"]).optional(),keepRun:o.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async e=>{try{const n=await _();const s=await n.release(e.leaseId,e.epoch,e.reset??"fast",e.keepRun===true);A(i=>i.leaseId===e.leaseId);return c(s)}catch(n){return y(n)}});t.registerTool("sim_farm_action",{description:"Act on sims in your lease: `screenshot` (returns file paths), `open-url` (deep link, pass `url`), `launch` the app, `reset` (restore seeded app data), or `rebuild` (fresh clone; the UDID changes, the result maps old to new).",inputSchema:{...D,udids:o.array(o.string()).min(1).describe("UDIDs from your lease."),action:o.enum(["screenshot","open-url","launch","reset","rebuild"]),url:o.string().optional().describe("For open-url."),requestId:S}},async e=>{const n=I("sim_farm_action");if(n)return n;try{return c(await u("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.url},10*6e4,e.requestId))}catch(s){return y(s)}});t.registerTool("sim_farm_lock",{description:"Hold or free a Mac-wide lock tied to your lease. `perf-slot` is for fair speed tests: while you hold it the farm boots, clones and screenshots nothing else, and it records how busy the Mac is every second. It waits only for farm work in flight, not for other runs to end, so other sims and other apps may still use the Mac. run_benchmark_batch on your farm sim then marks each run fair or unfair from that record; run the unfair cases again. Measure only while you hold it. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...D,name:o.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:o.boolean().describe("true = take it, false = free it."),waitSeconds:o.number().int().min(0).max(1800).optional()}},async e=>{const n=I("sim_farm_lock");if(n)return n;try{const s={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return c(await u(e.hold?"lock":"unlock",s,(e.waitSeconds??0)*1e3+3e4))}catch(s){return y(s)}});t.registerTool("sim_farm_report",{description:"Tell the person how your run is going. They watch it live in Buoy Desktop. Send one when a sim starts work, passes, fails or needs help, with a short note. `counts` only when you know the full task list. Reports never add time. Pass an increasing `sequence` so a retry is never counted twice.",inputSchema:{runId:T,udid:o.string().optional().describe("The sim this is about. Leave out for the whole run."),state:o.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:o.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:o.object({done:o.number().int().min(0),total:o.number().int().min(0),passed:o.number().int().min(0).optional(),failed:o.number().int().min(0).optional()}).optional(),resultSource:o.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:o.number().int().min(0).optional()}},async e=>{try{return c(await u("report",e,3e4))}catch(n){return y(n)}});t.registerTool("sim_farm_extend",{description:"Ask for more time on your run before deadlineAt. The farm never goes past the run's cap (capAt). It says how much you got.",inputSchema:{runId:T,minutes:o.number().int().min(1).max(1440).describe("Minutes from now.")}},async e=>{try{return c(await u("extend",e))}catch(n){return y(n)}});t.registerTool("sim_farm_finish",{description:"End your run with a result. Gives back every sim it still holds. The person sees the summary in Buoy Desktop, so make it one or two plain sentences, e.g. 'Idea 3 won: 52 \u2192 58 fps.'",inputSchema:{runId:T,outcome:o.enum(["passed","failed","mixed","unknown"]).optional(),summary:o.string().max(600).optional()}},async e=>{try{const n=await u("finish",e,6e4,`fin-${e.runId}`);A(s=>s.runId===e.runId);return c(n)}catch(n){return y(n)}})}async function Y(){const{startFarmd:t}=await import("./src-N7WTCDAL.mjs");const r=await t({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!r)process.exit(0);for(const e of["SIGTERM","SIGINT"])process.on(e,()=>void r.stop(e))}export{x as hasBuoyAccount,P as accountRequired,N as farmFairness,H as registerSimFarmTools,Y as runFarmd};
4
+ Next: ${t.next}`:""}`);return w(`Sim Farm error: ${t?.message??t}`)}var D={leaseId:o.string().describe("The lease id from sim_farm_acquire."),epoch:o.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};var S=o.string().optional().describe("Any unique string for this call. Repeating the same call with the same requestId returns the first result instead of acting twice.");var T=o.string().describe("The runId from sim_farm_acquire.");async function W(t,r,e){const n=new Map;const s=Date.now()+e;for(;;){for(const i of r){if(n.has(i))continue;const a=t.connectedDevices().find(l=>l.deviceId.endsWith(i)&&l.tier!=="checking");if(a)n.set(i,a.deviceId)}if(n.size===r.length||Date.now()>=s)return n;await new Promise(i=>setTimeout(i,500))}}function H(t,r){M=()=>t.server.getClientVersion()?.name;R=r;t.registerTool("sim_farm_status",{description:"Show the Sim Farm: how many farm simulators exist, which are awake or in use and by whom, free memory and disk, how many more sims fit, templates, leases, and recent events. Farm sims are slim iOS simulators (about 1.5 GB each instead of 5) that the farm makes from a template. Apps launch as a real phone (an iPhone SE, 2nd gen, by default): they see its RAM and cores and get its memory warnings. Each sim's `phone` shows the app's peak memory, and `overLimit: true` means a real phone would have closed the app. A safety cap closes an app that keeps growing. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:o.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:e})=>{try{return c(await u("status",{outside:!!e}))}catch(n){return y(n)}});t.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and, optionally, a seed folder (signed-in app data); clones are made from it in about 4 s each. Dev-client apps (Expo) need `launchUrl` so they open on Metro instead of the launcher. `grow` / `shrink` change how many sims exist. Use `dryRun` first: it returns the plan, what slim mode turns off (push, StoreKit, app links, widgets, Spotlight\u2026) and whether the Mac has room. You own the pool: Buoy Desktop has no size control. sim_farm_acquire makes sims as needed; `shrink` the ones you no longer need so they stop using disk space.",inputSchema:{mode:o.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size."),templateId:o.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:o.number().int().min(1).max(64).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:o.string().optional().describe("For create: path to a simulator .app build."),seedDir:o.string().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:o.string().optional().describe("For create: URL opened after each launch, e.g. exp+my-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081."),deviceType:o.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:o.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:o.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:o.number().int().min(1).max(65535).optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:o.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId:S}},async e=>{const n=I("sim_farm_setup");if(n)return n;try{if(e.mode==="create"){const a={appPath:e.appPath,seedDir:e.seedDir,deviceType:e.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:e.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:e.profile??"slim",metroPort:e.metroPort??8802,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(!a.appPath)return w("create needs appPath.");if(a.seedDir===void 0)return w("create needs seedDir, or seedDir: null for an app with no seed.");if(e.dryRun)return c({wouldCreateTemplate:a,budget:await u("budget",{ramGB:6,diskGB:4})});return c(await u("setup",a,15*6e4,e.requestId))}let s=e.templateId;if(!s){const a=await u("status");s=a.templates.filter(l=>l.usable).pop()?.id;if(!s)return w("No usable template yet. Run sim_farm_setup with mode create first.")}const i=e.count??1;if(e.dryRun){const a=await u("budget",{ramGB:0,diskGB:e.mode==="grow"?2*i:0});return c({mode:e.mode,templateId:s,count:i,roomForMoreSims:a.roomFor,ok:a.ok,reason:a.reason})}return c(await u(e.mode,{templateId:s,count:i},30*6e4,e.requestId))}catch(s){return y(s)}});t.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. The person watches your work live in Buoy Desktop, so give a clear `goal` and a `labels` entry per sim. Returns a run (runId, deadlineAt), a lease (leaseId + epoch) and exact sims (UDID, name, and buoyDeviceId once the app ran: the 8-character tail that its list_devices id ends with; with launch, also deviceId, the full id to pass to other tools). Parked sims are booted and new ones cloned as needed, within the Mac's memory and disk limits. This MCP keeps the lease alive while it runs; release it with sim_farm_finish when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run. While working, send sim_farm_report when a sim starts, passes or fails. The run stops at deadlineAt unless you ask for more time with sim_farm_extend. If any call returns RUN_STOPPED, the person stopped you: stop and tell them.",inputSchema:{count:o.number().int().min(1).max(64).describe("How many sims. There is no fixed cap: the farm gives as many as this Mac's memory and disk allow. Use allowPartial to take what fits."),goal:o.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'. With runId, the run's goal is used if you leave this out."),labels:o.array(o.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:o.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:o.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:o.string().optional().describe("Old name for goal. Use goal."),templateId:o.string().optional().describe("Defaults to the newest usable template."),launch:o.boolean().optional().describe("Launch the app on each sim and wait (up to about 30 s) until it joins Buoy. Each sim then has deviceId for other tools."),allowPartial:o.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:o.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:o.number().int().min(1).max(1440).optional().describe("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId:S}},async e=>{const n=I("sim_farm_acquire");if(n)return n;try{let s=e.templateId;if(!s){const d=await u("status");s=d.templates.filter(f=>f.usable).pop()?.id;if(!s)return w("No usable template yet. Run sim_farm_setup with mode create first.")}let i=(e.goal??e.purpose??"").trim();if(!i&&e.runId)i=((await u("status")).runs??[]).find(d=>d.id===e.runId)?.goal??"";if(!i)return w("Give a goal: one short sentence about what you are doing.");const a=await _();const l=await a.acquire({templateId:s,count:e.count,purpose:i,goal:i,labels:e.labels,kind:e.kind,runId:e.runId,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});for(const d of l.sims){const f=h.findIndex(g=>g.udid===d.udid);if(f>=0)h.splice(f,1);h.push({leaseId:l.leaseId,epoch:l.epoch,runId:l.runId,udid:d.udid,buoyDeviceId:d.buoyDeviceId??void 0})}let p=l;if(e.launch&&r){const d=l.sims.map(m=>m.buoyDeviceId).filter(m=>!!m);const f=d.length?await W(r,d,3e4):new Map;p={...l,sims:l.sims.map(m=>({...m,deviceId:m.buoyDeviceId&&f.get(m.buoyDeviceId)||null}))};const g=l.sims.filter(m=>m.buoyDeviceId&&!f.has(m.buoyDeviceId));if(g.length){p.warning=`The app started on ${g.map(m=>m.name).join(", ")} but has not joined Buoy after 30 s. Run list_devices again in a moment, or take a screenshot with sim_farm_action to see the screen.`}}const b=e.launch?l.sims.filter(d=>!d.buoyDeviceId):[];if(b.length){const d=(await u("status").catch(()=>null))?.templates?.find(g=>g.id===s);const f=d?.metroPort?` on port ${d.metroPort}`:"";return c({...p,warning:`The app did not report a Buoy id on ${b.map(g=>g.name).join(", ")}. It may still be loading, or Metro${f} may be down or broken. Check Metro${f}, then take a screenshot with sim_farm_action to see the screen.`})}return c(p)}catch(s){return y(s)}});t.registerTool("sim_farm_release",{description:"Give one lease's sims back. `fast` (default) stops the app and restores its seeded data in about 2 s; `full` rebuilds each sim from the template. Use `full` after anything changed OS settings (permissions, keychain, location) or after a crash. Releasing your last lease ends the run, unless `keepRun` is true. To end a run with a result, use sim_farm_finish instead.",inputSchema:{...D,reset:o.enum(["fast","full"]).optional(),keepRun:o.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async e=>{try{const n=await _();const s=await n.release(e.leaseId,e.epoch,e.reset??"fast",e.keepRun===true);A(i=>i.leaseId===e.leaseId);return c(s)}catch(n){return y(n)}});t.registerTool("sim_farm_action",{description:"Act on sims in your lease: `screenshot` (returns file paths), `open-url` (deep link, pass `url`), `launch` the app, `reset` (restore seeded app data), or `rebuild` (fresh clone; the UDID changes, the result maps old to new).",inputSchema:{...D,udids:o.array(o.string()).min(1).describe("UDIDs from your lease."),action:o.enum(["screenshot","open-url","launch","reset","rebuild"]),url:o.string().optional().describe("For open-url."),requestId:S}},async e=>{const n=I("sim_farm_action");if(n)return n;try{return c(await u("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.url},10*6e4,e.requestId))}catch(s){return y(s)}});t.registerTool("sim_farm_lock",{description:"Hold or free a Mac-wide lock tied to your lease. `perf-slot` is for fair speed tests: while you hold it the farm boots, clones and screenshots nothing else, and it records how busy the Mac is every second. It waits only for farm work in flight, not for other runs to end, so other sims and other apps may still use the Mac. run_benchmark_batch on your farm sim then marks each run fair or unfair from that record; run the unfair cases again. Measure only while you hold it. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...D,name:o.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:o.boolean().describe("true = take it, false = free it."),waitSeconds:o.number().int().min(0).max(1800).optional()}},async e=>{const n=I("sim_farm_lock");if(n)return n;try{const s={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return c(await u(e.hold?"lock":"unlock",s,(e.waitSeconds??0)*1e3+3e4))}catch(s){return y(s)}});t.registerTool("sim_farm_report",{description:"Tell the person how your run is going. They watch it live in Buoy Desktop. Send one when a sim starts work, passes, fails or needs help, with a short note. `counts` only when you know the full task list. Reports never add time. Pass an increasing `sequence` so a retry is never counted twice.",inputSchema:{runId:T,udid:o.string().optional().describe("The sim this is about. Leave out for the whole run."),state:o.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:o.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:o.object({done:o.number().int().min(0),total:o.number().int().min(0),passed:o.number().int().min(0).optional(),failed:o.number().int().min(0).optional()}).optional(),resultSource:o.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:o.number().int().min(0).optional()}},async e=>{try{return c(await u("report",e,3e4))}catch(n){return y(n)}});t.registerTool("sim_farm_extend",{description:"Ask for more time on your run before deadlineAt. The farm never goes past the run's cap (capAt). It says how much you got.",inputSchema:{runId:T,minutes:o.number().int().min(1).max(1440).describe("Minutes from now.")}},async e=>{try{return c(await u("extend",e))}catch(n){return y(n)}});t.registerTool("sim_farm_finish",{description:"End your run with a result. Gives back every sim it still holds. The person sees the summary in Buoy Desktop, so make it one or two plain sentences, e.g. 'Idea 3 won: 52 \u2192 58 fps.'",inputSchema:{runId:T,outcome:o.enum(["passed","failed","mixed","unknown"]).optional(),summary:o.string().max(600).optional()}},async e=>{try{const n=await u("finish",e,6e4,`fin-${e.runId}`);A(s=>s.runId===e.runId);return c(n)}catch(n){return y(n)}})}async function Y(){const{startFarmd:t}=await import("./src-MZAB6TNP.mjs");const r=await t({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!r)process.exit(0);for(const e of["SIGTERM","SIGINT"])process.on(e,()=>void r.stop(e))}export{x as hasBuoyAccount,P as accountRequired,N as farmFairness,H as registerSimFarmTools,Y as runFarmd};