agent-device 0.3.0 → 0.3.2

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.
Files changed (34) hide show
  1. package/README.md +26 -2
  2. package/dist/src/274.js +1 -0
  3. package/dist/src/bin.js +27 -22
  4. package/dist/src/daemon.js +15 -10
  5. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +4 -2
  6. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +149 -0
  7. package/package.json +2 -2
  8. package/skills/agent-device/SKILL.md +8 -1
  9. package/src/cli.ts +13 -0
  10. package/src/core/__tests__/capabilities.test.ts +2 -0
  11. package/src/core/capabilities.ts +2 -0
  12. package/src/daemon/handlers/__tests__/replay-heal.test.ts +5 -0
  13. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +219 -0
  14. package/src/daemon/handlers/__tests__/session.test.ts +122 -0
  15. package/src/daemon/handlers/find.ts +23 -3
  16. package/src/daemon/handlers/session.ts +175 -10
  17. package/src/daemon-client.ts +1 -24
  18. package/src/daemon.ts +1 -24
  19. package/src/platforms/__tests__/boot-diagnostics.test.ts +59 -0
  20. package/src/platforms/android/__tests__/index.test.ts +17 -0
  21. package/src/platforms/android/devices.ts +167 -42
  22. package/src/platforms/android/index.ts +101 -14
  23. package/src/platforms/boot-diagnostics.ts +128 -0
  24. package/src/platforms/ios/index.ts +161 -2
  25. package/src/platforms/ios/runner-client.ts +19 -1
  26. package/src/utils/__tests__/exec.test.ts +16 -0
  27. package/src/utils/__tests__/finders.test.ts +34 -0
  28. package/src/utils/__tests__/retry.test.ts +44 -0
  29. package/src/utils/args.ts +9 -1
  30. package/src/utils/exec.ts +39 -0
  31. package/src/utils/finders.ts +27 -9
  32. package/src/utils/retry.ts +143 -13
  33. package/src/utils/version.ts +26 -0
  34. package/dist/src/861.js +0 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ The project is in early development and considered experimental. Pull requests a
14
14
 
15
15
  ## Features
16
16
  - Platforms: iOS (simulator + limited device support) and Android (emulator + device).
17
- - Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`.
17
+ - Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`, `reinstall`.
18
18
  - Inspection commands: `snapshot` (accessibility tree).
19
19
  - Device tooling: `adb` (Android), `simctl`/`devicectl` (iOS via Xcode).
20
20
  - Minimal dependencies; TypeScript executed directly on Node 22+ (no build step).
@@ -75,7 +75,7 @@ Coordinates:
75
75
  - X increases to the right, Y increases downward.
76
76
 
77
77
  ## Command Index
78
- - `open`, `close`, `home`, `back`, `app-switcher`
78
+ - `boot`, `open`, `close`, `reinstall`, `home`, `back`, `app-switcher`
79
79
  - `snapshot`, `find`, `get`
80
80
  - `click`, `focus`, `type`, `fill`, `press`, `long-press`, `scroll`, `scrollintoview`, `is`
81
81
  - `alert`, `wait`, `screenshot`
@@ -96,6 +96,7 @@ Notes:
96
96
  - If XCTest returns 0 nodes (e.g., foreground app changed), agent-device falls back to AX when available.
97
97
 
98
98
  Flags:
99
+ - `--version, -V` print version and exit
99
100
  - `--platform ios|android`
100
101
  - `--device <name>`
101
102
  - `--udid <udid>` (iOS)
@@ -122,6 +123,13 @@ Sessions:
122
123
  - Session scripts are written to `~/.agent-device/sessions/<session>-<timestamp>.ad` when recording is enabled with `--save-script`.
123
124
  - Deterministic replay is `.ad`-based; use `replay --update` (`-u`) to update selector drift and rewrite the replay file in place.
124
125
 
126
+ Navigation helpers:
127
+ - `boot --platform ios|android` ensures the target is ready without launching an app.
128
+ - Use `boot` mainly when starting a new session and `open` fails because no booted simulator/emulator is available.
129
+ - `open [app]` already boots/activates the selected target when needed.
130
+ - `reinstall <app> <path>` uninstalls and installs the app binary in one command (Android + iOS simulator in v1).
131
+ - `reinstall` accepts package/bundle id style app names and supports `~` in paths.
132
+
125
133
  Find (semantic):
