agent-device 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/src/daemon.js +1 -1
- package/package.json +2 -1
- package/skills/agent-device/SKILL.md +156 -0
- package/skills/agent-device/references/coordinate-system.md +8 -0
- package/skills/agent-device/references/permissions.md +20 -0
- package/skills/agent-device/references/session-management.md +22 -0
- package/skills/agent-device/references/snapshot-refs.md +49 -0
- package/skills/agent-device/references/video-recording.md +39 -0
- package/src/platforms/ios/ax-snapshot.ts +2 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ App state:
|
|
|
114
114
|
## iOS notes
|
|
115
115
|
- Input commands (`press`, `type`, `scroll`, etc.) are supported only on simulators in v1 and use the XCTest runner.
|
|
116
116
|
- `alert` and `scrollintoview` use the XCTest runner and are simulator-only in v1.
|
|
117
|
+
- Real device support (including snapshots) is on the roadmap for iOS.
|
|
117
118
|
|
|
118
119
|
## Testing
|
|
119
120
|
|
package/dist/src/daemon.js
CHANGED
|
@@ -2,7 +2,7 @@ let e,t;import a from"node:crypto";import{isCancel as i,select as n}from"@clack/
|
|
|
2
2
|
[axsnapshot] exit=${a.exitCode} stdoutBytes=${i.length} stderrBytes=${r.length}
|
|
3
3
|
`,d.appendFileSync(e,o),(0!==a.exitCode||r.length>0)&&(r.length>0&&d.appendFileSync(e,`${r}
|
|
4
4
|
`),0!==a.exitCode&&i.length>0&&d.appendFileSync(e,`${i}
|
|
5
|
-
`))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",n=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:n})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,n=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<n&&(n=e.y);return i<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,o,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eE(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=r.join(e,"package.json");if(d.existsSync(t))return e;e=r.dirname(e)}return process.cwd()}(),t=r.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=r.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let n=r.join(t,".build","release","axsnapshot");if(d.existsSync(n))return n;let o=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==o.exitCode||!d.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:o.stderr,stdout:o.stdout});return n}async function eR(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await y();return await g(e,t)}if("ios"===t.platform){let e=await Y();return await g(e,t)}let a=[];try{a.push(...await y())}catch{}try{a.push(...await Y())}catch{}return await g(a,t)}async function eC(e,t,a,i,n){let r=function(e){switch(e.platform){case"android":return{open:t=>_(e,t),openDevice:()=>x(e),close:t=>L(e,t),tap:(t,a)=>E(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>Q(e,t),openDevice:()=>ee(e),close:t=>et(e,t),tap:(t,a)=>ea(e,t,a),longPress:(t,a,i)=>ei(e,t,a,i),focus:(t,a)=>en(e,t,a),type:t=>er(e,t),fill:(t,a,i)=>eo(e,t,a,i),scroll:(t,a)=>es(e,t,a),scrollIntoView:e=>el(e),screenshot:t=>ec(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await r.openDevice(),{app:null};return await r.open(e),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await r.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await r.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),o=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await ew(e,{command:"type",text:o,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})):await r.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ew(e,{command:"swipe",direction:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})}else await r.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await ew(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await ew(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await r.scrollIntoView(t),{text:t}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await r.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await ew(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"back"}}return await R(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await ew(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"home"}}return await C(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await ew(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"app-switcher"}}return await M(e),{action:"app-switcher"};case"settings":{let[t,i,r]=a;if("ios"===e.platform)return await eu(e,t,i,r??n?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=n?.snapshotBackend??"hybrid";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};if("hybrid"===t){let t=(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],a=function(e){let t=[];for(let a=0;a<e.length;a+=1){let i=e[a],n=i.depth??0;if((e[a+1]?.depth??-1)>n)continue;let r=eF(i.type);eM.has(r)&&t.push({index:a,depth:n,label:i.label,identifier:i.identifier,type:i.type})}return t}(t);if(0===a.length)return{nodes:t,truncated:!1,backend:"hybrid"};let i=await eT(e,t,a,{appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,raw:n?.snapshotRaw,verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});return{nodes:i.nodes,truncated:i.truncated,backend:"hybrid"}}let a=await ew(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}let eM=new Set(["tabbar","toolbar","group"]);async function eT(e,t,a,i){let n=[...t],r=!1,o=0;for(let t of a){let a=function(e){for(let t of[e.label,e.identifier]){if(!t)continue;let e=t.trim();if(e)return e}return null}(t);if(!a)continue;let s=await ew(e,{command:"snapshot",appBundleId:i.appBundleId,interactiveOnly:i.interactiveOnly,compact:i.compact,depth:i.depth,scope:a,raw:i.raw},{verbose:i.verbose,logPath:i.logPath,traceLogPath:i.traceLogPath});s.truncated&&(r=!0);let l=(s.nodes??[]).filter(e=>{let t=eF(e.type);return"application"!==t&&"window"!==t});if(0===l.length)continue;let c=function(e,t){let a=1/0;for(let t of e){let e=t.depth??0;e<a&&(a=e)}return Number.isFinite(a)||(a=0),e.map(e=>({...e,depth:t+(e.depth??0)-a}))}(l,t.depth+1);n.splice(t.index+1+o,0,...c),o+=c.length}return{nodes:n=n.map((e,t)=>({...e,index:t})),truncated:r}}function eF(e){if(!e)return"";let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}function eB(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function e$(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eU(e,t){return e.find(e=>e.ref===t)??null}function eV(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function ej(e,t,a,i={}){let n=eq(a);if(!n)return null;let r=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return eG(e.label,a);case"value":return eG(e.value,a);case"id":return eG(e.identifier,a);default:return Math.max(eG(e.label,a),eG(e.value,a),eG(e.identifier,a))}}(a,t,n);if(!(e<=0)&&(!r||e>r.score)&&(r={node:a,score:e},e>=2))break}return r?.node??null}function eG(e,t){let a=eq(e??"");return a?a===t?2:+!!a.includes(t):0}function eq(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eJ=new Map,eW=r.join(p.homedir(),".agent-device"),eX=r.join(eW,"daemon.json"),ez=r.join(eW,"daemon.log"),eH=r.join(eW,"sessions"),eY=function(){try{let e=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}();return JSON.parse(d.readFileSync(r.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eZ=a.randomBytes(24).toString("hex");function eK(e,t,a){return{appBundleId:t,verbose:e?.verbose,logPath:ez,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eQ(e){if(e.token!==eZ)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,a=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eJ.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:Y}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:Y}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=eJ.get(a),i=e.flags??{};if(!t&&!i.platform&&!i.device&&!i.udid&&!i.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eR(i);if(await e9(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:ep})),a=await t(n);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:r,listAndroidAppsMetadata:o}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await r(n,e.flags?.appsFilter)}}}if("appstate"===t){let t=eJ.get(a),i=e.flags??{},n=t?.device??await eR(i);if(await e9(n),"ios"===n.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await e1(n,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:r}=await Promise.resolve().then(()=>({getAndroidAppState:P})),o=await r(n);return{ok:!0,data:{platform:"android",package:o.package,activity:o.activity}}}if("open"===t){let i;if(eJ.has(a))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let n=await eR(e.flags??{});await e9(n);let r=Array.from(eJ.values()).find(e=>e.device.id===n.id);if(r)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${r.name}".`,details:{session:r.name,deviceId:n.id,deviceName:n.name}}};let o=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await t(n,e.positionals?.[0]??"")}catch{i=void 0}await eC(n,"open",e.positionals??[],e.flags?.out,{...eK(e.flags,i)});let s={name:a,device:n,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eJ.set(a,s),{ok:!0,data:{session:a}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e8(t),i=JSON.parse(d.readFileSync(e,"utf8")),n=i.optimizedActions??i.actions??[];for(let e of n)e&&"replay"!==e.command&&await eQ({token:eZ,session:a,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:n.length,session:a}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eJ.get(a);return i?(e.positionals&&e.positionals.length>0&&await eC(i.device,"close",e.positionals??[],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await ey(i.device.id),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),e3(i),eJ.delete(a),{ok:!0,data:{session:a}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=i?.appBundleId,o=e.flags?.snapshotScope;if(o&&o.trim().startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=e$(o.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${o}`}};let t=eU(i.snapshot.nodes,e),a=t?e7(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found or has no label`}};o=a}let s=await eC(n,"snapshot",[],e.flags?.out,{...eK({...e.flags,snapshotScope:o},r,i?.trace?.outPath)}),l=s?.nodes??[],c=eB(e.flags?.snapshotRaw?l:te(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:a,device:n,createdAt:i?.createdAt??Date.now(),appBundleId:i?.appBundleId??r,snapshot:u,actions:i?.actions??[],appName:i?.appName};return e0(d,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eJ.set(a,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=e.positionals??[];if(0===r.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let o=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=o(r[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===r[0])l=null!==(c=o(r[r.length-1]))?r.slice(1,-1).join(" "):r.slice(1).join(" ");else if(r[0].startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=e$(r[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${r[0]}`}};let t=eU(i.snapshot.nodes,e),a=t?e7(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r[0]} not found or has no label`}};c=o(r[r.length-1]),l=a}else l=null!==(c=o(r[r.length-1]))?r.slice(0,-1).join(" "):r.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===n.platform&&"simulator"===n.kind){let a=await ew(n,{command:"findText",text:l,appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});if(a?.found)return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(e6(eB((await q(n,{scope:l})).nodes??[]),l))return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===r){let a=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,r=Date.now();for(;Date.now()-r<a;){try{let a=await ew(n,{command:"alert",action:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:a}),{ok:!0,data:a}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let o=await ew(n,{command:"alert",action:"accept"===r||"dismiss"===r?r:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o}),{ok:!0,data:o}}if("record"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=eJ.get(a),o=n?.device??await eR(e.flags??{});n||await e9(o);let s=n??{name:a,device:o,createdAt:Date.now(),actions:[]};if("start"===i){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let i=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=r.resolve(i),l=r.dirname(n);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===o.platform){if("simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",o.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:a}=u("adb",["-s",o.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:a}}return eJ.set(a,s),e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:i}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",o.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",o.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let n=eJ.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===i){let a,i;if(n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let o=e8(e.positionals?.[1]??(a=n.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),r.join(eH,`${a}-${i}.trace.log`)));return d.mkdirSync(r.dirname(o),{recursive:!0}),d.appendFileSync(o,""),n.trace={outPath:o,startedAt:Date.now()},e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:o}}),{ok:!0,data:{trace:"started",outPath:o}}}if(!n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let o=n.trace.outPath;if(e.positionals?.[1]){let t=e8(e.positionals[1]);d.mkdirSync(r.dirname(t),{recursive:!0}),d.existsSync(o)?d.renameSync(o,t):d.appendFileSync(t,""),o=t}return n.trace=void 0,e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:o}}),{ok:!0,data:{trace:"stopped",outPath:o}}}if("settings"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if(!i||!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let r=eJ.get(a),o=r?.device??await eR(e.flags??{});r||await e9(o);let s=r?.appBundleId,l=await eC(o,"settings",[i,n,s??""],e.flags?.out,{...eK(e.flags,s,r?.trace?.outPath)});return r&&e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:i,state:n}}),{ok:!0,data:l??{setting:i,state:n}}}if("find"===t){let n=e.positionals??[];if(0===n.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:r,query:o,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",n=e.slice(a+1);if(0===n.length)return{locator:t,query:i,action:"click"};let r=n[0].toLowerCase();if("get"===r){let e=n[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===r)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(n[1])??void 0};if("exists"===r)return{locator:t,query:i,action:"exists"};if("click"===r)return{locator:t,query:i,action:"click"};if("focus"===r)return{locator:t,query:i,action:"focus"};if("fill"===r)return{locator:t,query:i,action:"fill",value:n.slice(1).join(" ")};if("type"===r)return{locator:t,query:i,action:"type",value:n.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${n[0]}`)}(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eJ.get(a);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eR(e.flags??{});u||await e9(d);let p=u?.appBundleId,h="role"!==r?o:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,y=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let i=await eC(d,"snapshot",[],e.flags?.out,{...eK({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),n=i?.nodes??[],r=eB(e.flags?.snapshotRaw?n:te(n));return w=t,g=r,u&&(u.snapshot={nodes:r,truncated:i?.truncated,createdAt:Date.now(),backend:i?.backend},eJ.set(a,u)),{nodes:r,truncated:i?.truncated,backend:i?.backend}};if("wait"===s){let a=c??1e4,i=Date.now();for(;Date.now()-i<a;){let{nodes:a}=await y();if(ej(a,r,o,{requireRect:!1}))return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-i}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-i}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:v}=await y(),I=ej(v,r,o,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N=`@${I.ref}`,b={...e.flags??{},noRecord:!0};if("exists"===s)return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){var i;let a=[(i=I).label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get text",text:a}}),{ok:!0,data:{ref:N,text:a,node:I}}}if("get_attrs"===s)return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get attrs"}}),{ok:!0,data:{ref:N,node:I}};if("click"===s){let i=await eQ({token:eZ,session:a,command:"click",positionals:[N],flags:b});return i.ok&&u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"click"}}),i}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let i=await eQ({token:eZ,session:a,command:"fill",positionals:[N,l],flags:b});return i.ok&&u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"fill"}}),i}if("focus"===s){let a=I.rect?eV(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let i=await eC(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"focus"}}),{ok:!0,data:i??{ref:N}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let a=I.rect?eV(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await eC(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});let i=await eC(d,"type",[l],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"type"}}),{ok:!0,data:i??{ref:N}}}}if("click"===t){let i=eJ.get(a);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",r=e$(n);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eU(i.snapshot.nodes,r);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=e6(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=e7(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let a=t.trim().toLowerCase();if(!a)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===a&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ew(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath}),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:r,mode:"text"}};let{x:c,y:u}=eV(o.rect);return await eC(i.device,"press",[String(c),String(u)],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)}),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:r,x:c,y:u}}}if("fill"===t){let i=eJ.get(a);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let a=e$(e.positionals[0]);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let n=e.positionals.length>=3?e.positionals[1]:"",r=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=eU(i.snapshot.nodes,a);if(!o?.rect&&n&&(o=e6(i.snapshot.nodes,n)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=e7(o,i.snapshot.nodes),{x:l,y:c}=eV(o.rect),u=await eC(i.device,"fill",[String(l),String(c),r],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)});return e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:a,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:a,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let r=eJ.get(a);if(!r?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=e$(n??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eU(r.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=e6(r.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===i)return e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}let n=eJ.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eC(n.device,t,e.positionals??[],e.flags?.out,{...eK(e.flags,n.appBundleId,n.trace?.outPath)});return e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function e0(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function e1(e,t,a){let i=e2(await eC(e,"snapshot",[],a?.out,{...eK({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let n=e2(await eC(e,"snapshot",[],a?.out,{...eK({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:n?.appName??n?.appBundleId??"unknown",appBundleId:n?.appBundleId,source:"snapshot-xctest"}}function e2(e){let t=eB(e?.nodes??[]),a=t.find(e=>"application"===tt(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),n=a.identifier?.trim();return i||n?{appName:i||void 0,appBundleId:n||void 0}:null}function e3(e){try{d.existsSync(eH)||d.mkdirSync(eH,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=r.join(eH,`${t}-${a}.ad`),n=r.join(eH,`${t}-${a}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let r of(a.push(`context platform=${e.device.platform} device="${i}"${n} theme=unknown`),t))r.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(e4(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(e4(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(e4(a));let i=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(e4(i)),n&&t.push(e4(n)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(e4(a)),t.push(e4(i));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e4(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e4(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(e4(a));return t.join(" ")}(r));return`${a.join("\n")}
|
|
5
|
+
`))),0!==s.exitCode){let e,t,a=(s.stderr??"").toString(),i=(e=a.toLowerCase()).includes("accessibility permission")?" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest).":e.includes("could not find ios app content")?" AX snapshot sometimes caches empty content. Try restarting the Simulator app.":"",n=!!((t=a.toLowerCase()).includes("could not find ios app content")||t.includes("timeout"));throw new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${a}${i}`,stdout:s.stdout,retryable:n})}return s},{shouldRetry:e=>{var t;return(t=e)instanceof f&&"COMMAND_FAILED"===t.code&&t.details?.retryable===!0}});try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");a=e.root,i=e.windowFrame??void 0}else a=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=a.frame??i,s=[],l=[],c=(e,t)=>{e.frame&&s.push(e.frame);let a=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(l.push({...e,frame:a,children:void 0,depth:t}),e.children??[]))c(i,t+1)};return c(a,0),{nodes:(function(e,t,a){if(!t||0===a.length)return e;let i=1/0,n=1/0;for(let e of a)e.x<i&&(i=e.x),e.y<n&&(n=e.y);return i<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(l,o,s).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eE(){let e=function(){let e=r.dirname(c(import.meta.url));for(let t=0;t<6;t+=1){let t=r.join(e,"package.json");if(d.existsSync(t))return e;e=r.dirname(e)}return process.cwd()}(),t=r.join(e,"ios-runner","AXSnapshot"),a=process.env.AGENT_DEVICE_AX_BINARY;if(a&&d.existsSync(a))return a;let i=r.join(e,"dist","bin","axsnapshot");if(d.existsSync(i))return i;let n=r.join(t,".build","release","axsnapshot");if(d.existsSync(n))return n;let o=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==o.exitCode||!d.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:o.stderr,stdout:o.stdout});return n}async function eR(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await J();let e=await y();return await g(e,t)}if("ios"===t.platform){let e=await Y();return await g(e,t)}let a=[];try{a.push(...await y())}catch{}try{a.push(...await Y())}catch{}return await g(a,t)}async function eC(e,t,a,i,n){let r=function(e){switch(e.platform){case"android":return{open:t=>_(e,t),openDevice:()=>x(e),close:t=>L(e,t),tap:(t,a)=>E(e,t,a),longPress:(t,a,i)=>T(e,t,a,i),focus:(t,a)=>B(e,t,a),type:t=>F(e,t),fill:(t,a,i)=>$(e,t,a,i),scroll:(t,a)=>U(e,t,a),scrollIntoView:t=>V(e,t),screenshot:t=>j(e,t)};case"ios":return{open:t=>Q(e,t),openDevice:()=>ee(e),close:t=>et(e,t),tap:(t,a)=>ea(e,t,a),longPress:(t,a,i)=>ei(e,t,a,i),focus:(t,a)=>en(e,t,a),type:t=>er(e,t),fill:(t,a,i)=>eo(e,t,a,i),scroll:(t,a)=>es(e,t,a),scrollIntoView:e=>el(e),screenshot:t=>ec(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=a[0];if(!e)return await r.openDevice(),{app:null};return await r.open(e),{app:e}}case"close":{let e=a[0];if(!e)return{closed:"session"};return await r.close(e),{app:e}}case"press":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(a[0]),t=Number(a[1]),i=a[2]?Number(a[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await r.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=a.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.focus(t,i),{x:t,y:i}}case"type":{let t=a.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ew(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}):await r.type(t),{text:t}}case"fill":{let t=Number(a[0]),i=Number(a[1]),o=a.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ew(e,{command:"tap",x:t,y:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await ew(e,{command:"type",text:o,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})):await r.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=a[0],i=a[1]?Number(a[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let a=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ew(e,{command:"swipe",direction:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath})}else await r.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let t=a.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let a=0;a<8;a+=1){let i=await ew(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});if(i?.found)return{text:t,attempts:a+1};await ew(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await r.scrollIntoView(t),{text:t}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await r.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await ew(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"back"}}return await R(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await ew(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"home"}}return await C(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await ew(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath}),{action:"app-switcher"}}return await M(e),{action:"app-switcher"};case"settings":{let[t,i,r]=a;if("ios"===e.platform)return await eu(e,t,i,r??n?.appBundleId),{setting:t,state:i};return await G(e,t,i),{setting:t,state:i}}case"snapshot":{let t=n?.snapshotBackend??"hybrid";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],truncated:!1,backend:"ax"};if("hybrid"===t){let t=(await eL(e,{traceLogPath:n?.traceLogPath})).nodes??[],a=function(e){let t=[];for(let a=0;a<e.length;a+=1){let i=e[a],n=i.depth??0;if((e[a+1]?.depth??-1)>n)continue;let r=eF(i.type);eM.has(r)&&t.push({index:a,depth:n,label:i.label,identifier:i.identifier,type:i.type})}return t}(t);if(0===a.length)return{nodes:t,truncated:!1,backend:"hybrid"};let i=await eT(e,t,a,{appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,raw:n?.snapshotRaw,verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});return{nodes:i.nodes,truncated:i.truncated,backend:"hybrid"}}let a=await ew(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath,traceLogPath:n?.traceLogPath});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"xctest"}}let a=await q(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:a.nodes??[],truncated:a.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}let eM=new Set(["tabbar","toolbar","group"]);async function eT(e,t,a,i){let n=[...t],r=!1,o=0;for(let t of a){let a=function(e){for(let t of[e.label,e.identifier]){if(!t)continue;let e=t.trim();if(e)return e}return null}(t);if(!a)continue;let s=await ew(e,{command:"snapshot",appBundleId:i.appBundleId,interactiveOnly:i.interactiveOnly,compact:i.compact,depth:i.depth,scope:a,raw:i.raw},{verbose:i.verbose,logPath:i.logPath,traceLogPath:i.traceLogPath});s.truncated&&(r=!0);let l=(s.nodes??[]).filter(e=>{let t=eF(e.type);return"application"!==t&&"window"!==t});if(0===l.length)continue;let c=function(e,t){let a=1/0;for(let t of e){let e=t.depth??0;e<a&&(a=e)}return Number.isFinite(a)||(a=0),e.map(e=>({...e,depth:t+(e.depth??0)-a}))}(l,t.depth+1);n.splice(t.index+1+o,0,...c),o+=c.length}return{nodes:n=n.map((e,t)=>({...e,index:t})),truncated:r}}function eF(e){if(!e)return"";let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}function eB(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function e$(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eU(e,t){return e.find(e=>e.ref===t)??null}function eV(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}function ej(e,t,a,i={}){let n=eq(a);if(!n)return null;let r=null;for(let a of e){if(i.requireRect&&!a.rect)continue;let e=function(e,t,a){switch(t){case"role":return function(e,t){let a=function(e){let t=e.trim();return t?((t=(t.split(".").pop()??t).replace(/XCUIElementType/gi,"").toLowerCase()).startsWith("ax")&&(t=t.replace(/^ax/,"")),t):""}(e??"");return a?a===t?2:+!!a.includes(t):0}(e.type,a);case"label":return eG(e.label,a);case"value":return eG(e.value,a);case"id":return eG(e.identifier,a);default:return Math.max(eG(e.label,a),eG(e.value,a),eG(e.identifier,a))}}(a,t,n);if(!(e<=0)&&(!r||e>r.score)&&(r={node:a,score:e},e>=2))break}return r?.node??null}function eG(e,t){let a=eq(e??"");return a?a===t?2:+!!a.includes(t):0}function eq(e){return e.trim().toLowerCase().replace(/\s+/g," ")}let eJ=new Map,eW=r.join(p.homedir(),".agent-device"),eX=r.join(eW,"daemon.json"),ez=r.join(eW,"daemon.log"),eH=r.join(eW,"sessions"),eY=function(){try{let e=function(){let e=r.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=r.join(t,"package.json");if(d.existsSync(e))return t;t=r.dirname(t)}return e}();return JSON.parse(d.readFileSync(r.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eZ=a.randomBytes(24).toString("hex");function eK(e,t,a){return{appBundleId:t,verbose:e?.verbose,logPath:ez,traceLogPath:a,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eQ(e){if(e.token!==eZ)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,a=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eJ.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:Y}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y})),{listIosDevices:a}=await Promise.resolve().then(()=>({listIosDevices:Y}));try{t.push(...await e())}catch{}try{t.push(...await a())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=eJ.get(a),i=e.flags??{};if(!t&&!i.platform&&!i.device&&!i.udid&&!i.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eR(i);if(await e9(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:t}=await Promise.resolve().then(()=>({listSimulatorApps:ep})),a=await t(n);return e.flags?.appsMetadata?{ok:!0,data:{apps:a}}:{ok:!0,data:{apps:a.map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:r,listAndroidAppsMetadata:o}=await Promise.resolve().then(()=>({listAndroidApps:D,listAndroidAppsMetadata:k}));return e.flags?.appsMetadata?{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}:{ok:!0,data:{apps:await r(n,e.flags?.appsFilter)}}}if("appstate"===t){let t=eJ.get(a),i=e.flags??{},n=t?.device??await eR(i);if(await e9(n),"ios"===n.platform){if(t?.appBundleId)return{ok:!0,data:{platform:"ios",appBundleId:t.appBundleId,appName:t.appName??t.appBundleId,source:"session"}};let a=await e1(n,t?.trace?.outPath,e.flags);return{ok:!0,data:{platform:"ios",appName:a.appName,appBundleId:a.appBundleId,source:a.source}}}let{getAndroidAppState:r}=await Promise.resolve().then(()=>({getAndroidAppState:P})),o=await r(n);return{ok:!0,data:{platform:"android",package:o.package,activity:o.activity}}}if("open"===t){let i;if(eJ.has(a))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let n=await eR(e.flags??{});await e9(n);let r=Array.from(eJ.values()).find(e=>e.device.id===n.id);if(r)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${r.name}".`,details:{session:r.name,deviceId:n.id,deviceName:n.name}}};let o=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:K}));i=await t(n,e.positionals?.[0]??"")}catch{i=void 0}await eC(n,"open",e.positionals??[],e.flags?.out,{...eK(e.flags,i)});let s={name:a,device:n,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),eJ.set(a,s),{ok:!0,data:{session:a}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{let e=e8(t),i=JSON.parse(d.readFileSync(e,"utf8")),n=i.optimizedActions??i.actions??[];for(let e of n)e&&"replay"!==e.command&&await eQ({token:eZ,session:a,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:n.length,session:a}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eJ.get(a);return i?(e.positionals&&e.positionals.length>0&&await eC(i.device,"close",e.positionals??[],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await ey(i.device.id),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:a}}),e3(i),eJ.delete(a),{ok:!0,data:{session:a}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=i?.appBundleId,o=e.flags?.snapshotScope;if(o&&o.trim().startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=e$(o.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${o}`}};let t=eU(i.snapshot.nodes,e),a=t?e7(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o} not found or has no label`}};o=a}let s=await eC(n,"snapshot",[],e.flags?.out,{...eK({...e.flags,snapshotScope:o},r,i?.trace?.outPath)}),l=s?.nodes??[],c=eB(e.flags?.snapshotRaw?l:te(l)),u={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},d={name:a,device:n,createdAt:i?.createdAt??Date.now(),appBundleId:i?.appBundleId??r,snapshot:u,actions:i?.actions??[],appName:i?.appName};return e0(d,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),eJ.set(a,d),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:d.appBundleId?d.appName??d.appBundleId:void 0,appBundleId:d.appBundleId}}}if("wait"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=e.positionals??[];if(0===r.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let o=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=o(r[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===r[0])l=null!==(c=o(r[r.length-1]))?r.slice(1,-1).join(" "):r.slice(1).join(" ");else if(r[0].startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=e$(r[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${r[0]}`}};let t=eU(i.snapshot.nodes,e),a=t?e7(t,i.snapshot.nodes):void 0;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r[0]} not found or has no label`}};c=o(r[r.length-1]),l=a}else l=null!==(c=o(r[r.length-1]))?r.slice(0,-1).join(" "):r.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let u=c??1e4,d=Date.now();for(;Date.now()-d<u;){if("ios"===n.platform&&"simulator"===n.kind){let a=await ew(n,{command:"findText",text:l,appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});if(a?.found)return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(e6(eB((await q(n,{scope:l})).nodes??[]),l))return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-d}}),{ok:!0,data:{text:l,waitedMs:Date.now()-d}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let i=eJ.get(a),n=i?.device??await eR(e.flags??{});i||await e9(n);let r=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===r){let a=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,r=Date.now();for(;Date.now()-r<a;){try{let a=await ew(n,{command:"alert",action:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:a}),{ok:!0,data:a}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let o=await ew(n,{command:"alert",action:"accept"===r||"dismiss"===r?r:"get",appBundleId:i?.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath});return i&&e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o}),{ok:!0,data:o}}if("record"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=eJ.get(a),o=n?.device??await eR(e.flags??{});n||await e9(o);let s=n??{name:a,device:o,createdAt:Date.now(),actions:[]};if("start"===i){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let i=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=r.resolve(i),l=r.dirname(n);if(d.existsSync(l)||d.mkdirSync(l,{recursive:!0}),"ios"===o.platform){if("simulator"!==o.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=u("xcrun",["simctl","io",o.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:a}=u("adb",["-s",o.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:a}}return eJ.set(a,s),e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:i}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await h("adb",["-s",o.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await h("adb",["-s",o.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,e0(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("trace"===t){let i=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"trace requires start|stop"}};let n=eJ.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}};if("start"===i){let a,i;if(n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"trace already in progress"}};let o=e8(e.positionals?.[1]??(a=n.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date().toISOString().replace(/[:.]/g,"-"),r.join(eH,`${a}-${i}.trace.log`)));return d.mkdirSync(r.dirname(o),{recursive:!0}),d.appendFileSync(o,""),n.trace={outPath:o,startedAt:Date.now()},e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start",outPath:o}}),{ok:!0,data:{trace:"started",outPath:o}}}if(!n.trace)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active trace"}};let o=n.trace.outPath;if(e.positionals?.[1]){let t=e8(e.positionals[1]);d.mkdirSync(r.dirname(t),{recursive:!0}),d.existsSync(o)?d.renameSync(o,t):d.appendFileSync(t,""),o=t}return n.trace=void 0,e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:o}}),{ok:!0,data:{trace:"stopped",outPath:o}}}if("settings"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if(!i||!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"settings requires <wifi|airplane|location> <on|off>"}};let r=eJ.get(a),o=r?.device??await eR(e.flags??{});r||await e9(o);let s=r?.appBundleId,l=await eC(o,"settings",[i,n,s??""],e.flags?.out,{...eK(e.flags,s,r?.trace?.outPath)});return r&&e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:l??{setting:i,state:n}}),{ok:!0,data:l??{setting:i,state:n}}}if("find"===t){let n=e.positionals??[];if(0===n.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a locator or text"}};let{locator:r,query:o,action:s,value:l,timeoutMs:c}=function(e){let t="any",a=0;["text","label","value","role","id"].includes(e[0])&&(t=e[0],a=1);let i=e[a]??"",n=e.slice(a+1);if(0===n.length)return{locator:t,query:i,action:"click"};let r=n[0].toLowerCase();if("get"===r){let e=n[1]?.toLowerCase();if("text"===e)return{locator:t,query:i,action:"get_text"};if("attrs"===e)return{locator:t,query:i,action:"get_attrs"};throw new f("INVALID_ARGS","find get only supports text or attrs")}if("wait"===r)return{locator:t,query:i,action:"wait",timeoutMs:function(e){if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}(n[1])??void 0};if("exists"===r)return{locator:t,query:i,action:"exists"};if("click"===r)return{locator:t,query:i,action:"click"};if("focus"===r)return{locator:t,query:i,action:"focus"};if("fill"===r)return{locator:t,query:i,action:"fill",value:n.slice(1).join(" ")};if("type"===r)return{locator:t,query:i,action:"type",value:n.slice(1).join(" ")};throw new f("INVALID_ARGS",`Unsupported find action: ${n[0]}`)}(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"find requires a value"}};let u=eJ.get(a);if(!u&&"exists"!==s&&"wait"!==s&&"get_text"!==s&&"get_attrs"!==s)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let d=u?.device??await eR(e.flags??{});u||await e9(d);let p=u?.appBundleId,h="role"!==r?o:void 0,m="click"===s||"focus"===s||"fill"===s||"type"===s,w=0,g=null,y=async()=>{let t=Date.now();if(g&&t-w<750)return{nodes:g};let i=await eC(d,"snapshot",[],e.flags?.out,{...eK({...e.flags,snapshotScope:h,snapshotInteractiveOnly:m,snapshotCompact:m},p,u?.trace?.outPath)}),n=i?.nodes??[],r=eB(e.flags?.snapshotRaw?n:te(n));return w=t,g=r,u&&(u.snapshot={nodes:r,truncated:i?.truncated,createdAt:Date.now(),backend:i?.backend},eJ.set(a,u)),{nodes:r,truncated:i?.truncated,backend:i?.backend}};if("wait"===s){let a=c??1e4,i=Date.now();for(;Date.now()-i<a;){let{nodes:a}=await y();if(ej(a,r,o,{requireRect:!1}))return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0,waitedMs:Date.now()-i}}),{ok:!0,data:{found:!0,waitedMs:Date.now()-i}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"find wait timed out"}}}let{nodes:v}=await y(),I=ej(v,r,o,{requireRect:m});if(!I)return{ok:!1,error:{code:"COMMAND_FAILED",message:"find did not match any element"}};let N=`@${I.ref}`,b={...e.flags??{},noRecord:!0};if("exists"===s)return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{found:!0}}),{ok:!0,data:{found:!0}};if("get_text"===s){var i;let a=[(i=I).label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get text",text:a}}),{ok:!0,data:{ref:N,text:a,node:I}}}if("get_attrs"===s)return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"get attrs"}}),{ok:!0,data:{ref:N,node:I}};if("click"===s){let i=await eQ({token:eZ,session:a,command:"click",positionals:[N],flags:b});return i.ok&&u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"click"}}),i}if("fill"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find fill requires text"}};let i=await eQ({token:eZ,session:a,command:"fill",positionals:[N,l],flags:b});return i.ok&&u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"fill"}}),i}if("focus"===s){let a=I.rect?eV(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};let i=await eC(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"focus"}}),{ok:!0,data:i??{ref:N}}}if("type"===s){if(!l)return{ok:!1,error:{code:"INVALID_ARGS",message:"find type requires text"}};let a=I.rect?eV(I.rect):null;if(!a)return{ok:!1,error:{code:"COMMAND_FAILED",message:"matched element has no bounds"}};await eC(d,"focus",[String(a.x),String(a.y)],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});let i=await eC(d,"type",[l],e.flags?.out,{...eK(e.flags,u?.appBundleId,u?.trace?.outPath)});return u&&e0(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:N,action:"type"}}),{ok:!0,data:i??{ref:N}}}}if("click"===t){let i=eJ.get(a);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",r=e$(n);if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eU(i.snapshot.nodes,r);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=e6(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=e7(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let a=t.trim().toLowerCase();if(!a)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===a&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ew(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:ez,traceLogPath:i?.trace?.outPath}),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:r,mode:"text"}};let{x:c,y:u}=eV(o.rect);return await eC(i.device,"press",[String(c),String(u)],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)}),e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:r,x:c,y:u}}}if("fill"===t){let i=eJ.get(a);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let a=e$(e.positionals[0]);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let n=e.positionals.length>=3?e.positionals[1]:"",r=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=eU(i.snapshot.nodes,a);if(!o?.rect&&n&&(o=e6(i.snapshot.nodes,n)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=e7(o,i.snapshot.nodes),{x:l,y:c}=eV(o.rect),u=await eC(i.device,"fill",[String(l),String(c),r],e.flags?.out,{...eK(e.flags,i.appBundleId,i.trace?.outPath)});return e0(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:a,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:a,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],n=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let r=eJ.get(a);if(!r?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=e$(n??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eU(r.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=e6(r.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===i)return e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return e0(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}let n=eJ.get(a);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eC(n.device,t,e.positionals??[],e.flags?.out,{...eK(e.flags,n.appBundleId,n.trace?.outPath)});return e0(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function e0(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}=e;return{platform:t,device:a,udid:i,serial:n,out:r,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:p,appsMetadata:f,noRecord:h,recordJson:m}}(t.flags),result:t.result})}async function e1(e,t,a){let i=e2(await eC(e,"snapshot",[],a?.out,{...eK({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"ax"},void 0,t)}));if(i?.appName||i?.appBundleId)return{appName:i.appName??i.appBundleId??"unknown",appBundleId:i.appBundleId,source:"snapshot-ax"};let n=e2(await eC(e,"snapshot",[],a?.out,{...eK({...a,snapshotDepth:1,snapshotCompact:!0,snapshotBackend:"xctest"},void 0,t)}));return{appName:n?.appName??n?.appBundleId??"unknown",appBundleId:n?.appBundleId,source:"snapshot-xctest"}}function e2(e){let t=eB(e?.nodes??[]),a=t.find(e=>"application"===tt(e.type??""))??t[0];if(!a)return null;let i=a.label?.trim(),n=a.identifier?.trim();return i||n?{appName:i||void 0,appBundleId:n||void 0}:null}function e3(e){try{d.existsSync(eH)||d.mkdirSync(eH,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),a=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=r.join(eH,`${t}-${a}.ad`),n=r.join(eH,`${t}-${a}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let a of e.actions)if("snapshot"!==a.command){if("click"===a.command||"fill"===a.command||"get"===a.command){let i=a.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:a.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(a)}return t}(e)},s=function(e,t){let a=[],i=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let r of(a.push(`context platform=${e.device.platform} device="${i}"${n} theme=unknown`),t))r.flags?.noRecord||a.push(function(e){let t=[e.command];if("click"===e.command){let a=e.positionals?.[0];if(a){t.push(e4(a));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(e4(i)),t.join(" ")}}if("fill"===e.command){let a=e.positionals?.[0];if(a&&a.startsWith("@")){t.push(e4(a));let i=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(e4(i)),n&&t.push(e4(n)),t.join(" ")}}if("get"===e.command){let a=e.positionals?.[0],i=e.positionals?.[1];if(a&&i){t.push(e4(a)),t.push(e4(i));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e4(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e4(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let a of e.positionals??[])t.push(e4(a));return t.join(" ")}(r));return`${a.join("\n")}
|
|
6
6
|
`}(e,o.optimizedActions);d.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&d.writeFileSync(n,JSON.stringify(o,null,2))}catch{}}function e8(e){return e.startsWith("~/")?r.join(p.homedir(),e.slice(2)):r.resolve(e)}function e4(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function e6(e,t){let a=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(a)||i.includes(a)||n.includes(a)})??null}function e7(e,t){let a=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return a&&e5(a)?a:function(e,t){if(!e.rect)return;let a=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!e5(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-a);(!i||n<i.distance)&&(i={label:t,distance:n})}return i?.label}(e,t)??(a&&e5(a)?a:void 0)}function e5(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function e9(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ef}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:I}));await t(e.id)}}function te(e){let t=[],a=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=tt(i.type??""),r=[i.label,i.value,i.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),o=!!r&&e5(r);if(("group"===n||"ioscontentgroup"===n)&&!o){t.push(e);continue}let s=Math.max(0,e-t.length);a.push({...i,depth:s})}return a}function tt(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(e=m.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async a=>{let i=(t+=a).indexOf("\n");for(;-1!==i;){let a,n=t.slice(0,i).trim();if(t=t.slice(i+1),0===n.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(n);a=await eQ(e)}catch(t){let e=l(t);a={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(a)}
|
|
7
7
|
`),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var a;a=t.port,d.existsSync(eW)||d.mkdirSync(eW,{recursive:!0}),d.writeFileSync(ez,""),d.writeFileSync(eX,JSON.stringify({port:a,token:eZ,pid:process.pid,version:eY},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port}
|
|
8
8
|
`)}}),t=async()=>{for(let e of Array.from(eJ.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await ey(e.device.id),e3(e);e.close(()=>{d.existsSync(eX)&&d.unlinkSync(eX),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let a=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${a.message}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Callstack",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"!ios-runner/**/.swiftpm",
|
|
38
38
|
"!ios-runner/**/xcuserdata",
|
|
39
39
|
"!ios-runner/**/*.xcuserstate",
|
|
40
|
+
"skills",
|
|
40
41
|
"src",
|
|
41
42
|
"README.md",
|
|
42
43
|
"LICENSE"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-device
|
|
3
|
+
description: Automates mobile and simulator interactions for iOS and Android devices. Use when navigating apps, taking snapshots/screenshots, tapping, typing, scrolling, or extracting UI info on mobile devices or simulators.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mobile Automation with agent-device
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
agent-device open Settings --platform ios
|
|
12
|
+
agent-device snapshot -i
|
|
13
|
+
agent-device snapshot -s @e3
|
|
14
|
+
agent-device click @e3
|
|
15
|
+
agent-device wait text "Camera"
|
|
16
|
+
agent-device alert wait 10000
|
|
17
|
+
agent-device fill @e5 "test"
|
|
18
|
+
agent-device close
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core workflow
|
|
22
|
+
|
|
23
|
+
1. Open app or just boot device: `open [app]`
|
|
24
|
+
2. Snapshot: `snapshot -i` to get compact refs
|
|
25
|
+
3. Interact using refs (`click @eN`, `fill @eN "text"`)
|
|
26
|
+
4. Re-snapshot after navigation or UI changes
|
|
27
|
+
5. Close session when done
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
### Navigation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
agent-device open [app] # Boot device/simulator; optionally launch app
|
|
35
|
+
agent-device close [app] # Close app or just end session
|
|
36
|
+
agent-device session list # List active sessions
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Snapshot (page analysis)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
agent-device snapshot # Full accessibility tree
|
|
43
|
+
agent-device snapshot -i # Interactive elements only (recommended)
|
|
44
|
+
agent-device snapshot -c # Compact output
|
|
45
|
+
agent-device snapshot -d 3 # Limit depth
|
|
46
|
+
agent-device snapshot -s "Camera" # Scope to label/identifier
|
|
47
|
+
agent-device snapshot --raw # Raw node output
|
|
48
|
+
agent-device snapshot --backend hybrid # Default: best speed vs correctness trade-off (AX fast, XCTest complete)
|
|
49
|
+
agent-device snapshot --backend ax # macOS Accessibility tree (fast, needs permissions)
|
|
50
|
+
agent-device snapshot --backend xctest # XCTest snapshot (slow, no permissions)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Hybrid will automatically fill empty containers (e.g. `group`, `tab bar`) by scoping XCTest to the container label.
|
|
54
|
+
It is recommended because AX is fast but can miss UI details, while XCTest is slower but more complete.
|
|
55
|
+
If you want explicit control or AX is unavailable, use `--backend xctest`.
|
|
56
|
+
In practice, if AX returns a `Tab Bar` group with no children, hybrid will run a scoped XCTest snapshot for `Tab Bar` and insert those nodes under the group.
|
|
57
|
+
|
|
58
|
+
### Find (semantic)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agent-device find "Sign In" click
|
|
62
|
+
agent-device find text "Sign In" click
|
|
63
|
+
agent-device find label "Email" fill "user@example.com"
|
|
64
|
+
agent-device find value "Search" type "query"
|
|
65
|
+
agent-device find role button click
|
|
66
|
+
agent-device find id "com.example:id/login" click
|
|
67
|
+
agent-device find "Settings" wait 10000
|
|
68
|
+
agent-device find "Settings" exists
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Settings helpers (simulators)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
agent-device settings wifi on
|
|
75
|
+
agent-device settings wifi off
|
|
76
|
+
agent-device settings airplane on
|
|
77
|
+
agent-device settings airplane off
|
|
78
|
+
agent-device settings location on
|
|
79
|
+
agent-device settings location off
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Note: iOS wifi/airplane toggles status bar indicators, not actual network state.
|
|
83
|
+
Airplane off clears status bar overrides.
|
|
84
|
+
|
|
85
|
+
### App state
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
agent-device appstate
|
|
89
|
+
agent-device apps --metadata --platform ios
|
|
90
|
+
agent-device apps --metadata --platform android
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Interactions (use @refs from snapshot)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
agent-device click @e1
|
|
97
|
+
agent-device focus @e2
|
|
98
|
+
agent-device fill @e2 "text" # Tap then type
|
|
99
|
+
agent-device type "text" # Type into focused field
|
|
100
|
+
agent-device press 300 500 # Tap by coordinates
|
|
101
|
+
agent-device long-press 300 500 800 # Long press (where supported)
|
|
102
|
+
agent-device scroll down 0.5
|
|
103
|
+
agent-device back
|
|
104
|
+
agent-device home
|
|
105
|
+
agent-device app-switcher
|
|
106
|
+
agent-device wait 1000
|
|
107
|
+
agent-device wait text "Settings"
|
|
108
|
+
agent-device alert get
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Get information
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
agent-device get text @e1
|
|
115
|
+
agent-device get attrs @e1
|
|
116
|
+
agent-device screenshot --out out.png
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Trace logs (AX/XCTest)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
agent-device trace start # Start trace capture
|
|
123
|
+
agent-device trace start ./trace.log # Start trace capture to path
|
|
124
|
+
agent-device trace stop # Stop trace capture
|
|
125
|
+
agent-device trace stop ./trace.log # Stop and move trace log
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Devices and apps
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
agent-device devices
|
|
132
|
+
agent-device apps --platform ios
|
|
133
|
+
agent-device apps --platform android # default: launchable only
|
|
134
|
+
agent-device apps --platform android --all
|
|
135
|
+
agent-device apps --platform android --user-installed
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Best practices
|
|
139
|
+
|
|
140
|
+
- Always snapshot right before interactions; refs invalidate on UI changes.
|
|
141
|
+
- Prefer `snapshot -i` to reduce output size.
|
|
142
|
+
- On iOS, hybrid is the default and uses AX first, so Accessibility permission is still required.
|
|
143
|
+
- If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`.
|
|
144
|
+
- Use `--session <name>` for parallel sessions; avoid device contention.
|
|
145
|
+
|
|
146
|
+
## References
|
|
147
|
+
|
|
148
|
+
- [references/snapshot-refs.md](references/snapshot-refs.md)
|
|
149
|
+
- [references/session-management.md](references/session-management.md)
|
|
150
|
+
- [references/permissions.md](references/permissions.md)
|
|
151
|
+
- [references/recording.md](references/recording.md)
|
|
152
|
+
- [references/coordinate-system.md](references/coordinate-system.md)
|
|
153
|
+
|
|
154
|
+
## Missing features roadmap (high level)
|
|
155
|
+
|
|
156
|
+
See [references/missing-features.md](references/missing-features.md) for planned parity with agent-browser.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Permissions and Setup
|
|
2
|
+
|
|
3
|
+
## iOS AX snapshot
|
|
4
|
+
|
|
5
|
+
Hybrid snapshot (default) is recommended for best speed vs correctness; it uses macOS Accessibility APIs and requires permission:
|
|
6
|
+
|
|
7
|
+
System Settings > Privacy & Security > Accessibility
|
|
8
|
+
|
|
9
|
+
If permission is missing, use:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-device snapshot --backend xctest --platform ios
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Hybrid/AX is fast; XCTest is slower but does not require permissions.
|
|
16
|
+
|
|
17
|
+
## Simulator troubleshooting
|
|
18
|
+
|
|
19
|
+
- If AX shows the Simulator chrome instead of app, restart Simulator.
|
|
20
|
+
- If AX returns empty, restart Simulator and re-open app.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Session Management
|
|
2
|
+
|
|
3
|
+
## Named sessions
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
agent-device --session auth open Settings --platform ios
|
|
7
|
+
agent-device --session auth snapshot -i --platform ios
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Sessions isolate device context. A device can only be held by one session at a time.
|
|
11
|
+
|
|
12
|
+
## Best practices
|
|
13
|
+
|
|
14
|
+
- Name sessions semantically.
|
|
15
|
+
- Close sessions when done.
|
|
16
|
+
- Use separate devices for parallel work.
|
|
17
|
+
|
|
18
|
+
## Listing sessions
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
agent-device session list
|
|
22
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Snapshot + Refs Workflow (Mobile)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Refs let agents interact without repeating full UI trees. Snapshot -> refs -> click/fill.
|
|
6
|
+
|
|
7
|
+
## Snapshot
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
agent-device snapshot -i --platform ios
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Page: com.apple.Preferences
|
|
17
|
+
App: com.apple.Preferences
|
|
18
|
+
|
|
19
|
+
@e1 [ioscontentgroup]
|
|
20
|
+
@e2 [button] "Camera"
|
|
21
|
+
@e3 [button] "Privacy & Security"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Using refs
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
agent-device click @e2 --platform ios
|
|
28
|
+
agent-device fill @e5 "test" --platform ios
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Ref lifecycle
|
|
32
|
+
|
|
33
|
+
Refs become invalid when UI changes (navigation, modal, dynamic list updates).
|
|
34
|
+
Always re-snapshot after any transition.
|
|
35
|
+
|
|
36
|
+
## Scope snapshots
|
|
37
|
+
|
|
38
|
+
Use `-s` to scope to labels/identifiers. This reduces size and speeds up results:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
agent-device snapshot -i -s "Camera" --platform ios
|
|
42
|
+
agent-device snapshot -i -s @e3 --platform ios
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Troubleshooting
|
|
46
|
+
|
|
47
|
+
- Ref not found: re-snapshot.
|
|
48
|
+
- AX returns Simulator window: restart Simulator and re-run.
|
|
49
|
+
- AX empty: verify Accessibility permission or use `--backend xctest` (hybrid is recommended because AX is fast but can miss UI details, while XCTest is slower but more complete).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Video Recording
|
|
2
|
+
|
|
3
|
+
Capture device automation sessions as video for debugging, documentation, or verification
|
|
4
|
+
|
|
5
|
+
## iOS Simulator
|
|
6
|
+
|
|
7
|
+
Use `agent-device record` commands (wrapper around simctl):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Start recording
|
|
11
|
+
agent-device record start ./recordings/ios.mov
|
|
12
|
+
|
|
13
|
+
# Perform actions
|
|
14
|
+
agent-device open App
|
|
15
|
+
agent-device snapshot
|
|
16
|
+
agent-device click @e3
|
|
17
|
+
agent-device close
|
|
18
|
+
|
|
19
|
+
# Stop recording
|
|
20
|
+
agent-device record stop
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Android Emulator/Device
|
|
24
|
+
|
|
25
|
+
Use `agent-device record` commands (wrapper around adb):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Start recording
|
|
29
|
+
agent-device record start ./recordings/android.mp4
|
|
30
|
+
|
|
31
|
+
# Perform actions
|
|
32
|
+
agent-device open App
|
|
33
|
+
agent-device snapshot
|
|
34
|
+
agent-device click @e3
|
|
35
|
+
agent-device close
|
|
36
|
+
|
|
37
|
+
# Stop recording
|
|
38
|
+
agent-device record stop
|
|
39
|
+
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { AppError } from '../../utils/errors.ts';
|
|
4
5
|
import { runCmd } from '../../utils/exec.ts';
|
|
5
6
|
import { withRetry } from '../../utils/retry.ts';
|
|
@@ -196,7 +197,7 @@ async function ensureAxSnapshotBinary(): Promise<string> {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
function findProjectRoot(): string {
|
|
199
|
-
let current =
|
|
200
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
200
201
|
for (let i = 0; i < 6; i += 1) {
|
|
201
202
|
const pkgPath = path.join(current, 'package.json');
|
|
202
203
|
if (fs.existsSync(pkgPath)) return current;
|