Socket
Book a DemoSign in
Socket

agent-device

Package Overview
Dependencies
Maintainers
1
Versions
71
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

agent-device - npm Package Compare versions

Comparing version
0.10.3
to
0.11.0
+3
dist/src/36.js
import{AsyncLocalStorage as e}from"node:async_hooks";import t,{createHash as r}from"node:crypto";import n,{existsSync as i,promises as a}from"node:fs";import o from"node:os";import s from"node:path";import{spawn as l,spawnSync as u}from"node:child_process";import{URL as d,fileURLToPath as c,pathToFileURL as p}from"node:url";let f=new e,m=/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key)/i,h=/(bearer\s+[a-z0-9._-]+|(?:api[_-]?key|token|secret|password)\s*[=:]\s*\S+)/i;function g(){return t.randomBytes(8).toString("hex")}async function w(e,r){let n={...e,diagnosticId:`${Date.now().toString(36)}-${t.randomBytes(4).toString("hex")}`,events:[]};return await f.run(n,r)}function I(){let e=f.getStore();return e?{diagnosticId:e.diagnosticId,requestId:e.requestId,session:e.session,command:e.command,debug:e.debug}:{}}function S(e){let t=f.getStore();if(!t)return;let r={ts:new Date().toISOString(),level:e.level??"info",phase:e.phase,session:t.session,requestId:t.requestId,command:t.command,durationMs:e.durationMs,data:e.data?A(e.data):void 0};if(t.events.push(r),!t.debug)return;let i=`[agent-device][diag] ${JSON.stringify(r)}
`;try{t.logPath&&n.appendFile(t.logPath,i,()=>{}),t.traceLogPath&&n.appendFile(t.traceLogPath,i,()=>{}),t.logPath||t.traceLogPath||process.stderr.write(i)}catch{}}async function v(e,t,r){let n=Date.now();try{let i=await t();return S({level:"info",phase:e,durationMs:Date.now()-n,data:r}),i}catch(t){throw S({level:"error",phase:e,durationMs:Date.now()-n,data:{...r??{},error:t instanceof Error?t.message:String(t)}}),t}}function y(e={}){let t=f.getStore();if(!t||!e.force&&!t.debug||0===t.events.length)return null;try{let e=(t.session??"default").replace(/[^a-zA-Z0-9._-]/g,"_"),r=new Date().toISOString().slice(0,10),i=s.join(o.homedir(),".agent-device","logs",e,r);n.mkdirSync(i,{recursive:!0});let a=new Date().toISOString().replace(/[:.]/g,"-"),l=s.join(i,`${a}-${t.diagnosticId}.ndjson`),u=t.events.map(e=>JSON.stringify(A(e)));return n.writeFileSync(l,`${u.join("\n")}
`),t.events=[],l}catch{return null}}function A(e){return function e(t,r,n){if(null==t)return t;if("string"==typeof t){var i=t,a=n;let e=i.trim();if(!e)return i;if(a&&m.test(a)||h.test(e))return"[REDACTED]";let r=function(e){try{let t=new URL(e);return t.search&&(t.search="?REDACTED"),(t.username||t.password)&&(t.username="REDACTED",t.password="REDACTED"),t.toString()}catch{return null}}(e);return r||(e.length>400?`${e.slice(0,200)}...<truncated>`:e)}if("object"!=typeof t)return t;if(r.has(t))return"[Circular]";if(r.add(t),Array.isArray(t))return t.map(t=>e(t,r));let o={};for(let[n,i]of Object.entries(t)){if(m.test(n)){o[n]="[REDACTED]";continue}o[n]=e(i,r,n)}return o}(e,new WeakSet)}class b extends Error{code;details;cause;constructor(e,t,r,n){super(t),this.code=e,this.details=r,this.cause=n}}function $(e){return e instanceof b?e:e instanceof Error?new b("UNKNOWN",e.message,void 0,e):new b("UNKNOWN","Unknown error",{err:e})}function E(e,t={}){let r=$(e),n=r.details?A(r.details):void 0,i=n&&"string"==typeof n.hint?n.hint:void 0,a=(n&&"string"==typeof n.diagnosticId?n.diagnosticId:void 0)??t.diagnosticId,o=(n&&"string"==typeof n.logPath?n.logPath:void 0)??t.logPath,s=i??function(e){switch(e){case"INVALID_ARGS":return"Check command arguments and run --help for usage examples.";case"SESSION_NOT_FOUND":return"Run open first or pass an explicit device selector.";case"TOOL_MISSING":return"Install required platform tooling and ensure it is available in PATH.";case"DEVICE_NOT_FOUND":return"Verify the target device is booted/connected and selectors match.";case"UNSUPPORTED_OPERATION":return"This command is not available for the selected platform/device.";case"COMMAND_FAILED":default:return"Retry with --debug and inspect diagnostics log for details.";case"UNAUTHORIZED":return"Refresh daemon metadata and retry the command."}}(r.code),l=function(e){if(!e)return;let t={...e};return delete t.hint,delete t.diagnosticId,delete t.logPath,Object.keys(t).length>0?t:void 0}(n),u=function(e,t,r){if("COMMAND_FAILED"!==e||r?.processExitError!==!0)return t;let n=function(e){let t=[/^an error was encountered processing the command/i,/^underlying error\b/i,/^simulator device failed to complete the requested operation/i];for(let r of e.split("\n")){let e=r.trim();if(e&&!t.some(t=>t.test(e)))return e.length>200?`${e.slice(0,200)}...`:e}return null}("string"==typeof r?.stderr?r.stderr:"");return n||t}(r.code,r.message,n);return{code:r.code,message:u,hint:s,diagnosticId:a,logPath:o,details:l}}let N="<wifi|airplane|location> <on|off>",D="appearance <light|dark|toggle>",x="faceid <match|nonmatch|enroll|unenroll>",_="touchid <match|nonmatch|enroll|unenroll>",T="fingerprint <match|nonmatch>",L="permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]",O="permission <grant|reset> <accessibility|screen-recording|input-monitoring>",M=`settings ${N} | settings ${D} | settings ${x} | settings ${_} | settings ${T} | settings ${L} | settings ${O}`,C=`settings requires ${N}, ${D}, ${x}, ${_}, ${T}, ${L}, or ${O}`,k=["app","frontmost-app","desktop","menubar"];function R(e){let t=e?.trim().toLowerCase();if("app"===t||"frontmost-app"===t||"desktop"===t||"menubar"===t)return t;throw new b("INVALID_ARGS",`Invalid surface: ${e}. Use ${k.join("|")}.`)}function P(e){var t;let r,n=B(e.label),i=B(e.value),a=B(e.identifier),o=(t=a)&&!/^[\w.]+:id\/[\w.-]+$/i.test(t)&&!/^_?NS:\d+$/i.test(t)?a:"";return(r=F(e.type??"")).includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||r.includes("textview")||r.includes("textarea")?i||n||o:n||i||o}function B(e){return"string"==typeof e?e.trim():""}function F(e){let t=e.trim().replace(/XCUIElementType/gi,"").replace(/^AX/,"").toLowerCase(),r=Math.max(t.lastIndexOf("."),t.lastIndexOf("/"));return -1!==r&&(t=t.slice(r+1)),t}function j(e,t={}){let r=[],n=[];for(let i of e){let e=i.depth??0;for(;r.length>0&&e<=r[r.length-1];)r.pop();let a=i.label?.trim()||i.value?.trim()||i.identifier?.trim()||"",o=U(i.type??"Element"),s="group"===o&&!a;s&&r.push(e);let l=s?e:Math.max(0,e-r.length);n.push({node:i,depth:l,type:o,text:G(i,l,s,o,t)})}return n}function G(e,t,r,n,i={}){var a,o,s,l,u;let d,c,p=n??U(e.type??"Element"),f=(d=P(e),{text:d,isLargeSurface:c=function(e,t){if("text-view"===t||"text-field"===t||"search"===t)return!0;let r=F(e.type??""),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("textview")||r.includes("textarea")||r.includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||n.includes("text area")||n.includes("text field")}(e,p),shouldSummarize:c&&!!(a=d)&&(a.length>80||/[\r\n]/.test(a))}),m=(o=e,s=p,l=i,u=f,l.summarizeTextSurfaces&&u.shouldSummarize&&function(e,t,r){let n=B(e.label);if(n&&n!==r)return n;let i=B(e.identifier);if(i&&!H(i)&&i!==r)return i;switch(t){case"text":case"text-view":return"Text view";case"text-field":return"Text field";case"search":return"Search field";default:return""}}(o,s,u.text)||z(o,s)),h=" ".repeat(t),g=e.ref?`@${e.ref}`:"",w=(function(e,t,r,n){let i,a=[];if(!1===e.enabled&&a.push("disabled"),!r.summarizeTextSurfaces||(!0===e.selected&&a.push("selected"),V(t)&&a.push("editable"),function(e,t){if("scroll-area"===t)return!0;let r=(e.type??"").toLowerCase(),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("scroll")||n.includes("scroll")}(e,t)&&a.push("scrollable"),!n.shouldSummarize))return a;return a.push(`preview:"${((i=n.text.replace(/\s+/g," ").trim()).length<=48?i:`${i.slice(0,45)}...`).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`),a.push("truncated"),[...new Set(a)]})(e,p,i,f).map(e=>` [${e}]`).join(""),I=m?` "${m}"`:"";return r?`${h}${g} [${p}]${w}`.trimEnd():`${h}${g} [${p}]${I}${w}`.trimEnd()}function z(e,t){let r=e.label?.trim(),n=e.value?.trim();if(V(t)){if(n)return n;if(r)return r}else if(r)return r;if(n)return n;let i=e.identifier?.trim();return!i||H(i)&&("group"===t||"image"===t||"list"===t||"collection"===t)?"":i}function U(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.includes(".")&&(t=t.replace(/^android\.widget\./,"").replace(/^android\.view\./,"").replace(/^android\.webkit\./,"").replace(/^androidx\./,"").replace(/^com\.google\.android\./,"").replace(/^com\.android\./,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":case"imagebutton":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"checkedtextview":return"text";case"textfield":case"edittext":return"text-field";case"textview":return r?"text":"text-view";case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":case"imageview":return"image";case"webview":return"webview";case"framelayout":case"linearlayout":case"relativelayout":case"constraintlayout":case"viewgroup":case"view":case"group":return"group";case"listview":case"recyclerview":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":case"scrollview":case"nestedscrollview":return"scroll-area";case"table":return"table";default:return t||"element"}}function V(e){return"text-field"===e||"text-view"===e||"search"===e}function H(e){return/^[\w.]+:id\/[\w.-]+$/i.test(e)}function q(){try{let e=J();return JSON.parse(n.readFileSync(s.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}function J(){let e=s.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=s.join(t,"package.json");if(n.existsSync(e))return t;t=s.dirname(t)}return e}function W(e){return e?{message:e}:{}}function K(e,t){return t?{...e,message:t}:e}function X(e){return"string"==typeof e?.message&&e.message.length>0?e.message:null}async function Z(e,t,r={}){return new Promise((n,i)=>{let a=l(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached}),o="",s=r.binaryStdout?Buffer.alloc(0):void 0,u="",d=!1,c=en(r.timeoutMs),p=c?setTimeout(()=>{d=!0,a.kill("SIGKILL")},c):null;r.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),void 0!==r.stdin&&a.stdin.write(r.stdin),a.stdin.end(),a.stdout.on("data",e=>{r.binaryStdout?s=Buffer.concat([s??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):o+=e}),a.stderr.on("data",e=>{u+=e}),a.on("error",r=>{(p&&clearTimeout(p),"ENOENT"===r.code)?i(new b("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new b("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),a.on("close",a=>{p&&clearTimeout(p);let l=a??1;d&&c?i(new b("COMMAND_FAILED",`${e} timed out after ${c}ms`,{cmd:e,args:t,stdout:o,stderr:u,exitCode:l,timeoutMs:c})):0===l||r.allowFailure?n({stdout:o,stderr:u,exitCode:l,stdoutBuffer:s}):i(new b("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:o,stderr:u,exitCode:l,processExitError:!0}))})})}async function Q(e){try{var t;let{shell:r,args:n}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),i=await Z(r,n,{allowFailure:!0});return 0===i.exitCode&&i.stdout.trim().length>0}catch{return!1}}function Y(e,t,r={}){let n=u(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],encoding:r.binaryStdout?void 0:"utf8",input:r.stdin,timeout:en(r.timeoutMs)});if(n.error){let i=n.error.code;if("ETIMEDOUT"===i)throw new b("COMMAND_FAILED",`${e} timed out after ${en(r.timeoutMs)}ms`,{cmd:e,args:t,timeoutMs:en(r.timeoutMs)},n.error);if("ENOENT"===i)throw new b("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n.error);throw new b("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n.error)}let i=r.binaryStdout?Buffer.isBuffer(n.stdout)?n.stdout:Buffer.from(n.stdout??""):void 0,a=r.binaryStdout?"":"string"==typeof n.stdout?n.stdout:(n.stdout??"").toString(),o="string"==typeof n.stderr?n.stderr:(n.stderr??"").toString(),s=n.status??1;if(0!==s&&!r.allowFailure)throw new b("COMMAND_FAILED",`${e} exited with code ${s}`,{cmd:e,args:t,stdout:a,stderr:o,exitCode:s,processExitError:!0});return{stdout:a,stderr:o,exitCode:s,stdoutBuffer:i}}function ee(e,t,r={}){l(e,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function et(e,t,r={}){return new Promise((n,i)=>{let a=l(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached});r.onSpawn?.(a);let o="",s="",u=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),void 0!==r.stdin&&a.stdin.write(r.stdin),a.stdin.end(),a.stdout.on("data",e=>{if(r.binaryStdout){u=Buffer.concat([u??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);o+=t,r.onStdoutChunk?.(t)}),a.stderr.on("data",e=>{let t=String(e);s+=t,r.onStderrChunk?.(t)}),a.on("error",r=>{"ENOENT"===r.code?i(new b("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new b("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),a.on("close",a=>{let l=a??1;0===l||r.allowFailure?n({stdout:o,stderr:s,exitCode:l,stdoutBuffer:u}):i(new b("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:o,stderr:s,exitCode:l,processExitError:!0}))})})}function er(e,t,r={}){let n=l(e,t,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"],detached:r.detached}),i="",a="";n.stdout.setEncoding("utf8"),n.stderr.setEncoding("utf8"),n.stdout.on("data",e=>{i+=e}),n.stderr.on("data",e=>{a+=e});let o=new Promise((o,s)=>{n.on("error",r=>{"ENOENT"===r.code?s(new b("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):s(new b("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),n.on("close",n=>{let l=n??1;0===l||r.allowFailure?o({stdout:i,stderr:a,exitCode:l}):s(new b("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:i,stderr:a,exitCode:l,processExitError:!0}))})});return{child:n,wait:o}}function en(e){if(!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t}let ei=[/(^|[\/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,/(^|[\/\s"'=])src\/daemon\.ts($|[\s"'])/];function ea(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function eo(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=Y("ps",["-p",String(e),"-o","lstart="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function es(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=Y("ps",["-p",String(e),"-o","command="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function el(e,t){let r;if(!ea(e))return!1;if(t){let r=eo(e);if(!r||r!==t)return!1}let n=es(e);return!!n&&!!(r=n.toLowerCase().replaceAll("\\","/")).includes("agent-device")&&ei.some(e=>e.test(r))}function eu(e,t){try{return process.kill(e,t),!0}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return!1;throw t}}async function ed(e,t){if(!ea(e))return!0;let r=Date.now();for(;Date.now()-r<t;)if(await new Promise(e=>setTimeout(e,50)),!ea(e))return!0;return!ea(e)}async function ec(e,t){!el(e,t.expectedStartTime)||!eu(e,"SIGTERM")||await ed(e,t.termTimeoutMs)||eu(e,"SIGKILL")&&await ed(e,t.killTimeoutMs)}function ep(e){return e?.HOME?.trim()||o.homedir()}function ef(e,t={}){return"~"===e?ep(t.env):e.startsWith("~/")?s.join(ep(t.env),e.slice(2)):e}function em(e,t={}){let r=ef(e,t);return s.isAbsolute(r)?r:s.resolve(t.cwd??process.cwd(),r)}function eh(e){let t,r=(t=(e??"").trim())?em(t):s.join(ef("~"),".agent-device");return{baseDir:r,infoPath:s.join(r,"daemon.json"),lockPath:s.join(r,"daemon.lock"),logPath:s.join(r,"daemon.log"),sessionsDir:s.join(r,"sessions")}}function eg(e){let t=(e??"").trim().toLowerCase();return"http"===t?"http":"dual"===t?"dual":"socket"}function ew(e){let t=(e??"").trim().toLowerCase();return"auto"===t?"auto":"socket"===t?"socket":"http"===t?"http":"auto"}function eI(e){return"tenant"===(e??"").trim().toLowerCase()?"tenant":"none"}function eS(e){if(!e)return;let t=e.trim();if(t&&/^[a-zA-Z0-9._-]{1,128}$/.test(t))return t}let ev=100,ey=new Set(["batch","replay"]),eA=new Set(["command","positionals","flags","runtime"]);function eb(e){let t;try{t=JSON.parse(e)}catch{throw new b("INVALID_ARGS","Batch steps must be valid JSON.")}if(!Array.isArray(t)||0===t.length)throw new b("INVALID_ARGS","Batch steps must be a non-empty JSON array.");return t}function e$(e,t){if(!Array.isArray(e)||0===e.length)throw new b("INVALID_ARGS","batch requires a non-empty batchSteps array.");if(e.length>t)throw new b("INVALID_ARGS",`batch has ${e.length} steps; max allowed is ${t}.`);let r=[];for(let t=0;t<e.length;t+=1){let n=e[t];if(!n||"object"!=typeof n)throw new b("INVALID_ARGS",`Invalid batch step at index ${t}.`);let i=Object.keys(n).filter(e=>!eA.has(e));if(i.length>0){let e=i.map(e=>`"${e}"`).join(", ");throw new b("INVALID_ARGS",`Batch step ${t+1} has unknown field(s): ${e}. Allowed fields: command, positionals, flags, runtime.`)}let a="string"==typeof n.command?n.command.trim().toLowerCase():"";if(!a)throw new b("INVALID_ARGS",`Batch step ${t+1} requires command.`);if(ey.has(a))throw new b("INVALID_ARGS",`Batch step ${t+1} cannot run ${a}.`);if(void 0!==n.positionals&&!Array.isArray(n.positionals))throw new b("INVALID_ARGS",`Batch step ${t+1} positionals must be an array.`);let o=n.positionals??[];if(o.some(e=>"string"!=typeof e))throw new b("INVALID_ARGS",`Batch step ${t+1} positionals must contain only strings.`);if(void 0!==n.flags&&("object"!=typeof n.flags||Array.isArray(n.flags)||!n.flags))throw new b("INVALID_ARGS",`Batch step ${t+1} flags must be an object.`);if(void 0!==n.runtime&&("object"!=typeof n.runtime||Array.isArray(n.runtime)||!n.runtime))throw new b("INVALID_ARGS",`Batch step ${t+1} runtime must be an object.`);r.push({command:a,positionals:o,flags:n.flags??{},runtime:n.runtime})}return r}function eE(e){let t=e.appId??e.bundleId??e.packageName;return{session:e.session,appId:t,appBundleId:e.bundleId,package:e.packageName}}function eN(e,t,r){return{deviceId:t,deviceName:r,..."android"===e?{serial:t}:"ios"===e?{udid:t}:{}}}function eD(e,t={}){let r=t.includeAndroidSerial??!0;return{platform:e.platform,target:e.target,device:e.name,id:e.id,..."ios"===e.platform?{device_udid:e.ios?.udid??e.id,ios_simulator_device_set:e.ios?.simulatorSetPath??null}:{},..."android"===e.platform&&r?{serial:e.android?.serial??e.id}:{}}}function ex(e){return{name:e.name,...eD(e.device,{includeAndroidSerial:!1}),createdAt:e.createdAt}}function e_(e){return{platform:e.platform,id:e.id,name:e.name,kind:e.kind,target:e.target,..."boolean"==typeof e.booted?{booted:e.booted}:{}}}function eT(e){let t=e.created?"Created":"Reused",r=e.booted?" (booted)":"";return K({udid:e.udid,device:e.device,runtime:e.runtime,ios_simulator_device_set:e.iosSimulatorDeviceSet??null,created:e.created,booted:e.booted},`${t}: ${e.device} ${e.udid}${r}`)}function eL(e){return e.bundleId??e.package??e.app}function eO(e){return K({app:e.app,appPath:e.appPath,platform:e.platform,...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.package?{package:e.package}:{}},`Installed: ${eL(e)}`)}function eM(e){return e.appName??e.bundleId??e.packageName??e.launchTarget}function eC(e){return K({launchTarget:e.launchTarget,...e.appName?{appName:e.appName}:{},...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.packageName?{package:e.packageName}:{},...e.installablePath?{installablePath:e.installablePath}:{},...e.archivePath?{archivePath:e.archivePath}:{},...e.materializationId?{materializationId:e.materializationId}:{},...e.materializationExpiresAt?{materializationExpiresAt:e.materializationExpiresAt}:{}},`Installed: ${eM(e)}`)}function ek(e){let t=e.appName??e.appBundleId??e.session;return K({session:e.session,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{},...e.startup?{startup:e.startup}:{},...e.runtime?{runtime:e.runtime}:{},...e.device?eD(e.device):{}},t?`Opened: ${t}`:"Opened")}function eR(e){return{session:e.session,...e.shutdown?{shutdown:e.shutdown}:{},...W(e.session?`Closed: ${e.session}`:"Closed")}}function eP(e){return{nodes:e.nodes,truncated:e.truncated,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{}}}export{TextDecoder,styleText}from"node:util";export{default as node_net}from"node:net";export{default as node_http}from"node:http";export{default as node_https}from"node:https";export{b as AppError,ev as DEFAULT_BATCH_MAX_STEPS,k as SESSION_SURFACES,C as SETTINGS_INVALID_ARGS_MESSAGE,M as SETTINGS_USAGE_OVERRIDE,$ as asAppError,eE as buildAppIdentifiers,eN as buildDeviceIdentifiers,j as buildSnapshotDisplayLines,r as createHash,g as createRequestId,z as displayLabel,S as emitDiagnostic,i as existsSync,ef as expandUserHomePath,d as external_node_url_URL,P as extractReadableText,c as fileURLToPath,J as findProjectRoot,y as flushDiagnosticsToSessionFile,U as formatRole,G as formatSnapshotLine,I as getDiagnosticsMeta,el as isAgentDeviceDaemonProcess,ea as isProcessAlive,t as node_crypto,n as node_fs,o as node_os,s as node_path,E as normalizeError,eS as normalizeTenantId,eb as parseBatchStepsJson,R as parseSessionSurface,p as pathToFileURL,a as promises,X as readCommandMessage,es as readProcessCommand,eo as readProcessStartTime,q as readVersion,eh as resolveDaemonPaths,eg as resolveDaemonServerMode,ew as resolveDaemonTransportPreference,eL as resolveDeployResultTarget,eM as resolveInstallFromSourceResultTarget,eI as resolveSessionIsolationMode,em as resolveUserPath,Z as runCmd,er as runCmdBackground,ee as runCmdDetached,et as runCmdStreaming,Y as runCmdSync,eR as serializeCloseResult,eO as serializeDeployResult,e_ as serializeDevice,eT as serializeEnsureSimulatorResult,eC as serializeInstallFromSourceResult,ek as serializeOpenResult,ex as serializeSessionListEntry,eP as serializeSnapshotResult,l as spawn,ec as stopProcessForTakeover,W as successText,e$ as validateAndNormalizeBatchSteps,Q as whichCmd,v as withDiagnosticTimer,w as withDiagnosticsScope,K as withSuccessText};
# iOS Runner Protocol
The Apple runner speaks a small internal HTTP+JSON protocol between the TypeScript daemon and the XCUITest host. This protocol is a maintainer document, not part of the public user docs, but it should stay explicit so the TypeScript and Swift sides do not drift.
## Transport
- Endpoint: `POST /command`
- Content type: `application/json`
- Request body: one JSON command object
- Response body: one JSON envelope
The daemon probes `http://127.0.0.1:<port>/command` for simulator and desktop flows, and can use a tunneled device address for physical iOS/tvOS devices before falling back to localhost.
## Request Shape
Every request includes a `command` field. Additional fields depend on the command family.
Examples:
```json
{ "command": "tap", "x": 120, "y": 240 }
```
```json
{
"command": "snapshot",
"interactiveOnly": true,
"compact": true,
"depth": 2,
"scope": "app",
"raw": false
}
```
```json
{ "command": "recordStart", "outPath": "/tmp/demo.mp4", "fps": 30 }
```
The current command names are defined in:
- [`../src/platforms/ios/runner-client.ts`](../src/platforms/ios/runner-client.ts)
- [`AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift`](AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift)
## Response Shape
Successful and failed responses use the same top-level envelope:
```json
{
"ok": true,
"data": {
"message": "ok"
}
}
```
```json
{
"ok": false,
"error": {
"code": "UNSUPPORTED_OPERATION",
"message": "Unable to dismiss the iOS keyboard without a native dismiss gesture or control"
}
}
```
`data` is command-specific. Common fields include snapshot nodes, text lookup results, gesture timing, visibility metadata, and screenshot or recording output details.
## Maintenance Rules
- Treat the TypeScript and Swift wire models as a single contract.
- When adding, removing, or renaming a command, update the protocol fixtures/tests in the same change.
- Keep this file focused on the actual wire shape rather than implementation details of command execution.
+8
-0

@@ -286,8 +286,12 @@ declare type AgentDeviceClient = {

intervalMs?: number;
delayMs?: number;
holdMs?: number;
jitterPx?: number;
pixels?: number;
doubleTap?: boolean;
clickButton?: 'primary' | 'secondary' | 'middle';
backMode?: 'in-app' | 'system';
pauseMs?: number;
pattern?: 'one-way' | 'ping-pong';
maxScrolls?: number;
activity?: string;

@@ -305,2 +309,6 @@ header?: string[];

replayUpdate?: boolean;
failFast?: boolean;
timeoutMs?: number;
retries?: number;
artifactsDir?: string;
steps?: string;

@@ -307,0 +315,0 @@ stepsFile?: string;

+84
-16

@@ -425,16 +425,49 @@ import XCTest

}
let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
let target: XCUIElement?
if let x = command.x, let y = command.y {
target = textInputAt(app: activeApp, x: x, y: y) ?? focusedTextInput(app: activeApp)
} else {
target = focusedTextInput(app: activeApp)
}
func typeIntoTarget(_ value: String) {
if let focused = target {
focused.typeText(value)
} else {
activeApp.typeText(value)
}
}
if command.clearFirst == true {
guard let focused = focusedTextInput(app: activeApp) else {
return Response(ok: false, error: ErrorPayload(message: "no focused text input to clear"))
guard let focused = target else {
let message =
(command.x != nil && command.y != nil)
? "no text input found at the provided coordinates to clear"
: "no focused text input to clear"
return Response(ok: false, error: ErrorPayload(message: message))
}
clearTextInput(focused)
focused.typeText(text)
return Response(ok: true, data: DataPayload(message: "typed"))
}
if let focused = focusedTextInput(app: activeApp) {
focused.typeText(text)
if delaySeconds > 0 && text.count > 1 {
let chunks = Array(text)
for (index, character) in chunks.enumerated() {
typeIntoTarget(String(character))
if index + 1 < chunks.count {
Thread.sleep(forTimeInterval: delaySeconds)
}
}
} else {
activeApp.typeText(text)
typeIntoTarget(text)
}
return Response(ok: true, data: DataPayload(message: "typed"))
case .interactionFrame:
let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
return Response(
ok: true,
data: DataPayload(
x: frame.minX,
y: frame.minY,
referenceWidth: frame.width,
referenceHeight: frame.height
)
)
case .swipe:

@@ -444,8 +477,14 @@ guard let direction = command.direction else {

}
let referenceFrame = resolvedGestureReferenceFrame(app: activeApp)
var executedFrame: DragVisualizationFrame?
let timing = measureGesture {
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
swipe(app: activeApp, direction: direction)
executedFrame = swipe(
app: activeApp,
direction: direction
)
}
}
guard let dragFrame = executedFrame else {
return Response(ok: false, error: ErrorPayload(message: "swipe is only supported on tvOS"))
}
return Response(

@@ -457,4 +496,8 @@ ok: true,

gestureEndUptimeMs: timing.gestureEndUptimeMs,
referenceWidth: referenceFrame.referenceWidth,
referenceHeight: referenceFrame.referenceHeight
x: dragFrame.x,
y: dragFrame.y,
x2: dragFrame.x2,
y2: dragFrame.y2,
referenceWidth: dragFrame.referenceWidth,
referenceHeight: dragFrame.referenceHeight
)

@@ -516,8 +559,13 @@ )

#endif
case .back:
if tapNavigationBack(app: activeApp) {
return Response(ok: true, data: DataPayload(message: "back"))
case .back, .backInApp:
if tapInAppBackControl(app: activeApp) {
let message = command.command == .back ? "back" : "backInApp"
return Response(ok: true, data: DataPayload(message: message))
}
performBackGesture(app: activeApp)
return Response(ok: true, data: DataPayload(message: "back"))
return Response(ok: false, error: ErrorPayload(message: "in-app back control is not available"))
case .backSystem:
if performSystemBackAction(app: activeApp) {
return Response(ok: true, data: DataPayload(message: "backSystem"))
}
return Response(ok: false, error: ErrorPayload(message: "system back is not available"))
case .home:

@@ -529,2 +577,22 @@ pressHomeButton()

return Response(ok: true, data: DataPayload(message: "appSwitcher"))
case .keyboardDismiss:
let result = dismissKeyboard(app: activeApp)
if result.wasVisible && !result.dismissed {
return Response(
ok: false,
error: ErrorPayload(
code: "UNSUPPORTED_OPERATION",
message: "Unable to dismiss the iOS keyboard without a native dismiss gesture or control"
)
)
}
return Response(
ok: true,
data: DataPayload(
message: "keyboardDismiss",
visible: result.visible,
wasVisible: result.wasVisible,
dismissed: result.dismissed
)
)
case .alert:

@@ -531,0 +599,0 @@ let action = (command.action ?? "get").lowercased()

+139
-49

@@ -20,10 +20,5 @@ import XCTest

struct GestureReferenceFrame {
let referenceWidth: Double
let referenceHeight: Double
}
// MARK: - Navigation Gestures
func tapNavigationBack(app: XCUIApplication) -> Bool {
func tapInAppBackControl(app: XCUIApplication) -> Bool {
#if os(macOS)

@@ -41,3 +36,3 @@ if let back = macOSNavigationBackElement(app: app) {

}
return pressTvRemoteMenuIfAvailable()
return false
#endif

@@ -56,2 +51,14 @@ }

func performSystemBackAction(app: XCUIApplication) -> Bool {
#if os(macOS)
return false
#else
if pressTvRemoteMenuIfAvailable() {
return true
}
performBackGesture(app: app)
return true
#endif
}
func performAppSwitcherGesture(app: XCUIApplication) {

@@ -167,17 +174,112 @@ if performTvRemoteAppSwitcherIfAvailable() {

func textInputAt(app: XCUIApplication, x: Double, y: Double) -> XCUIElement? {
let point = CGPoint(x: x, y: y)
var matched: XCUIElement?
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
// Prefer the smallest matching field so nested editable controls win over large containers.
let candidates = app.descendants(matching: .any).allElementsBoundByIndex
.filter { element in
guard element.exists else { return false }
switch element.elementType {
case .textField, .secureTextField, .searchField, .textView:
let frame = element.frame
return !frame.isEmpty && frame.contains(point)
default:
return false
}
}
.sorted { left, right in
let leftArea = max(1, left.frame.width * left.frame.height)
let rightArea = max(1, right.frame.width * right.frame.height)
if leftArea != rightArea {
return leftArea < rightArea
}
if left.frame.minY != right.frame.minY {
return left.frame.minY < right.frame.minY
}
if left.frame.minX != right.frame.minX {
return left.frame.minX < right.frame.minX
}
return left.elementType.rawValue < right.elementType.rawValue
}
matched = candidates.first
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return nil
}
return matched
}
func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
let focused = app
.descendants(matching: .any)
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
.firstMatch
guard focused.exists else { return nil }
var focused: XCUIElement?
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
let candidate = app
.descendants(matching: .any)
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
.firstMatch
guard candidate.exists else { return }
switch focused.elementType {
case .textField, .secureTextField, .searchField, .textView:
return focused
default:
switch candidate.elementType {
case .textField, .secureTextField, .searchField, .textView:
focused = candidate
default:
return
}
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return nil
}
return focused
}
func isKeyboardVisible(app: XCUIApplication) -> Bool {
let keyboard = app.keyboards.firstMatch
return keyboard.exists && !keyboard.frame.isEmpty
}
func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
let wasVisible = isKeyboardVisible(app: app)
guard wasVisible else {
return (wasVisible: false, dismissed: false, visible: false)
}
let keyboard = app.keyboards.firstMatch
keyboard.swipeDown()
sleepFor(0.2)
if !isKeyboardVisible(app: app) {
return (wasVisible: true, dismissed: true, visible: false)
}
if tapKeyboardDismissControl(app: app) {
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
}
return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
}
private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
for label in ["Hide keyboard", "Dismiss keyboard"] {
let candidates = [
app.keyboards.buttons[label],
app.keyboards.keys[label],
app.toolbars.buttons[label],
]
if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
hittable.tap()
return true
}
}
return false
}
private func moveCaretToEnd(element: XCUIElement) {

@@ -329,3 +431,3 @@ let frame = element.frame

private func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
let window = app.windows.firstMatch

@@ -342,10 +444,2 @@ let windowFrame = window.frame

func resolvedGestureReferenceFrame(app: XCUIApplication) -> GestureReferenceFrame {
let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
return GestureReferenceFrame(
referenceWidth: frame.width,
referenceHeight: frame.height
)
}
func runSeries(count: Int, pauseMs: Double, operation: (Int) -> Void) {

@@ -362,35 +456,32 @@ let total = max(count, 1)

func swipe(app: XCUIApplication, direction: SwipeDirection) {
func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
if performTvRemoteSwipeIfAvailable(direction: direction) {
return
let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
let midX = frame.midX
let midY = frame.midY
return DragVisualizationFrame(
x: midX,
y: midY,
x2: midX,
y2: midY,
referenceWidth: frame.width,
referenceHeight: frame.height
)
}
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
let left = target.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
let right = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
switch direction {
case .up:
end.press(forDuration: 0.1, thenDragTo: start)
case .down:
start.press(forDuration: 0.1, thenDragTo: end)
case .left:
right.press(forDuration: 0.1, thenDragTo: left)
case .right:
left.press(forDuration: 0.1, thenDragTo: right)
}
return nil
}
private func performTvRemoteSwipeIfAvailable(direction: SwipeDirection) -> Bool {
private func performTvRemoteSwipeIfAvailable(direction: String) -> Bool {
#if os(tvOS)
switch direction {
case .up:
case "up":
XCUIRemote.shared.press(.up)
case .down:
case "down":
XCUIRemote.shared.press(.down)
case .left:
case "left":
XCUIRemote.shared.press(.left)
case .right:
case "right":
XCUIRemote.shared.press(.right)
default:
return false
}

@@ -471,3 +562,2 @@ return true

}
}

@@ -155,3 +155,3 @@ import XCTest

switch command.command {
case .findText, .readText, .snapshot, .screenshot:
case .interactionFrame, .findText, .readText, .snapshot, .screenshot:
return true

@@ -174,3 +174,14 @@ case .alert:

switch command {
case .tap, .longPress, .drag, .type, .swipe, .back, .appSwitcher, .pinch:
case
.tap,
.longPress,
.drag,
.type,
.swipe,
.back,
.backInApp,
.backSystem,
.appSwitcher,
.keyboardDismiss,
.pinch:
return true

@@ -177,0 +188,0 @@ default:

@@ -8,2 +8,3 @@ // MARK: - Wire Models

case longPress
case interactionFrame
case drag

@@ -18,4 +19,7 @@ case dragSeries

case back
case backInApp
case backSystem
case home
case appSwitcher
case keyboardDismiss
case alert

@@ -29,9 +33,2 @@ case pinch

enum SwipeDirection: String, Codable {
case up
case down
case left
case right
}
struct Command: Codable {

@@ -41,2 +38,3 @@ let command: CommandType

let text: String?
let delayMs: Int?
let clearFirst: Bool?

@@ -55,3 +53,3 @@ let action: String?

let durationMs: Double?
let direction: SwipeDirection?
let direction: String?
let scale: Double?

@@ -95,2 +93,5 @@ let outPath: String?

let currentUptimeMs: Double?
let visible: Bool?
let wasVisible: Bool?
let dismissed: Bool?

@@ -112,3 +113,6 @@ init(

referenceHeight: Double? = nil,
currentUptimeMs: Double? = nil
currentUptimeMs: Double? = nil,
visible: Bool? = nil,
wasVisible: Bool? = nil,
dismissed: Bool? = nil
) {

@@ -130,2 +134,5 @@ self.message = message

self.currentUptimeMs = currentUptimeMs
self.visible = visible
self.wasVisible = wasVisible
self.dismissed = dismissed
}

@@ -135,3 +142,9 @@ }

struct ErrorPayload: Codable {
let code: String?
let message: String
init(code: String? = nil, message: String) {
self.code = code
self.message = message
}
}

@@ -138,0 +151,0 @@

import XCTest
extension RunnerTests {
private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
.button,
.link,
.menuItem,
.other,
.staticText
]
private struct SnapshotTraversalContext {
let queryRoot: XCUIElement
let rootSnapshot: XCUIElementSnapshot

@@ -71,2 +80,14 @@ let viewport: CGRect

var cachedDescendantElements: [XCUIElement]?
func collapsedTabDescendants() -> [XCUIElement] {
if let cachedDescendantElements {
return cachedDescendantElements
}
let fetched = safeSnapshotElementsQuery {
context.queryRoot.descendants(matching: .any).allElementsBoundByIndex
}
cachedDescendantElements = fetched
return fetched
}
var nodes: [SnapshotNode] = []

@@ -78,2 +99,12 @@ var truncated = false

)
if context.maxDepth > 0 {
let didTruncateFallback = appendCollapsedTabFallbackNodes(
to: &nodes,
containerSnapshot: context.rootSnapshot,
resolveElements: collapsedTabDescendants,
depth: 1,
nodeLimit: fastSnapshotLimit
)
truncated = truncated || didTruncateFallback
}

@@ -124,2 +155,12 @@ var seen = Set<String>()

)
if visibleDepth < context.maxDepth {
let didTruncateFallback = appendCollapsedTabFallbackNodes(
to: &nodes,
containerSnapshot: snapshot,
resolveElements: collapsedTabDescendants,
depth: visibleDepth + 1,
nodeLimit: fastSnapshotLimit
)
truncated = truncated || didTruncateFallback
}

@@ -253,2 +294,3 @@ }

return SnapshotTraversalContext(
queryRoot: queryRoot,
rootSnapshot: rootSnapshot,

@@ -383,2 +425,181 @@ viewport: viewport,

}
private func appendCollapsedTabFallbackNodes(
to nodes: inout [SnapshotNode],
containerSnapshot: XCUIElementSnapshot,
resolveElements: () -> [XCUIElement],
depth: Int,
nodeLimit: Int
) -> Bool {
let fallbackNodes = collapsedTabFallbackNodes(
for: containerSnapshot,
resolveElements: resolveElements,
startingIndex: nodes.count,
depth: depth
)
if fallbackNodes.isEmpty { return false }
let remaining = max(0, nodeLimit - nodes.count)
if remaining == 0 { return true }
nodes.append(contentsOf: fallbackNodes.prefix(remaining))
return fallbackNodes.count > remaining
}
private func collapsedTabFallbackNodes(
for containerSnapshot: XCUIElementSnapshot,
resolveElements: () -> [XCUIElement],
startingIndex: Int,
depth: Int
) -> [SnapshotNode] {
if !containerSnapshot.children.isEmpty { return [] }
guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
let containerFrame = containerSnapshot.frame
if containerFrame.isNull || containerFrame.isEmpty { return [] }
// Collapsed tab containers should be rare, so a full descendant scan is acceptable once per
// snapshot as a fallback for XCTest omitting the tab children from the snapshot tree.
let elements = resolveElements()
let candidates = elements.compactMap { element in
collapsedTabCandidateNode(
element: element,
containerSnapshot: containerSnapshot,
containerFrame: containerFrame
)
}
.sorted { left, right in
if left.rect.x != right.rect.x {
return left.rect.x < right.rect.x
}
return left.rect.y < right.rect.y
}
if candidates.count < 2 { return [] }
let rowMidpoints = candidates.map { $0.rect.y + ($0.rect.height / 2) }
let rowSpread = (rowMidpoints.max() ?? 0) - (rowMidpoints.min() ?? 0)
// Allow modest vertical jitter and short two-row wraps while still rejecting unrelated controls.
if rowSpread > max(24.0, Double(containerFrame.height) * 0.6) { return [] }
var seen = Set<String>()
let uniqueCandidates = candidates.filter { node in
let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
if seen.contains(key) { return false }
seen.insert(key)
return true
}
if uniqueCandidates.count < 2 { return [] }
return uniqueCandidates.enumerated().map { offset, node in
SnapshotNode(
index: startingIndex + offset,
type: node.type,
label: node.label,
identifier: node.identifier,
value: node.value,
rect: node.rect,
enabled: node.enabled,
hittable: node.hittable,
depth: depth
)
}
}
private func collapsedTabCandidateNode(
element: XCUIElement,
containerSnapshot: XCUIElementSnapshot,
containerFrame: CGRect
) -> SnapshotNode? {
var node: SnapshotNode?
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
if !element.exists { return }
let elementType = element.elementType
if !Self.collapsedTabCandidateTypes.contains(elementType) { return }
let frame = element.frame
if frame.isNull || frame.isEmpty { return }
if frame.equalTo(containerFrame) { return }
let area = max(CGFloat(1), frame.width * frame.height)
let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
if area >= containerArea * 0.9 { return }
let center = CGPoint(x: frame.midX, y: frame.midY)
if !containerFrame.contains(center) { return }
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
let valueText = snapshotValueText(element)
let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
if !hasContent { return }
if sameSemanticElement(
containerSnapshot: containerSnapshot,
elementType: elementType,
label: label,
identifier: identifier
) {
return
}
node = SnapshotNode(
index: 0,
type: elementTypeName(elementType),
label: label.isEmpty ? nil : label,
identifier: identifier.isEmpty ? nil : identifier,
value: valueText,
rect: snapshotRect(from: frame),
enabled: element.isEnabled,
hittable: element.isHittable,
depth: 0
)
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return nil
}
return node
}
private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
let frame = snapshot.frame
if frame.isNull || frame.isEmpty { return false }
if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
switch snapshot.elementType {
case .tabBar, .segmentedControl, .slider:
return true
default:
return false
}
}
private func snapshotValueText(_ element: XCUIElement) -> String? {
let text = String(describing: element.value ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
private func sameSemanticElement(
containerSnapshot: XCUIElementSnapshot,
elementType: XCUIElement.ElementType,
label: String,
identifier: String
) -> Bool {
if containerSnapshot.elementType != elementType { return false }
let containerLabel = containerSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
let containerIdentifier = containerSnapshot.identifier
.trimmingCharacters(in: .whitespacesAndNewlines)
return containerLabel == label && containerIdentifier == identifier
}
private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
var elements: [XCUIElement] = []
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
elements = fetch()
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return []
}
return elements
}
}
# agent-device iOS Runner
This folder is reserved for the lightweight XCUITest runner used to provide element-level automation on iOS.
This folder contains the lightweight XCUITest runner used to provide element-level automation for Apple-family targets.
## Intent
- Provide a minimal XCTest target that exposes UI automation over a small HTTP server.

@@ -11,5 +12,13 @@ - Allow local builds via `xcodebuild` and caching for faster subsequent runs.

## Status
Planned for the automation layer. See `docs/ios-automation.md` and `docs/ios-runner-protocol.md`.
Current internal runner for iOS, tvOS, and macOS desktop automation.
Protocol and maintenance references:
- Protocol overview: [`RUNNER_PROTOCOL.md`](RUNNER_PROTOCOL.md)
- TypeScript client: [`../src/platforms/ios/runner-client.ts`](../src/platforms/ios/runner-client.ts)
- Swift wire models: [`AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift`](AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift)
## UITest Runner File Map
`AgentDeviceRunnerUITests/RunnerTests` is split into focused files to reduce context size for contributors and LLM agents.

@@ -27,1 +36,7 @@

- `RunnerTests+ScreenRecorder.swift`: nested `ScreenRecorder` implementation.
## Protocol Notes
- The daemon posts JSON commands to `POST /command` on the runner's local HTTP listener.
- The runner responds with a JSON envelope shaped as `{ ok, data?, error? }`.
- The protocol is internal to `agent-device`; when adding or renaming commands, update both wire models and the protocol tests/docs in the same change.
{
"name": "agent-device",
"version": "0.10.3",
"version": "0.11.0",
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",

@@ -34,9 +34,12 @@ "license": "MIT",

"format": "prettier --write src test skills",
"prepublishOnly": "pnpm build:all",
"prepack": "pnpm build:all",
"typecheck": "tsc -p tsconfig.json",
"test": "node --test",
"test:unit": "node --test src/__tests__/*.test.ts src/core/__tests__/*.test.ts src/daemon/__tests__/*.test.ts src/daemon/handlers/__tests__/*.test.ts src/platforms/**/__tests__/*.test.ts src/utils/**/__tests__/*.test.ts",
"test": "vitest run",
"test:unit": "vitest run",
"test:smoke": "node --test test/integration/smoke-*.test.ts",
"test:integration": "node --test test/integration/*.test.ts"
"test:integration": "node --test test/integration/*.test.ts",
"test:replay:ios": "node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator",
"test:replay:ios-device": "node --experimental-strip-types src/bin.ts test test/integration/replays/ios/device",
"test:replay:android": "node --experimental-strip-types src/bin.ts test test/integration/replays/android",
"test:replay:macos": "node --experimental-strip-types src/bin.ts test test/integration/replays/macos"
},

@@ -82,4 +85,5 @@ "files": [

"prettier": "^3.3.3",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.1.2"
}
}

@@ -29,2 +29,5 @@ <a href="https://www.callstack.com/open-source?utm_campaign=generic&utm_source=github&utm_medium=referral&utm_content=agent-device" align="center">

- Refs vs selectors: use refs for discovery, use selectors for durable replay and assertions.
- Tests: run deterministic `.ad` scripts as a light e2e test suite.
- Replay scripts: save `.ad` flows with `--save-script`, replay one script with `replay`, or run a folder/glob as a serial suite with `test`.
`test` supports metadata-aware retries up to 3 additional attempts, per-test timeouts, flaky pass reporting, and runner-managed artifacts under `.agent-device/test-artifacts` by default. Each attempt writes `replay.ad` and `result.txt`; failed attempts also keep copied logs and artifacts when available.
- Human docs vs agent skills: docs explain the system for people; skills provide compact operating guidance for agents.

@@ -42,2 +45,3 @@

agent-device fill @e5 "test"
agent-device fill @e5 "search" --delay-ms 80
agent-device close

@@ -54,2 +58,4 @@ ```

In non-JSON mode, core mutating commands print a short success acknowledgment so agents and humans can distinguish successful actions from dropped or silent no-ops.
## Where To Go Next

@@ -56,0 +62,0 @@

@@ -23,2 +23,3 @@ # Exploration

- User asks you to tap, type, or choose an element: `snapshot -i`, then act
- The on-screen keyboard is blocking the next step: `keyboard dismiss`; on iOS do this only while an app session is active, and use `keyboard status|get` only on Android
- UI does not expose the answer: say so plainly; do not browse or force the app into a new state unless asked

@@ -32,2 +33,3 @@

- `find`
- `keyboard status|get` on Android when keyboard visibility or input type matters

@@ -40,3 +42,5 @@ ## Interaction commands

- `type`
- `scrollintoview`
- `wait`
- `keyboard dismiss` when the keyboard obscures the next target

@@ -99,2 +103,4 @@ ## Most common mistake to avoid

- Use refs for discovery, debugging, and short local loops.
- Use `scrollintoview @ref` when the target is already known from the current snapshot and you want the command to re-snapshot after each swipe until the element reaches the viewport safe band.
- Cap long searches with `--max-scrolls <n>` when the list may be unbounded or the target may not exist.
- Use selectors for deterministic scripts, assertions, and replay-friendly actions.

@@ -117,2 +123,4 @@ - Prefer selector or `@ref` targeting over raw coordinates.

- Use `type` to append text to the current insertion point.
- If the keyboard blocks the next control after text entry, prefer `keyboard dismiss` instead of backing out of the screen.
- On iOS, `keyboard dismiss` depends on the active app session to keep the target app foregrounded, so do not rely on selector-only dismiss calls after closing or without `open`.
- Do not use `fill` or `type` just to make the app reveal information that is not currently visible unless the user asked for that interaction.

@@ -142,3 +150,4 @@

- visibility or presence claim: `is visible` or plain `snapshot`
- visibility claim for what is on-screen now: `is visible` or plain `snapshot`
- presence claim regardless of viewport visibility: `is exists`
- exact text, label, or value claim: `get text`

@@ -149,2 +158,6 @@ - post-action state change: act, then `wait`, then `is` or `get`

Notes:
- `wait text` is useful for synchronizing on text presence, but it is not the same as `is visible`.
Anti-hallucination rules:

@@ -220,2 +233,3 @@

- `flags` is optional and defaults to `{}`.
- Only `command`, `positionals`, `flags`, and `runtime` are accepted as top-level step keys.
- Nested `batch` and `replay` are rejected.

@@ -227,2 +241,3 @@ - Supported error mode is stop-on-first-error.

- Success returns fields such as `total`, `executed`, `totalDurationMs`, and `results[]`.
- Human-mode `batch` runs also print a short per-step success summary.
- Failed runs include `details.step`, `details.command`, `details.executed`, and `details.partialResults`.

@@ -229,0 +244,0 @@ - Replan from the first failing step instead of rerunning the whole flow blindly.

@@ -86,5 +86,10 @@ # Verification

agent-device replay -u ./session.ad
agent-device test ./smoke --platform android
```
- Prefer selector-based actions in recorded `.ad` replays.
- Use `test` when you already have multiple `.ad` flows and need a quick regression pass after updating or recording them.
- Keep the skill-level rule simple: use `replay -u` to maintain one script, use `test` to verify a folder or matcher of scripts.
- Treat `test` as a human and CI-facing suite runner that an agent can invoke for verification, not as the main source of product documentation.
- Failed runs keep suite artifacts under `.agent-device/test-artifacts` by default, which is usually enough for debugging without extra agent-side processing.
- Use update mode for maintenance, not as a substitute for fixing a broken interaction strategy.

@@ -91,0 +96,0 @@

@@ -51,2 +51,5 @@ ---

- Use `type` to append text.
- If the on-screen keyboard blocks the next step, prefer `keyboard dismiss` over navigation. On iOS, keep an app session open first; `keyboard status|get` remains Android-only.
- When a task asks to "go back", use plain `back` for predictable app-owned navigation and reserve `back --system` for platform back gestures or button semantics.
- Use `type --delay-ms` or `fill --delay-ms` for debounced search fields that drop characters when typed too quickly.
- If there is no simulator, no app install, or no open app session yet, switch to `bootstrap-install.md` instead of improvising setup steps.

@@ -53,0 +56,0 @@ - Use the smallest unblock action first when transient UI blocks inspection, but do not navigate, search, or enter new text just to make the UI reveal data unless the user asked for that interaction.

import{AsyncLocalStorage as e}from"node:async_hooks";import t,{createHash as r}from"node:crypto";import n,{existsSync as i,promises as o}from"node:fs";import s from"node:os";import a from"node:path";import{spawn as l,spawnSync as c}from"node:child_process";import{URL as u,fileURLToPath as d,pathToFileURL as f}from"node:url";let m=new e,p=/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key)/i,h=/(bearer\s+[a-z0-9._-]+|(?:api[_-]?key|token|secret|password)\s*[=:]\s*\S+)/i;function w(){return t.randomBytes(8).toString("hex")}async function g(e,r){let n={...e,diagnosticId:`${Date.now().toString(36)}-${t.randomBytes(4).toString("hex")}`,events:[]};return await m.run(n,r)}function S(){let e=m.getStore();return e?{diagnosticId:e.diagnosticId,requestId:e.requestId,session:e.session,command:e.command,debug:e.debug}:{}}function y(e){let t=m.getStore();if(!t)return;let r={ts:new Date().toISOString(),level:e.level??"info",phase:e.phase,session:t.session,requestId:t.requestId,command:t.command,durationMs:e.durationMs,data:e.data?I(e.data):void 0};if(t.events.push(r),!t.debug)return;let i=`[agent-device][diag] ${JSON.stringify(r)}
`;try{t.logPath&&n.appendFile(t.logPath,i,()=>{}),t.traceLogPath&&n.appendFile(t.traceLogPath,i,()=>{}),t.logPath||t.traceLogPath||process.stderr.write(i)}catch{}}async function v(e,t,r){let n=Date.now();try{let i=await t();return y({level:"info",phase:e,durationMs:Date.now()-n,data:r}),i}catch(t){throw y({level:"error",phase:e,durationMs:Date.now()-n,data:{...r??{},error:t instanceof Error?t.message:String(t)}}),t}}function A(e={}){let t=m.getStore();if(!t||!e.force&&!t.debug||0===t.events.length)return null;try{let e=(t.session??"default").replace(/[^a-zA-Z0-9._-]/g,"_"),r=new Date().toISOString().slice(0,10),i=a.join(s.homedir(),".agent-device","logs",e,r);n.mkdirSync(i,{recursive:!0});let o=new Date().toISOString().replace(/[:.]/g,"-"),l=a.join(i,`${o}-${t.diagnosticId}.ndjson`),c=t.events.map(e=>JSON.stringify(I(e)));return n.writeFileSync(l,`${c.join("\n")}
`),t.events=[],l}catch{return null}}function I(e){return function e(t,r,n){if(null==t)return t;if("string"==typeof t){var i=t,o=n;let e=i.trim();if(!e)return i;if(o&&p.test(o)||h.test(e))return"[REDACTED]";let r=function(e){try{let t=new URL(e);return t.search&&(t.search="?REDACTED"),(t.username||t.password)&&(t.username="REDACTED",t.password="REDACTED"),t.toString()}catch{return null}}(e);return r||(e.length>400?`${e.slice(0,200)}...<truncated>`:e)}if("object"!=typeof t)return t;if(r.has(t))return"[Circular]";if(r.add(t),Array.isArray(t))return t.map(t=>e(t,r));let s={};for(let[n,i]of Object.entries(t)){if(p.test(n)){s[n]="[REDACTED]";continue}s[n]=e(i,r,n)}return s}(e,new WeakSet)}class E extends Error{code;details;cause;constructor(e,t,r,n){super(t),this.code=e,this.details=r,this.cause=n}}function x(e){return e instanceof E?e:e instanceof Error?new E("UNKNOWN",e.message,void 0,e):new E("UNKNOWN","Unknown error",{err:e})}function D(e,t={}){let r=x(e),n=r.details?I(r.details):void 0,i=n&&"string"==typeof n.hint?n.hint:void 0,o=(n&&"string"==typeof n.diagnosticId?n.diagnosticId:void 0)??t.diagnosticId,s=(n&&"string"==typeof n.logPath?n.logPath:void 0)??t.logPath,a=i??function(e){switch(e){case"INVALID_ARGS":return"Check command arguments and run --help for usage examples.";case"SESSION_NOT_FOUND":return"Run open first or pass an explicit device selector.";case"TOOL_MISSING":return"Install required platform tooling and ensure it is available in PATH.";case"DEVICE_NOT_FOUND":return"Verify the target device is booted/connected and selectors match.";case"UNSUPPORTED_OPERATION":return"This command is not available for the selected platform/device.";case"COMMAND_FAILED":default:return"Retry with --debug and inspect diagnostics log for details.";case"UNAUTHORIZED":return"Refresh daemon metadata and retry the command."}}(r.code),l=function(e){if(!e)return;let t={...e};return delete t.hint,delete t.diagnosticId,delete t.logPath,Object.keys(t).length>0?t:void 0}(n),c=function(e,t,r){if("COMMAND_FAILED"!==e||r?.processExitError!==!0)return t;let n=function(e){let t=[/^an error was encountered processing the command/i,/^underlying error\b/i,/^simulator device failed to complete the requested operation/i];for(let r of e.split("\n")){let e=r.trim();if(e&&!t.some(t=>t.test(e)))return e.length>200?`${e.slice(0,200)}...`:e}return null}("string"==typeof r?.stderr?r.stderr:"");return n||t}(r.code,r.message,n);return{code:r.code,message:c,hint:a,diagnosticId:o,logPath:s,details:l}}let $="<wifi|airplane|location> <on|off>",b="appearance <light|dark|toggle>",N="faceid <match|nonmatch|enroll|unenroll>",_="touchid <match|nonmatch|enroll|unenroll>",L="fingerprint <match|nonmatch>",T="permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]",M="permission <grant|reset> <accessibility|screen-recording|input-monitoring>",O=`settings ${$} | settings ${b} | settings ${N} | settings ${_} | settings ${L} | settings ${T} | settings ${M}`,C=`settings requires ${$}, ${b}, ${N}, ${_}, ${L}, ${T}, or ${M}`,R=["app","frontmost-app","desktop","menubar"];function P(e){let t=e?.trim().toLowerCase();if("app"===t||"frontmost-app"===t||"desktop"===t||"menubar"===t)return t;throw new E("INVALID_ARGS",`Invalid surface: ${e}. Use ${R.join("|")}.`)}function k(e){var t;let r,n=F(e.label),i=F(e.value),o=F(e.identifier),s=(t=o)&&!/^[\w.]+:id\/[\w.-]+$/i.test(t)&&!/^_?NS:\d+$/i.test(t)?o:"";return(r=B(e.type??"")).includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||r.includes("textview")||r.includes("textarea")?i||n||s:n||i||s}function F(e){return"string"==typeof e?e.trim():""}function B(e){let t=e.trim().replace(/XCUIElementType/gi,"").replace(/^AX/,"").toLowerCase(),r=Math.max(t.lastIndexOf("."),t.lastIndexOf("/"));return -1!==r&&(t=t.slice(r+1)),t}function j(e,t={}){let r=[],n=[];for(let i of e){let e=i.depth??0;for(;r.length>0&&e<=r[r.length-1];)r.pop();let o=i.label?.trim()||i.value?.trim()||i.identifier?.trim()||"",s=V(i.type??"Element"),a="group"===s&&!o;a&&r.push(e);let l=a?e:Math.max(0,e-r.length);n.push({node:i,depth:l,type:s,text:G(i,l,a,s,t)})}return n}function G(e,t,r,n,i={}){var o,s,a,l,c;let u,d,f=n??V(e.type??"Element"),m=(u=k(e),{text:u,isLargeSurface:d=function(e,t){if("text-view"===t||"text-field"===t||"search"===t)return!0;let r=B(e.type??""),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("textview")||r.includes("textarea")||r.includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||n.includes("text area")||n.includes("text field")}(e,f),shouldSummarize:d&&!!(o=u)&&(o.length>80||/[\r\n]/.test(o))}),p=(s=e,a=f,l=i,c=m,l.summarizeTextSurfaces&&c.shouldSummarize&&function(e,t,r){let n=F(e.label);if(n&&n!==r)return n;let i=F(e.identifier);if(i&&!H(i)&&i!==r)return i;switch(t){case"text":case"text-view":return"Text view";case"text-field":return"Text field";case"search":return"Search field";default:return""}}(s,a,c.text)||U(s,a)),h=" ".repeat(t),w=e.ref?`@${e.ref}`:"",g=(function(e,t,r,n){let i,o=[];if(!1===e.enabled&&o.push("disabled"),!r.summarizeTextSurfaces||(!0===e.selected&&o.push("selected"),z(t)&&o.push("editable"),function(e,t){if("scroll-area"===t)return!0;let r=(e.type??"").toLowerCase(),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("scroll")||n.includes("scroll")}(e,t)&&o.push("scrollable"),!n.shouldSummarize))return o;return o.push(`preview:"${((i=n.text.replace(/\s+/g," ").trim()).length<=48?i:`${i.slice(0,45)}...`).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`),o.push("truncated"),[...new Set(o)]})(e,f,i,m).map(e=>` [${e}]`).join(""),S=p?` "${p}"`:"";return r?`${h}${w} [${f}]${g}`.trimEnd():`${h}${w} [${f}]${S}${g}`.trimEnd()}function U(e,t){let r=e.label?.trim(),n=e.value?.trim();if(z(t)){if(n)return n;if(r)return r}else if(r)return r;if(n)return n;let i=e.identifier?.trim();return!i||H(i)&&("group"===t||"image"===t||"list"===t||"collection"===t)?"":i}function V(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.includes(".")&&(t=t.replace(/^android\.widget\./,"").replace(/^android\.view\./,"").replace(/^android\.webkit\./,"").replace(/^androidx\./,"").replace(/^com\.google\.android\./,"").replace(/^com\.android\./,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":case"imagebutton":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"checkedtextview":return"text";case"textfield":case"edittext":return"text-field";case"textview":return r?"text":"text-view";case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":case"imageview":return"image";case"webview":return"webview";case"framelayout":case"linearlayout":case"relativelayout":case"constraintlayout":case"viewgroup":case"view":case"group":return"group";case"listview":case"recyclerview":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":case"scrollview":case"nestedscrollview":return"scroll-area";case"table":return"table";default:return t||"element"}}function z(e){return"text-field"===e||"text-view"===e||"search"===e}function H(e){return/^[\w.]+:id\/[\w.-]+$/i.test(e)}function q(){try{let e=J();return JSON.parse(n.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}function J(){let e=a.dirname(d(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(n.existsSync(e))return t;t=a.dirname(t)}return e}async function W(e,t,r={}){return new Promise((n,i)=>{let o=l(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached}),s="",a=r.binaryStdout?Buffer.alloc(0):void 0,c="",u=!1,d=ee(r.timeoutMs),f=d?setTimeout(()=>{u=!0,o.kill("SIGKILL")},d):null;r.binaryStdout||o.stdout.setEncoding("utf8"),o.stderr.setEncoding("utf8"),void 0!==r.stdin&&o.stdin.write(r.stdin),o.stdin.end(),o.stdout.on("data",e=>{r.binaryStdout?a=Buffer.concat([a??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):s+=e}),o.stderr.on("data",e=>{c+=e}),o.on("error",r=>{(f&&clearTimeout(f),"ENOENT"===r.code)?i(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),o.on("close",o=>{f&&clearTimeout(f);let l=o??1;u&&d?i(new E("COMMAND_FAILED",`${e} timed out after ${d}ms`,{cmd:e,args:t,stdout:s,stderr:c,exitCode:l,timeoutMs:d})):0===l||r.allowFailure?n({stdout:s,stderr:c,exitCode:l,stdoutBuffer:a}):i(new E("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:s,stderr:c,exitCode:l,processExitError:!0}))})})}async function K(e){try{var t;let{shell:r,args:n}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),i=await W(r,n,{allowFailure:!0});return 0===i.exitCode&&i.stdout.trim().length>0}catch{return!1}}function X(e,t,r={}){let n=c(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],encoding:r.binaryStdout?void 0:"utf8",input:r.stdin,timeout:ee(r.timeoutMs)});if(n.error){let i=n.error.code;if("ETIMEDOUT"===i)throw new E("COMMAND_FAILED",`${e} timed out after ${ee(r.timeoutMs)}ms`,{cmd:e,args:t,timeoutMs:ee(r.timeoutMs)},n.error);if("ENOENT"===i)throw new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n.error);throw new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n.error)}let i=r.binaryStdout?Buffer.isBuffer(n.stdout)?n.stdout:Buffer.from(n.stdout??""):void 0,o=r.binaryStdout?"":"string"==typeof n.stdout?n.stdout:(n.stdout??"").toString(),s="string"==typeof n.stderr?n.stderr:(n.stderr??"").toString(),a=n.status??1;if(0!==a&&!r.allowFailure)throw new E("COMMAND_FAILED",`${e} exited with code ${a}`,{cmd:e,args:t,stdout:o,stderr:s,exitCode:a,processExitError:!0});return{stdout:o,stderr:s,exitCode:a,stdoutBuffer:i}}function Z(e,t,r={}){l(e,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function Q(e,t,r={}){return new Promise((n,i)=>{let o=l(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached});r.onSpawn?.(o);let s="",a="",c=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||o.stdout.setEncoding("utf8"),o.stderr.setEncoding("utf8"),void 0!==r.stdin&&o.stdin.write(r.stdin),o.stdin.end(),o.stdout.on("data",e=>{if(r.binaryStdout){c=Buffer.concat([c??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);s+=t,r.onStdoutChunk?.(t)}),o.stderr.on("data",e=>{let t=String(e);a+=t,r.onStderrChunk?.(t)}),o.on("error",r=>{"ENOENT"===r.code?i(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),o.on("close",o=>{let l=o??1;0===l||r.allowFailure?n({stdout:s,stderr:a,exitCode:l,stdoutBuffer:c}):i(new E("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:s,stderr:a,exitCode:l,processExitError:!0}))})})}function Y(e,t,r={}){let n=l(e,t,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"],detached:r.detached}),i="",o="";n.stdout.setEncoding("utf8"),n.stderr.setEncoding("utf8"),n.stdout.on("data",e=>{i+=e}),n.stderr.on("data",e=>{o+=e});let s=new Promise((s,a)=>{n.on("error",r=>{"ENOENT"===r.code?a(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):a(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),n.on("close",n=>{let l=n??1;0===l||r.allowFailure?s({stdout:i,stderr:o,exitCode:l}):a(new E("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:i,stderr:o,exitCode:l,processExitError:!0}))})});return{child:n,wait:s}}function ee(e){if(!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t}let et=[/(^|[\/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,/(^|[\/\s"'=])src\/daemon\.ts($|[\s"'])/];function er(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function en(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=X("ps",["-p",String(e),"-o","lstart="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function ei(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=X("ps",["-p",String(e),"-o","command="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function eo(e,t){let r;if(!er(e))return!1;if(t){let r=en(e);if(!r||r!==t)return!1}let n=ei(e);return!!n&&!!(r=n.toLowerCase().replaceAll("\\","/")).includes("agent-device")&&et.some(e=>e.test(r))}function es(e,t){try{return process.kill(e,t),!0}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return!1;throw t}}async function ea(e,t){if(!er(e))return!0;let r=Date.now();for(;Date.now()-r<t;)if(await new Promise(e=>setTimeout(e,50)),!er(e))return!0;return!er(e)}async function el(e,t){!eo(e,t.expectedStartTime)||!es(e,"SIGTERM")||await ea(e,t.termTimeoutMs)||es(e,"SIGKILL")&&await ea(e,t.killTimeoutMs)}function ec(e){return e?.HOME?.trim()||s.homedir()}function eu(e,t={}){return"~"===e?ec(t.env):e.startsWith("~/")?a.join(ec(t.env),e.slice(2)):e}function ed(e,t={}){let r=eu(e,t);return a.isAbsolute(r)?r:a.resolve(t.cwd??process.cwd(),r)}function ef(e){let t,r=(t=(e??"").trim())?ed(t):a.join(eu("~"),".agent-device");return{baseDir:r,infoPath:a.join(r,"daemon.json"),lockPath:a.join(r,"daemon.lock"),logPath:a.join(r,"daemon.log"),sessionsDir:a.join(r,"sessions")}}function em(e){let t=(e??"").trim().toLowerCase();return"http"===t?"http":"dual"===t?"dual":"socket"}function ep(e){let t=(e??"").trim().toLowerCase();return"auto"===t?"auto":"socket"===t?"socket":"http"===t?"http":"auto"}function eh(e){return"tenant"===(e??"").trim().toLowerCase()?"tenant":"none"}function ew(e){if(!e)return;let t=e.trim();if(t&&/^[a-zA-Z0-9._-]{1,128}$/.test(t))return t}let eg=100,eS=new Set(["batch","replay"]);function ey(e){let t;try{t=JSON.parse(e)}catch{throw new E("INVALID_ARGS","Batch steps must be valid JSON.")}if(!Array.isArray(t)||0===t.length)throw new E("INVALID_ARGS","Batch steps must be a non-empty JSON array.");return t}function ev(e,t){if(!Array.isArray(e)||0===e.length)throw new E("INVALID_ARGS","batch requires a non-empty batchSteps array.");if(e.length>t)throw new E("INVALID_ARGS",`batch has ${e.length} steps; max allowed is ${t}.`);let r=[];for(let t=0;t<e.length;t+=1){let n=e[t];if(!n||"object"!=typeof n)throw new E("INVALID_ARGS",`Invalid batch step at index ${t}.`);let i="string"==typeof n.command?n.command.trim().toLowerCase():"";if(!i)throw new E("INVALID_ARGS",`Batch step ${t+1} requires command.`);if(eS.has(i))throw new E("INVALID_ARGS",`Batch step ${t+1} cannot run ${i}.`);if(void 0!==n.positionals&&!Array.isArray(n.positionals))throw new E("INVALID_ARGS",`Batch step ${t+1} positionals must be an array.`);let o=n.positionals??[];if(o.some(e=>"string"!=typeof e))throw new E("INVALID_ARGS",`Batch step ${t+1} positionals must contain only strings.`);if(void 0!==n.flags&&("object"!=typeof n.flags||Array.isArray(n.flags)||!n.flags))throw new E("INVALID_ARGS",`Batch step ${t+1} flags must be an object.`);if(void 0!==n.runtime&&("object"!=typeof n.runtime||Array.isArray(n.runtime)||!n.runtime))throw new E("INVALID_ARGS",`Batch step ${t+1} runtime must be an object.`);r.push({command:i,positionals:o,flags:n.flags??{},runtime:n.runtime})}return r}export{TextDecoder,styleText}from"node:util";export{default as node_net}from"node:net";export{default as node_http}from"node:http";export{default as node_https}from"node:https";export{E as AppError,eg as DEFAULT_BATCH_MAX_STEPS,R as SESSION_SURFACES,C as SETTINGS_INVALID_ARGS_MESSAGE,O as SETTINGS_USAGE_OVERRIDE,x as asAppError,j as buildSnapshotDisplayLines,r as createHash,w as createRequestId,U as displayLabel,y as emitDiagnostic,i as existsSync,eu as expandUserHomePath,u as external_node_url_URL,k as extractReadableText,d as fileURLToPath,J as findProjectRoot,A as flushDiagnosticsToSessionFile,V as formatRole,G as formatSnapshotLine,S as getDiagnosticsMeta,eo as isAgentDeviceDaemonProcess,er as isProcessAlive,t as node_crypto,n as node_fs,s as node_os,a as node_path,D as normalizeError,ew as normalizeTenantId,ey as parseBatchStepsJson,P as parseSessionSurface,f as pathToFileURL,o as promises,ei as readProcessCommand,en as readProcessStartTime,q as readVersion,ef as resolveDaemonPaths,em as resolveDaemonServerMode,ep as resolveDaemonTransportPreference,eh as resolveSessionIsolationMode,ed as resolveUserPath,W as runCmd,Y as runCmdBackground,Z as runCmdDetached,Q as runCmdStreaming,X as runCmdSync,l as spawn,el as stopProcessForTakeover,ev as validateAndNormalizeBatchSteps,K as whichCmd,v as withDiagnosticTimer,g as withDiagnosticsScope};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display