126
134
  - `find <text> <action> [value]` finds by any text (label/value/identifier) using a scoped snapshot.
127
135
  - `find text|label|value|role|id <value> <action> [value]` for specific locators.
@@ -183,6 +191,13 @@ App state:
183
191
  - Built-in retries cover transient runner connection failures, AX snapshot hiccups, and Android UI dumps.
184
192
  - For snapshot issues (missing elements), compare with `--raw` flag for unaltered output and scope with `-s "<label>"`.
185
193
 
194
+ Boot diagnostics:
195
+ - Boot failures include normalized reason codes in `error.details.reason` (JSON mode) and verbose logs.
196
+ - Reason codes: `IOS_BOOT_TIMEOUT`, `IOS_RUNNER_CONNECT_TIMEOUT`, `ANDROID_BOOT_TIMEOUT`, `ADB_TRANSPORT_UNAVAILABLE`, `CI_RESOURCE_STARVATION_SUSPECTED`, `BOOT_COMMAND_FAILED`, `UNKNOWN`.
197
+ - Android boot waits fail fast for permission/tooling issues and do not always collapse into timeout errors.
198
+ - Use `agent-device boot --platform ios|android` when starting a new session only if `open` cannot find/connect to an available target.
199
+ - Set `AGENT_DEVICE_RETRY_LOGS=1` to print structured retry telemetry (attempt, phase, delay, elapsed/remaining deadline, reason).
200
+
186
201
  ## App resolution
187
202
  - Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).
188
203
  - Human-readable names are resolved when possible (e.g., `Settings`).
@@ -199,6 +214,14 @@ App state:
199
214
  pnpm test
200
215
  ```
201
216
 
217
+ Useful local checks:
218
+
219
+ ```bash
220
+ pnpm typecheck
221
+ pnpm test:unit
222
+ pnpm test:smoke
223
+ ```
224
+
202
225
  ## Build
203
226
 
204
227
  ```bash
@@ -208,6 +231,7 @@ pnpm build
208
231
  Environment selectors:
209
232
  - `ANDROID_DEVICE=Pixel_9_Pro_XL` or `ANDROID_SERIAL=emulator-5554`
210
233
  - `IOS_DEVICE="iPhone 17 Pro"` or `IOS_UDID=<udid>`
234
+ - `AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS=<ms>` to adjust iOS simulator boot timeout (default: `120000`, minimum: `5000`).
211
235
 
212
236
  Test screenshots are written to:
213
237
  - `test/screenshots/android-settings.png`
