agent-device
Advanced tools
| 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. |
@@ -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; |
@@ -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() |
@@ -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 | ||
| } | ||
| } |
+17
-2
| # 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. |
+10
-6
| { | ||
| "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" | ||
| } | ||
| } |
+6
-0
@@ -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
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 53 instances in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 53 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
986692
4.15%54
1.89%2473
3.21%86
7.5%8
-11.11%7
16.67%114
6.54%