@@ -0,0 +1 @@
1
+ import e,{promises as t}from"node:fs";import n from"node:path";import{fileURLToPath as o,pathToFileURL as r}from"node:url";import{spawn as i}from"node:child_process";function d(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}class u extends Error{constructor(e,t,n,o){super(t),d(this,"code",void 0),d(this,"details",void 0),d(this,"cause",void 0),this.code=e,this.details=n,this.cause=o}}function s(e){return e instanceof u?e:e instanceof Error?new u("UNKNOWN",e.message,void 0,e):new u("UNKNOWN","Unknown error",{err:e})}function a(){try{let t=c();return JSON.parse(e.readFileSync(n.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}function c(){let t=n.dirname(o(import.meta.url)),r=t;for(let t=0;t<6;t+=1){let t=n.join(r,"package.json");if(e.existsSync(t))return r;r=n.dirname(r)}return t}async function f(e,t,n={}){return new Promise((o,r)=>{let d=i(e,t,{cwd:n.cwd,env:n.env,stdio:["pipe","pipe","pipe"]}),s="",a=n.binaryStdout?Buffer.alloc(0):void 0,c="",f=!1,l=function(e){if(!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t}(n.timeoutMs),m=l?setTimeout(()=>{f=!0,d.kill("SIGKILL")},l):null;n.binaryStdout||d.stdout.setEncoding("utf8"),d.stderr.setEncoding("utf8"),void 0!==n.stdin&&d.stdin.write(n.stdin),d.stdin.end(),d.stdout.on("data",e=>{n.binaryStdout?a=Buffer.concat([a??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):s+=e}),d.stderr.on("data",e=>{c+=e}),d.on("error",n=>{(m&&clearTimeout(m),"ENOENT"===n.code)?r(new u("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n)):r(new u("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n))}),d.on("close",i=>{m&&clearTimeout(m);let d=i??1;f&&l?r(new u("COMMAND_FAILED",`${e} timed out after ${l}ms`,{cmd:e,args:t,stdout:s,stderr:c,exitCode:d,timeoutMs:l})):0===d||n.allowFailure?o({stdout:s,stderr:c,exitCode:d,stdoutBuffer:a}):r(new u("COMMAND_FAILED",`${e} exited with code ${d}`,{cmd:e,args:t,stdout:s,stderr:c,exitCode:d}))})})}async function l(e){try{var t;let{shell:n,args:o}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),r=await f(n,o,{allowFailure:!0});return 0===r.exitCode&&r.stdout.trim().length>0}catch{return!1}}function m(e,t,n={}){i(e,t,{cwd:n.cwd,env:n.env,stdio:"ignore",detached:!0}).unref()}async function p(e,t,n={}){return new Promise((o,r)=>{let d=i(e,t,{cwd:n.cwd,env:n.env,stdio:["pipe","pipe","pipe"]}),s="",a="",c=n.binaryStdout?Buffer.alloc(0):void 0;n.binaryStdout||d.stdout.setEncoding("utf8"),d.stderr.setEncoding("utf8"),void 0!==n.stdin&&d.stdin.write(n.stdin),d.stdin.end(),d.stdout.on("data",e=>{if(n.binaryStdout){c=Buffer.concat([c??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);s+=t,n.onStdoutChunk?.(t)}),d.stderr.on("data",e=>{let t=String(e);a+=t,n.onStderrChunk?.(t)}),d.on("error",n=>{"ENOENT"===n.code?r(new u("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n)):r(new u("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n))}),d.on("close",i=>{let d=i??1;0===d||n.allowFailure?o({stdout:s,stderr:a,exitCode:d,stdoutBuffer:c}):r(new u("COMMAND_FAILED",`${e} exited with code ${d}`,{cmd:e,args:t,stdout:s,stderr:a,exitCode:d}))})})}function w(e,t,n={}){let o=i(e,t,{cwd:n.cwd,env:n.env,stdio:["ignore","pipe","pipe"]}),r="",d="";o.stdout.setEncoding("utf8"),o.stderr.setEncoding("utf8"),o.stdout.on("data",e=>{r+=e}),o.stderr.on("data",e=>{d+=e});let s=new Promise((i,s)=>{o.on("error",n=>{"ENOENT"===n.code?s(new u("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n)):s(new u("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n))}),o.on("close",o=>{let a=o??1;0===a||n.allowFailure?i({stdout:r,stderr:d,exitCode:a}):s(new u("COMMAND_FAILED",`${e} exited with code ${a}`,{cmd:e,args:t,stdout:r,stderr:d,exitCode:a}))})});return{child:o,wait:s}}export{default as node_net}from"node:net";export{default as node_os}from"node:os";export{s as asAppError,u as errors_AppError,o as fileURLToPath,c as findProjectRoot,e as node_fs,n as node_path,r as pathToFileURL,t as promises,a as readVersion,f as runCmd,w as runCmdBackground,m as runCmdDetached,p as runCmdStreaming,l as whichCmd};
package/dist/src/bin.js CHANGED
@@ -1,14 +1,17 @@
1
- import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as a,node_fs as i,node_os as s,node_net as o,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
- `)}function d(e){let t=e.details?`
1
+ import{node_path as e,asAppError as t,pathToFileURL as r,runCmdDetached as n,node_fs as a,node_os as i,node_net as s,errors_AppError as o,readVersion as l,findProjectRoot as c}from"./274.js";function d(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
+ `)}function u(e){let t=e.details?`
3
3
  ${JSON.stringify(e.details,null,2)}`:"";process.stderr.write(`Error (${e.code}): ${e.message}${t}
4
- `)}function u(e,t,r){let n=p(e.type??"Element"),a=function(e,t){var r,n;let a=e.label?.trim(),i=e.value?.trim();if("text-field"===(r=t)||"text-view"===r||"search"===r){if(i)return i;if(a)return a}else if(a)return a;if(i)return i;let s=e.identifier?.trim();return!s||(n=s,/^[\w.]+:id\/[\w.-]+$/i.test(n)&&("group"===t||"image"===t||"list"===t||"collection"===t))?"":s}(e,n),i=" ".repeat(t),s=e.ref?`@${e.ref}`:"",o=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),l=o?` [${o}]`:"",c=a?` "${a}"`:"";return r?`${i}${s} [${n}]${l}`.trimEnd():`${i}${s} [${n}]${c}${l}`.trimEnd()}function p(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),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"}}let f=e.join(s.homedir(),".agent-device"),m=e.join(f,"daemon.json"),h=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function w(e){let t=await y(),r={...e,token:t.token};return await x(t,r)}async function y(){let t=g(),r=function(){try{let t=$();return JSON.parse(i.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await v(t))return t;t&&(t.version!==r||!await v(t))&&i.existsSync(m)&&i.unlinkSync(m),await b();let n=Date.now();for(;Date.now()-n<5e3;){let e=g();if(e&&await v(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:m,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function g(){if(!i.existsSync(m))return null;try{let e=JSON.parse(i.readFileSync(m,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function v(e){return new Promise(t=>{let r=o.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function b(){let t=$(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),s=i.existsSync(r);if(!s&&!i.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let o=s?[r]:["--experimental-strip-types",n];a(process.execPath,o)}async function x(e,t){return new Promise((r,n)=>{let a=o.createConnection({host:"127.0.0.1",port:e.port},()=>{a.write(`${JSON.stringify(t)}
5
- `)}),i=setTimeout(()=>{a.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:h}))},h),s="";a.setEncoding("utf8"),a.on("data",e=>{let t=(s+=e).indexOf("\n");if(-1===t)return;let o=s.slice(0,t).trim();if(o)try{let e=JSON.parse(o);a.end(),clearTimeout(i),r(e)}catch(e){clearTimeout(i),n(e)}}),a.on("error",e=>{clearTimeout(i),n(e)})})}function $(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(i.existsSync(t))return n;n=e.dirname(n)}return r}async function S(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let a=e[n];if("--json"===a){t.json=!0;continue}if("--help"===a||"-h"===a){t.help=!0;continue}if("--verbose"===a||"-v"===a){t.verbose=!0;continue}if("-i"===a){t.snapshotInteractiveOnly=!0;continue}if("-c"===a){t.snapshotCompact=!0;continue}if("--raw"===a){t.snapshotRaw=!0;continue}if("--no-record"===a){t.noRecord=!0;continue}if("--save-script"===a){t.saveScript=!0;continue}if("--update"===a||"-u"===a){t.replayUpdate=!0;continue}if("--user-installed"===a){t.appsFilter="user-installed";continue}if("--all"===a){t.appsFilter="all";continue}if("--metadata"===a){t.appsMetadata=!0;continue}if(a.startsWith("--backend")){let r=a.includes("=")?a.split("=")[1]:e[n+1];if(a.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(a.startsWith("--")){let[r,i]=a.split("="),s=i??e[n+1];switch(!i&&(n+=1),r){case"--platform":if("ios"!==s&&"android"!==s)throw new l("INVALID_ARGS",`Invalid platform: ${s}`);t.platform=s;break;case"--depth":{let e=Number(s);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${s}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=s;break;case"--device":t.device=s;break;case"--udid":t.udid=s;break;case"--serial":t.serial=s;break;case"--out":t.out=s;break;case"--session":t.session=s;break;case"--activity":t.activity=s;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===a){let r=e[n+1];n+=1;let a=Number(r);if(!Number.isFinite(a)||a<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(a);continue}if("-s"===a){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(a)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
4
+ `)}function p(e,t,r){let n=f(e.type??"Element"),a=function(e,t){var r,n;let a=e.label?.trim(),i=e.value?.trim();if("text-field"===(r=t)||"text-view"===r||"search"===r){if(i)return i;if(a)return a}else if(a)return a;if(i)return i;let s=e.identifier?.trim();return!s||(n=s,/^[\w.]+:id\/[\w.-]+$/i.test(n)&&("group"===t||"image"===t||"list"===t||"collection"===t))?"":s}(e,n),i=" ".repeat(t),s=e.ref?`@${e.ref}`:"",o=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),l=o?` [${o}]`:"",c=a?` "${a}"`:"";return r?`${i}${s} [${n}]${l}`.trimEnd():`${i}${s} [${n}]${c}${l}`.trimEnd()}function f(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),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"}}let m=e.join(i.homedir(),".agent-device"),h=e.join(m,"daemon.json"),w=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function y(e){let t=await v(),r={...e,token:t.token};return await $(t,r)}async function v(){let e=g(),t=l();if(e&&e.version===t&&await b(e))return e;e&&(e.version!==t||!await b(e))&&a.existsSync(h)&&a.unlinkSync(h),await x();let r=Date.now();for(;Date.now()-r<5e3;){let e=g();if(e&&await b(e))return e;await new Promise(e=>setTimeout(e,100))}throw new o("COMMAND_FAILED","Failed to start daemon",{infoPath:h,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function g(){if(!a.existsSync(h))return null;try{let e=JSON.parse(a.readFileSync(h,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function b(e){return new Promise(t=>{let r=s.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function x(){let t=c(),r=e.join(t,"dist","src","daemon.js"),i=e.join(t,"src","daemon.ts"),s=a.existsSync(r);if(!s&&!a.existsSync(i))throw new o("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:i});let l=s?[r]:["--experimental-strip-types",i];n(process.execPath,l)}async function $(e,t){return new Promise((r,n)=>{let a=s.createConnection({host:"127.0.0.1",port:e.port},()=>{a.write(`${JSON.stringify(t)}
5
+ `)}),i=setTimeout(()=>{a.destroy(),n(new o("COMMAND_FAILED","Daemon request timed out",{timeoutMs:w}))},w),l="";a.setEncoding("utf8"),a.on("data",e=>{let t=(l+=e).indexOf("\n");if(-1===t)return;let s=l.slice(0,t).trim();if(s)try{let e=JSON.parse(s);a.end(),clearTimeout(i),r(e)}catch(e){clearTimeout(i),n(e)}}),a.on("error",e=>{clearTimeout(i),n(e)})})}async function S(r){let n=function(e){let t={json:!1,help:!1,version:!1},r=[];for(let n=0;n<e.length;n+=1){let a=e[n];if("--json"===a){t.json=!0;continue}if("--help"===a||"-h"===a){t.help=!0;continue}if("--version"===a||"-V"===a){t.version=!0;continue}if("--verbose"===a||"-v"===a){t.verbose=!0;continue}if("-i"===a){t.snapshotInteractiveOnly=!0;continue}if("-c"===a){t.snapshotCompact=!0;continue}if("--raw"===a){t.snapshotRaw=!0;continue}if("--no-record"===a){t.noRecord=!0;continue}if("--save-script"===a){t.saveScript=!0;continue}if("--update"===a||"-u"===a){t.replayUpdate=!0;continue}if("--user-installed"===a){t.appsFilter="user-installed";continue}if("--all"===a){t.appsFilter="all";continue}if("--metadata"===a){t.appsMetadata=!0;continue}if(a.startsWith("--backend")){let r=a.includes("=")?a.split("=")[1]:e[n+1];if(a.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new o("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(a.startsWith("--")){let[r,i]=a.split("="),s=i??e[n+1];switch(!i&&(n+=1),r){case"--platform":if("ios"!==s&&"android"!==s)throw new o("INVALID_ARGS",`Invalid platform: ${s}`);t.platform=s;break;case"--depth":{let e=Number(s);if(!Number.isFinite(e)||e<0)throw new o("INVALID_ARGS",`Invalid depth: ${s}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=s;break;case"--device":t.device=s;break;case"--udid":t.udid=s;break;case"--serial":t.serial=s;break;case"--out":t.out=s;break;case"--session":t.session=s;break;case"--activity":t.activity=s;break;default:throw new o("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===a){let r=e[n+1];n+=1;let a=Number(r);if(!Number.isFinite(a)||a<0)throw new o("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(a);continue}if("-s"===a){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(a)}return{command:r.shift()??null,positionals:r,flags:t}}(r);n.flags.version&&(process.stdout.write(`${l()}
6
+ `),process.exit(0)),(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json]
6
7
 
7
8
  CLI to control iOS and Android devices for AI agents.
8
9
 
9
10
  Commands:
11
+ boot Ensure target device/simulator is booted and ready
10
12
  open [app] Boot device/simulator; optionally launch app
11
13
  close [app] Close app or just end session
14
+ reinstall <app> <path> Uninstall + install app from binary path
12
15
  snapshot [-i] [-c] [-d <depth>] [-s <scope>] [--raw] [--backend ax|xctest]
13
16
  Capture accessibility tree
14
17
  -i Interactive elements only
@@ -68,31 +71,33 @@ Flags:
68
71
  --update, -u Replay: update selectors and rewrite replay file in place
69
72
  --user-installed Apps: list user-installed packages (Android only)
70
73
  --all Apps: list all packages (Android only)
74
+ --version, -V Print version and exit
71
75
 
72
- `),process.exit(+!n.flags.help));let{command:a,positionals:o,flags:f}=n,m=f.session??process.env.AGENT_DEVICE_SESSION??"default",h=f.verbose&&!f.json?function(){try{let t=e.join(s.homedir(),".agent-device","daemon.log"),r=0,n=!1,a=setInterval(()=>{if(n||!i.existsSync(t))return;let e=i.statSync(t);if(e.size<=r)return;let a=i.openSync(t,"r"),s=Buffer.alloc(e.size-r);i.readSync(a,s,0,s.length,r),i.closeSync(a),r=e.size,s.length>0&&process.stdout.write(s.toString("utf8"))},200);return()=>{n=!0,clearInterval(a)}}catch{return null}}():null;try{if("session"===a){let e=o[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await w({session:m,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);f.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
73
- `),h&&h();return}let e=await w({session:m,command:a,positionals:o,flags:f});if(e.ok){if(f.json){c({success:!0,data:e.data??{}}),h&&h();return}if("snapshot"===a){process.stdout.write(function(e,t={}){let r=e.nodes,n=Array.isArray(r)?r:[],a=!!e.truncated,i="string"==typeof e.appName?e.appName:void 0,s="string"==typeof e.appBundleId?e.appBundleId:void 0,o=[];i&&o.push(`Page: ${i}`),s&&o.push(`App: ${s}`);let l=`Snapshot: ${n.length} nodes${a?" (truncated)":""}`,c=o.length>0?`${o.join("\n")}
76
+ `),process.exit(+!n.flags.help));let{command:s,positionals:c,flags:m}=n,h=m.session??process.env.AGENT_DEVICE_SESSION??"default",w=m.verbose&&!m.json?function(){try{let t=e.join(i.homedir(),".agent-device","daemon.log"),r=0,n=!1,s=setInterval(()=>{if(n||!a.existsSync(t))return;let e=a.statSync(t);if(e.size<=r)return;let i=a.openSync(t,"r"),s=Buffer.alloc(e.size-r);a.readSync(i,s,0,s.length,r),a.closeSync(i),r=e.size,s.length>0&&process.stdout.write(s.toString("utf8"))},200);return()=>{n=!0,clearInterval(s)}}catch{return null}}():null;try{if("session"===s){let e=c[0]??"list";if("list"!==e)throw new o("INVALID_ARGS","session only supports list");let t=await y({session:h,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new o(t.error.code,t.error.message);m.json?d({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)}
77
+ `),w&&w();return}let e=await y({session:h,command:s,positionals:c,flags:m});if(e.ok){if(m.json){d({success:!0,data:e.data??{}}),w&&w();return}if("snapshot"===s){process.stdout.write(function(e,t={}){let r=e.nodes,n=Array.isArray(r)?r:[],a=!!e.truncated,i="string"==typeof e.appName?e.appName:void 0,s="string"==typeof e.appBundleId?e.appBundleId:void 0,o=[];i&&o.push(`Page: ${i}`),s&&o.push(`App: ${s}`);let l=`Snapshot: ${n.length} nodes${a?" (truncated)":""}`,c=o.length>0?`${o.join("\n")}
74
78
  `:"";if(0===n.length)return`${c}${l}
75
79
  `;if(t.raw){let e=n.map(e=>JSON.stringify(e));return`${c}${l}
76
80
  ${e.join("\n")}
77
- `}if(t.flatten){let e=n.map(e=>u(e,0,!1));return`${c}${l}
81
+ `}if(t.flatten){let e=n.map(e=>p(e,0,!1));return`${c}${l}
78
82
  ${e.join("\n")}
79
- `}let d=[],f=[];for(let e of n){let t=e.depth??0;for(;d.length>0&&t<=d[d.length-1];)d.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n="group"===p(e.type??"Element")&&!r;n&&d.push(t);let a=n?t:Math.max(0,t-d.length);f.push(u(e,a,n))}return`${c}${l}
80
- ${f.join("\n")}
81
- `}(e.data??{},{raw:f.snapshotRaw,flatten:f.snapshotInteractiveOnly})),h&&h();return}if("get"===a){let t=o[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
82
- `),h&&h();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
83
- `),h&&h();return}}if("find"===a){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
84
- `),h&&h();return}if("boolean"==typeof t?.found){process.stdout.write(`Found: ${t.found}
85
- `),h&&h();return}if(t?.node){process.stdout.write(`${JSON.stringify(t.node,null,2)}
86
- `),h&&h();return}}if("is"===a){let t=e.data?.predicate??"assertion";process.stdout.write(`Passed: is ${t}
87
- `),h&&h();return}if("click"===a){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
88
- `),h&&h();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===a){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",a="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${a}`});process.stdout.write(`${e.join("\n")}
89
- `),h&&h();return}if("apps"===a){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
90
- `),h&&h();return}if("appstate"===a){let e=t?.platform,r=t?.appBundleId,n=t?.appName,a=t?.source,i=t?.package,s=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
83
+ `}let d=[],u=[];for(let e of n){let t=e.depth??0;for(;d.length>0&&t<=d[d.length-1];)d.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n="group"===f(e.type??"Element")&&!r;n&&d.push(t);let a=n?t:Math.max(0,t-d.length);u.push(p(e,a,n))}return`${c}${l}
84
+ ${u.join("\n")}
85
+ `}(e.data??{},{raw:m.snapshotRaw,flatten:m.snapshotInteractiveOnly})),w&&w();return}if("get"===s){let t=c[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t}
86
+ `),w&&w();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)}
87
+ `),w&&w();return}}if("find"===s){let t=e.data;if("string"==typeof t?.text){process.stdout.write(`${t.text}
88
+ `),w&&w();return}if("boolean"==typeof t?.found){process.stdout.write(`Found: ${t.found}
89
+ `),w&&w();return}if(t?.node){process.stdout.write(`${JSON.stringify(t.node,null,2)}
90
+ `),w&&w();return}}if("is"===s){let t=e.data?.predicate??"assertion";process.stdout.write(`Passed: is ${t}
91
+ `),w&&w();return}if("boot"===s){let t=e.data?.platform??"unknown",r=e.data?.device??e.data?.id??"unknown";process.stdout.write(`Boot ready: ${r} (${t})
92
+ `),w&&w();return}if("click"===s){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n})
93
+ `),w&&w();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===s){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",a="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${a}`});process.stdout.write(`${e.join("\n")}
94
+ `),w&&w();return}if("apps"===s){let e=(Array.isArray(t.apps)?t.apps:[]).map(e=>{if("string"==typeof e)return e;if(e&&"object"==typeof e){let t=e.bundleId??e.package,r=e.name??e.label;return r&&t?`${r} (${t})`:t&&"boolean"==typeof e.launchable?`${t} (launchable=${e.launchable})`:t?String(t):JSON.stringify(e)}return String(e)});process.stdout.write(`${e.join("\n")}
95
+ `),w&&w();return}if("appstate"===s){let e=t?.platform,r=t?.appBundleId,n=t?.appName,a=t?.source,i=t?.package,s=t?.activity;if("ios"===e){process.stdout.write(`Foreground app: ${n??r}
91
96
  `),r&&process.stdout.write(`Bundle: ${r}
92
97
  `),a&&process.stdout.write(`Source: ${a}
93
- `),h&&h();return}if("android"===e){process.stdout.write(`Foreground app: ${i??"unknown"}
98
+ `),w&&w();return}if("android"===e){process.stdout.write(`Foreground app: ${i??"unknown"}
94
99
  `),s&&process.stdout.write(`Activity: ${s}
95
- `),h&&h();return}}}h&&h();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(f.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(d(e),f.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(`
100
+ `),w&&w();return}}}w&&w();return}throw new o(e.error.code,e.error.message,e.error.details)}catch(r){let e=t(r);if(m.json)d({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(u(e),m.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(`
96
101
  [daemon log]
97
102
  ${n}
98
- `)}}catch{}h&&h(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{d(r(e)),process.exit(1)}),S(process.argv.slice(2));
103
+ `)}}catch{}w&&w(),process.exit(1)}}r(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{u(t(e)),process.exit(1)}),S(process.argv.slice(2));