agent-device 0.7.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -25,6 +25,8 @@ Use this skill as a router, not a full manual.
25
25
  - Normal UI task: `open` -> `snapshot -i` -> `press/fill` -> `diff snapshot -i` -> `close`
26
26
  - Debug/crash: `open <app>` -> `logs clear --restart` -> reproduce -> `network dump` -> `logs path` -> targeted `grep`
27
27
  - Replay drift: `replay -u <path>` -> verify updated selectors
28
+ - Remote multi-tenant run: allocate lease -> run commands with tenant isolation flags -> heartbeat/release lease
29
+ - Device-scope isolation run: set iOS simulator set / Android allowlist -> run selectors within scope only
28
30
 
29
31
  ## Canonical Flows
30
32
 
@@ -57,12 +59,42 @@ Logging is off by default. Enable only for debugging windows.
57
59
  agent-device replay -u ./session.ad
58
60
  ```
59
61
 
62
+ ### 4) Remote Tenant Lease Flow (HTTP JSON-RPC)
63
+
64
+ ```bash
65
+ # Allocate lease
66
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
67
+ -H "content-type: application/json" \
68
+ -H "Authorization: Bearer <token>" \
69
+ -d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"runId":"run-123","tenantId":"acme","ttlMs":60000}}'
70
+
71
+ # Use lease in tenant-isolated command execution
72
+ agent-device --daemon-transport http \
73
+ --tenant acme \
74
+ --session-isolation tenant \
75
+ --run-id run-123 \
76
+ --lease-id <lease-id> \
77
+ session list --json
78
+
79
+ # Heartbeat and release
80
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
81
+ -H "content-type: application/json" \
82
+ -H "Authorization: Bearer <token>" \
83
+ -d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
84
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
85
+ -H "content-type: application/json" \
86
+ -H "Authorization: Bearer <token>" \
87
+ -d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
88
+ ```
89
+
60
90
  ## Command Skeleton (Minimal)
61
91
 
62
92
  ### Session and navigation
63
93
 
64
94
  ```bash
65
95
  agent-device devices
96
+ agent-device devices --platform ios --ios-simulator-device-set /tmp/tenant-a/simulators
97
+ agent-device devices --platform android --android-device-allowlist emulator-5554,device-1234
66
98
  agent-device open [app|url] [url]
67
99
  agent-device open [app] --relaunch
68
100
  agent-device close [app]
@@ -70,8 +102,16 @@ agent-device session list
70
102
  ```
71
103
 
72
104
  Use `boot` only as fallback when `open` cannot find/connect to a ready target.
105
+ For Android emulators by AVD name, use `boot --platform android --device <avd-name>`.
106
+ For Android emulators without GUI, add `--headless`.
73
107
  Use `--target mobile|tv` with `--platform` (required) to pick phone/tablet vs TV targets (AndroidTV/tvOS).
74
108
 
109
+ Isolation scoping quick reference:
110
+ - `--ios-simulator-device-set <path>` scopes iOS simulator discovery + command execution to one simulator set.
111
+ - `--android-device-allowlist <serials>` scopes Android discovery/selection to comma/space separated serials.
112
+ - Scope is applied before selectors (`--device`, `--udid`, `--serial`); out-of-scope selectors fail with `DEVICE_NOT_FOUND`.
113
+ - With iOS simulator-set scope enabled, iOS physical devices are not enumerated.
114
+
75
115
  TV quick reference:
76
116
  - AndroidTV: `open`/`apps` use TV launcher discovery automatically.
77
117
  - TV target selection works on emulators/simulators and connected physical devices (AndroidTV + AppleTV).
@@ -101,6 +141,7 @@ agent-device clipboard write "token"
101
141
  agent-device perf --json
102
142
  agent-device network dump [limit] [summary|headers|body|all]
103
143
  agent-device push <bundle|package> <payload.json|inline-json>
144
+ agent-device trigger-app-event screenshot_taken '{"source":"qa"}'
104
145
  agent-device get text @e1
105
146
  agent-device screenshot out.png
106
147
  agent-device settings permission grant notifications
@@ -129,16 +170,23 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
129
170
  - iOS `appstate` is session-scoped; Android `appstate` is live foreground state.
130
171
  - Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on Android and iOS simulators; iOS physical devices are not supported yet.
131
172
  - `network dump` is best-effort and parses HTTP(s) entries from the session app log file.
132
- - iOS settings helpers are simulator-only; use `appearance light|dark|toggle` and faceid `match|nonmatch|enroll|unenroll`.
173
+ - Biometric settings: iOS simulator supports `settings faceid|touchid <match|nonmatch|enroll|unenroll>`; Android supports `settings fingerprint <match|nonmatch>` where runtime tooling is available.
133
174
  - For AndroidTV/tvOS selection, always pair `--target` with `--platform` (`ios`, `android`, or `apple` alias); target-only selection is invalid.
134
175
  - `push` simulates notification delivery:
135
176
  - iOS simulator uses APNs-style payload JSON.
136
177
  - Android uses broadcast action + typed extras (string/boolean/number).
178
+ - `trigger-app-event` requires app-defined deep-link hooks and URL template configuration (`AGENT_DEVICE_APP_EVENT_URL_TEMPLATE` or platform-specific variants).
179
+ - `trigger-app-event` requires an active session or explicit selectors (`--platform`, `--device`, `--udid`, `--serial`); on iOS physical devices, custom-scheme triggers require active app context.
180
+ - Canonical trigger behavior and caveats are documented in [`website/docs/docs/commands.md`](../../website/docs/docs/commands.md) under **App event triggers**.
137
181
  - Permission settings are app-scoped and require an active session app:
138
182
  `settings permission <grant|deny|reset> <camera|microphone|photos|contacts|notifications> [full|limited]`
139
183
  - `full|limited` mode applies only to iOS `photos`; other targets reject mode.
140
184
  - On Android, non-ASCII `fill/type` may require an ADB keyboard IME on some system images; only install IME APKs from trusted sources and verify checksum/signature.
141
185
  - If using `--save-script`, prefer explicit path syntax (`--save-script=flow.ad` or `./flow.ad`).
186
+ - For tenant-isolated remote runs, always pass `--tenant`, `--session-isolation tenant`, `--run-id`, and `--lease-id` together.
187
+ - Use short lease TTLs and heartbeat only while work is active; release leases immediately after run completion/failure.
188
+ - Env equivalents for scoped runs: `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat `IOS_SIMULATOR_DEVICE_SET`) and
189
+ `AGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST` (compat `ANDROID_DEVICE_ALLOWLIST`).
142
190
 
143
191
  ## Security and Trust Notes
144
192
 
@@ -146,6 +194,7 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
146
194
  - If install is required, pin an exact version (for example: `npx --yes agent-device@<exact-version> --help`).
147
195
  - Signing/provisioning environment variables are optional, sensitive, and only for iOS physical-device setup.
148
196
  - Logs/artifacts are written under `~/.agent-device`; replay scripts write to explicit paths you provide.
197
+ - For remote daemon mode, prefer `AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual` with `AGENT_DEVICE_HTTP_AUTH_HOOK` and tenant-scoped lease admission.
149
198
  - Keep logging off unless debugging and use least-privilege/isolated environments for autonomous runs.
150
199
 
151
200
  ## Common Mistakes
@@ -165,3 +214,4 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
165
214
  - [references/coordinate-system.md](references/coordinate-system.md)
166
215
  - [references/batching.md](references/batching.md)
167
216
  - [references/perf-metrics.md](references/perf-metrics.md)
217
+ - [references/remote-tenancy.md](references/remote-tenancy.md)
@@ -0,0 +1,77 @@
1
+ # Remote Tenancy and Lease Admission
2
+
3
+ Use this reference for remote daemon HTTP flows that require explicit
4
+ tenant/run admission control.
5
+
6
+ ## Transport prerequisites
7
+
8
+ - Start daemon in HTTP mode (`AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual`).
9
+ - Use a token from daemon metadata or `Authorization: Bearer <token>`.
10
+ - Prefer an auth hook (`AGENT_DEVICE_HTTP_AUTH_HOOK`) for caller validation and
11
+ tenant injection.
12
+
13
+ ## Lease lifecycle (JSON-RPC)
14
+
15
+ Use `POST /rpc` with JSON-RPC 2.0 methods:
16
+
17
+ - `agent_device.lease.allocate`
18
+ - `agent_device.lease.heartbeat`
19
+ - `agent_device.lease.release`
20
+
21
+ Example allocate:
22
+
23
+ ```bash
24
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
25
+ -H "content-type: application/json" \
26
+ -H "Authorization: Bearer <token>" \
27
+ -d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"tenantId":"acme","runId":"run-123","ttlMs":60000}}'
28
+ ```
29
+
30
+ Example heartbeat:
31
+
32
+ ```bash
33
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
34
+ -H "content-type: application/json" \
35
+ -H "Authorization: Bearer <token>" \
36
+ -d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
37
+ ```
38
+
39
+ Example release:
40
+
41
+ ```bash
42
+ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
43
+ -H "content-type: application/json" \
44
+ -H "Authorization: Bearer <token>" \
45
+ -d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
46
+ ```
47
+
48
+ ## Command admission contract
49
+
50
+ For tenant-isolated command execution, pass all four flags:
51
+
52
+ ```bash
53
+ agent-device --daemon-transport http \
54
+ --tenant acme \
55
+ --session-isolation tenant \
56
+ --run-id run-123 \
57
+ --lease-id <lease-id> \
58
+ session list --json
59
+ ```
60
+
61
+ Admission checks require tenant/run/lease scope alignment.
62
+
63
+ ## Failure semantics
64
+
65
+ - Missing tenant/run/lease fields in tenant isolation mode: `INVALID_ARGS`
66
+ - Lease not active or wrong scope: `UNAUTHORIZED`
67
+ - Method mismatch: JSON-RPC `-32601` (HTTP 404)
68
+
69
+ ## Operational guidance
70
+
71
+ - Keep TTL short and heartbeat only while a run is active.
72
+ - Release lease immediately on run completion/error paths.
73
+ - For bounded hosts, configure:
74
+ - `AGENT_DEVICE_MAX_SIMULATOR_LEASES`
75
+ - `AGENT_DEVICE_LEASE_TTL_MS`
76
+ - `AGENT_DEVICE_LEASE_MIN_TTL_MS`
77
+ - `AGENT_DEVICE_LEASE_MAX_TTL_MS`
@@ -14,6 +14,8 @@ Sessions isolate device context. A device can only be held by one session at a t
14
14
  - Name sessions semantically.
15
15
  - Close sessions when done.
16
16
  - Use separate sessions for parallel work.
17
+ - For remote tenant-scoped automation, run commands with:
18
+ `--tenant <id> --session-isolation tenant --run-id <id> --lease-id <id>`
17
19
  - In iOS sessions, use `open <app>`. `open <url>` opens deep links; on devices `http(s)://` opens Safari when no app is active, and custom schemes require an active app in the session.
18
20
  - In iOS sessions, `open <app> <url>` opens a deep link.
19
21
  - On iOS, `appstate` is session-scoped and requires a matching active session on the target device.
@@ -23,6 +25,22 @@ Sessions isolate device context. A device can only be held by one session at a t
23
25
  - For deterministic replay scripts, prefer selector-based actions and assertions.
24
26
  - Use `replay -u` to update selector drift during maintenance.
25
27
 
28
+ ## Scoped device isolation
29
+
30
+ Use scoped discovery when sessions must not see host-global device lists.
31
+
32
+ ```bash
33
+ agent-device devices --platform ios --ios-simulator-device-set /tmp/tenant-a/simulators
34
+ agent-device devices --platform android --android-device-allowlist emulator-5554,device-1234
35
+ ```
36
+
37
+ - Scope is applied before selectors (`--device`, `--udid`, `--serial`).
38
+ - If selector target is outside scope, resolution fails with `DEVICE_NOT_FOUND`.
39
+ - With iOS simulator-set scope enabled, iOS physical devices are not enumerated.
40
+ - Environment equivalents:
41
+ - `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat: `IOS_SIMULATOR_DEVICE_SET`)
42
+ - `AGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST` (compat: `ANDROID_DEVICE_ALLOWLIST`)
43
+
26
44
  ## Listing sessions
27
45
 
28
46
  ```bash
@@ -35,3 +53,9 @@ agent-device session list
35
53
  agent-device replay ./session.ad --session auth
36
54
  agent-device replay -u ./session.ad --session auth
37
55
  ```
56
+
57
+ ## Tenant isolation note
58
+
59
+ When session isolation is set to tenant mode, session namespace is scoped as
60
+ `<tenant>:<session>`. For remote runs, allocate and maintain an active lease
61
+ for the same tenant/run scope before executing tenant-isolated commands.
package/dist/src/735.js DELETED
@@ -1,3 +0,0 @@
1
- import{AsyncLocalStorage as e}from"node:async_hooks";import t from"node:crypto";import r,{promises as n}from"node:fs";import o from"node:os";import i from"node:path";import{fileURLToPath as a,pathToFileURL as s}from"node:url";import{spawn as c,spawnSync as l}from"node:child_process";let d=new e,u=/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key)/i,f=/(bearer\s+[a-z0-9._-]+|(?:api[_-]?key|token|secret|password)\s*[=:]\s*\S+)/i;function m(){return t.randomBytes(8).toString("hex")}async function p(e,r){let n={...e,diagnosticId:`${Date.now().toString(36)}-${t.randomBytes(4).toString("hex")}`,events:[]};return await d.run(n,r)}function h(){let e=d.getStore();return e?{diagnosticId:e.diagnosticId,requestId:e.requestId,session:e.session,command:e.command,debug:e.debug}:{}}function g(e){let t=d.getStore();if(!t)return;let n={ts:new Date().toISOString(),level:e.level??"info",phase:e.phase,session:t.session,requestId:t.requestId,command:t.command,durationMs:e.durationMs,data:e.data?y(e.data):void 0};if(t.events.push(n),!t.debug)return;let o=`[agent-device][diag] ${JSON.stringify(n)}
2
- `;try{t.logPath&&r.appendFile(t.logPath,o,()=>{}),t.traceLogPath&&r.appendFile(t.traceLogPath,o,()=>{}),t.logPath||t.traceLogPath||process.stderr.write(o)}catch{}}async function w(e,t,r){let n=Date.now();try{let o=await t();return g({level:"info",phase:e,durationMs:Date.now()-n,data:r}),o}catch(t){throw g({level:"error",phase:e,durationMs:Date.now()-n,data:{...r??{},error:t instanceof Error?t.message:String(t)}}),t}}function S(e={}){let t=d.getStore();if(!t||!e.force&&!t.debug||0===t.events.length)return null;try{let e=(t.session??"default").replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date().toISOString().slice(0,10),a=i.join(o.homedir(),".agent-device","logs",e,n);r.mkdirSync(a,{recursive:!0});let s=new Date().toISOString().replace(/[:.]/g,"-"),c=i.join(a,`${s}-${t.diagnosticId}.ndjson`),l=t.events.map(e=>JSON.stringify(y(e)));return r.writeFileSync(c,`${l.join("\n")}
3
- `),t.events=[],c}catch{return null}}function y(e){return function e(t,r,n){if(null==t)return t;if("string"==typeof t){var o=t,i=n;let e=o.trim();if(!e)return o;if(i&&u.test(i)||f.test(e))return"[REDACTED]";let r=function(e){try{let t=new URL(e);return t.search&&(t.search="?REDACTED"),(t.username||t.password)&&(t.username="REDACTED",t.password="REDACTED"),t.toString()}catch{return null}}(e);return r||(e.length>400?`${e.slice(0,200)}...<truncated>`:e)}if("object"!=typeof t)return t;if(r.has(t))return"[Circular]";if(r.add(t),Array.isArray(t))return t.map(t=>e(t,r));let a={};for(let[n,o]of Object.entries(t)){if(u.test(n)){a[n]="[REDACTED]";continue}a[n]=e(o,r,n)}return a}(e,new WeakSet)}class A extends Error{code;details;cause;constructor(e,t,r,n){super(t),this.code=e,this.details=r,this.cause=n}}function I(e){return e instanceof A?e:e instanceof Error?new A("UNKNOWN",e.message,void 0,e):new A("UNKNOWN","Unknown error",{err:e})}function E(e,t={}){let r=I(e),n=r.details?y(r.details):void 0,o=n&&"string"==typeof n.hint?n.hint:void 0,i=(n&&"string"==typeof n.diagnosticId?n.diagnosticId:void 0)??t.diagnosticId,a=(n&&"string"==typeof n.logPath?n.logPath:void 0)??t.logPath,s=o??function(e){switch(e){case"INVALID_ARGS":return"Check command arguments and run --help for usage examples.";case"SESSION_NOT_FOUND":return"Run open first or pass an explicit device selector.";case"TOOL_MISSING":return"Install required platform tooling and ensure it is available in PATH.";case"DEVICE_NOT_FOUND":return"Verify the target device is booted/connected and selectors match.";case"UNSUPPORTED_OPERATION":return"This command is not available for the selected platform/device.";case"COMMAND_FAILED":default:return"Retry with --debug and inspect diagnostics log for details.";case"UNAUTHORIZED":return"Refresh daemon metadata and retry the command."}}(r.code),c=function(e){if(!e)return;let t={...e};return delete t.hint,delete t.diagnosticId,delete t.logPath,Object.keys(t).length>0?t:void 0}(n),l=function(e,t,r){if("COMMAND_FAILED"!==e||r?.processExitError!==!0)return t;let n=function(e){let t=[/^an error was encountered processing the command/i,/^underlying error\b/i,/^simulator device failed to complete the requested operation/i];for(let r of e.split("\n")){let e=r.trim();if(e&&!t.some(t=>t.test(e)))return e.length>200?`${e.slice(0,200)}...`:e}return null}("string"==typeof r?.stderr?r.stderr:"");return n||t}(r.code,r.message,n);return{code:r.code,message:l,hint:s,diagnosticId:i,logPath:a,details:c}}let v="<wifi|airplane|location> <on|off>",D="appearance <light|dark|toggle>",b="faceid <match|nonmatch|enroll|unenroll>",N="permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]",$=`settings ${v} | settings ${D} | settings ${b} | settings ${N}`,_=`settings requires ${v}, ${D}, ${b}, or ${N}`;function M(e){let t=[],r=[];for(let n of e){let e=n.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let o=n.label?.trim()||n.value?.trim()||n.identifier?.trim()||"",i=L(n.type??"Element"),a="group"===i&&!o;a&&t.push(e);let s=a?e:Math.max(0,e-t.length);r.push({node:n,depth:s,type:i,text:O(n,s,a,i)})}return r}function O(e,t,r,n){let o=n??L(e.type??"Element"),i=T(e,o),a=" ".repeat(t),s=e.ref?`@${e.ref}`:"",c=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),l=c?` [${c}]`:"",d=i?` "${i}"`:"";return r?`${a}${s} [${o}]${l}`.trimEnd():`${a}${s} [${o}]${d}${l}`.trimEnd()}function T(e,t){var r,n;let o=e.label?.trim(),i=e.value?.trim();if("text-field"===(r=t)||"text-view"===r||"search"===r){if(i)return i;if(o)return o}else if(o)return o;if(i)return i;let a=e.identifier?.trim();return!a||(n=a,/^[\w.]+:id\/[\w.-]+$/i.test(n)&&("group"===t||"image"===t||"list"===t||"collection"===t))?"":a}function L(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.includes(".")&&(t=t.replace(/^android\.widget\./,"").replace(/^android\.view\./,"").replace(/^android\.webkit\./,"").replace(/^androidx\./,"").replace(/^com\.google\.android\./,"").replace(/^com\.android\./,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":case"imagebutton":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"checkedtextview":return"text";case"textfield":case"edittext":return"text-field";case"textview":return r?"text":"text-view";case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":case"imageview":return"image";case"webview":return"webview";case"framelayout":case"linearlayout":case"relativelayout":case"constraintlayout":case"viewgroup":case"view":case"group":return"group";case"listview":case"recyclerview":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":case"scrollview":case"nestedscrollview":return"scroll-area";case"table":return"table";default:return t||"element"}}function x(){try{let e=C();return JSON.parse(r.readFileSync(i.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}function C(){let e=i.dirname(a(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=i.join(t,"package.json");if(r.existsSync(e))return t;t=i.dirname(t)}return e}async function R(e,t,r={}){return new Promise((n,o)=>{let i=c(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached}),a="",s=r.binaryStdout?Buffer.alloc(0):void 0,l="",d=!1,u=j(r.timeoutMs),f=u?setTimeout(()=>{d=!0,i.kill("SIGKILL")},u):null;r.binaryStdout||i.stdout.setEncoding("utf8"),i.stderr.setEncoding("utf8"),void 0!==r.stdin&&i.stdin.write(r.stdin),i.stdin.end(),i.stdout.on("data",e=>{r.binaryStdout?s=Buffer.concat([s??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):a+=e}),i.stderr.on("data",e=>{l+=e}),i.on("error",r=>{(f&&clearTimeout(f),"ENOENT"===r.code)?o(new A("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):o(new A("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),i.on("close",i=>{f&&clearTimeout(f);let c=i??1;d&&u?o(new A("COMMAND_FAILED",`${e} timed out after ${u}ms`,{cmd:e,args:t,stdout:a,stderr:l,exitCode:c,timeoutMs:u})):0===c||r.allowFailure?n({stdout:a,stderr:l,exitCode:c,stdoutBuffer:s}):o(new A("COMMAND_FAILED",`${e} exited with code ${c}`,{cmd:e,args:t,stdout:a,stderr:l,exitCode:c,processExitError:!0}))})})}async function F(e){try{var t;let{shell:r,args:n}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),o=await R(r,n,{allowFailure:!0});return 0===o.exitCode&&o.stdout.trim().length>0}catch{return!1}}function P(e,t,r={}){let n=l(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],encoding:r.binaryStdout?void 0:"utf8",input:r.stdin,timeout:j(r.timeoutMs)});if(n.error){let o=n.error.code;if("ETIMEDOUT"===o)throw new A("COMMAND_FAILED",`${e} timed out after ${j(r.timeoutMs)}ms`,{cmd:e,args:t,timeoutMs:j(r.timeoutMs)},n.error);if("ENOENT"===o)throw new A("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n.error);throw new A("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n.error)}let o=r.binaryStdout?Buffer.isBuffer(n.stdout)?n.stdout:Buffer.from(n.stdout??""):void 0,i=r.binaryStdout?"":"string"==typeof n.stdout?n.stdout:(n.stdout??"").toString(),a="string"==typeof n.stderr?n.stderr:(n.stderr??"").toString(),s=n.status??1;if(0!==s&&!r.allowFailure)throw new A("COMMAND_FAILED",`${e} exited with code ${s}`,{cmd:e,args:t,stdout:i,stderr:a,exitCode:s,processExitError:!0});return{stdout:i,stderr:a,exitCode:s,stdoutBuffer:o}}function k(e,t,r={}){c(e,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function B(e,t,r={}){return new Promise((n,o)=>{let i=c(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached});r.onSpawn?.(i);let a="",s="",l=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||i.stdout.setEncoding("utf8"),i.stderr.setEncoding("utf8"),void 0!==r.stdin&&i.stdin.write(r.stdin),i.stdin.end(),i.stdout.on("data",e=>{if(r.binaryStdout){l=Buffer.concat([l??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);a+=t,r.onStdoutChunk?.(t)}),i.stderr.on("data",e=>{let t=String(e);s+=t,r.onStderrChunk?.(t)}),i.on("error",r=>{"ENOENT"===r.code?o(new A("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):o(new A("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),i.on("close",i=>{let c=i??1;0===c||r.allowFailure?n({stdout:a,stderr:s,exitCode:c,stdoutBuffer:l}):o(new A("COMMAND_FAILED",`${e} exited with code ${c}`,{cmd:e,args:t,stdout:a,stderr:s,exitCode:c,processExitError:!0}))})})}function G(e,t,r={}){let n=c(e,t,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"],detached:r.detached}),o="",i="";n.stdout.setEncoding("utf8"),n.stderr.setEncoding("utf8"),n.stdout.on("data",e=>{o+=e}),n.stderr.on("data",e=>{i+=e});let a=new Promise((a,s)=>{n.on("error",r=>{"ENOENT"===r.code?s(new A("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):s(new A("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),n.on("close",n=>{let c=n??1;0===c||r.allowFailure?a({stdout:o,stderr:i,exitCode:c}):s(new A("COMMAND_FAILED",`${e} exited with code ${c}`,{cmd:e,args:t,stdout:o,stderr:i,exitCode:c,processExitError:!0}))})});return{child:n,wait:a}}function j(e){if(!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t}let U=[/(^|[\/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,/(^|[\/\s"'=])src\/daemon\.ts($|[\s"'])/];function V(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function q(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=P("ps",["-p",String(e),"-o","lstart="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function H(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=P("ps",["-p",String(e),"-o","command="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function J(e,t){let r;if(!V(e))return!1;if(t){let r=q(e);if(!r||r!==t)return!1}let n=H(e);return!!n&&!!(r=n.toLowerCase().replaceAll("\\","/")).includes("agent-device")&&U.some(e=>e.test(r))}function W(e,t){try{return process.kill(e,t),!0}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return!1;throw t}}async function z(e,t){if(!V(e))return!0;let r=Date.now();for(;Date.now()-r<t;)if(await new Promise(e=>setTimeout(e,50)),!V(e))return!0;return!V(e)}async function K(e,t){!J(e,t.expectedStartTime)||!W(e,"SIGTERM")||await z(e,t.termTimeoutMs)||W(e,"SIGKILL")&&await z(e,t.killTimeoutMs)}let X=100,Z=new Set(["batch","replay"]);function Q(e){let t;try{t=JSON.parse(e)}catch{throw new A("INVALID_ARGS","Batch steps must be valid JSON.")}if(!Array.isArray(t)||0===t.length)throw new A("INVALID_ARGS","Batch steps must be a non-empty JSON array.");return t}function Y(e,t){if(!Array.isArray(e)||0===e.length)throw new A("INVALID_ARGS","batch requires a non-empty batchSteps array.");if(e.length>t)throw new A("INVALID_ARGS",`batch has ${e.length} steps; max allowed is ${t}.`);let r=[];for(let t=0;t<e.length;t+=1){let n=e[t];if(!n||"object"!=typeof n)throw new A("INVALID_ARGS",`Invalid batch step at index ${t}.`);let o="string"==typeof n.command?n.command.trim().toLowerCase():"";if(!o)throw new A("INVALID_ARGS",`Batch step ${t+1} requires command.`);if(Z.has(o))throw new A("INVALID_ARGS",`Batch step ${t+1} cannot run ${o}.`);if(void 0!==n.positionals&&!Array.isArray(n.positionals))throw new A("INVALID_ARGS",`Batch step ${t+1} positionals must be an array.`);let i=n.positionals??[];if(i.some(e=>"string"!=typeof e))throw new A("INVALID_ARGS",`Batch step ${t+1} positionals must contain only strings.`);if(void 0!==n.flags&&("object"!=typeof n.flags||Array.isArray(n.flags)||!n.flags))throw new A("INVALID_ARGS",`Batch step ${t+1} flags must be an object.`);r.push({command:o,positionals:i,flags:n.flags??{}})}return r}export{default as node_net}from"node:net";export{A as AppError,X as DEFAULT_BATCH_MAX_STEPS,_ as SETTINGS_INVALID_ARGS_MESSAGE,$ as SETTINGS_USAGE_OVERRIDE,I as asAppError,M as buildSnapshotDisplayLines,m as createRequestId,T as displayLabel,g as emitDiagnostic,a as fileURLToPath,C as findProjectRoot,S as flushDiagnosticsToSessionFile,L as formatRole,O as formatSnapshotLine,h as getDiagnosticsMeta,J as isAgentDeviceDaemonProcess,V as isProcessAlive,t as node_crypto,r as node_fs,o as node_os,i as node_path,E as normalizeError,Q as parseBatchStepsJson,s as pathToFileURL,n as promises,H as readProcessCommand,q as readProcessStartTime,x as readVersion,R as runCmd,G as runCmdBackground,k as runCmdDetached,B as runCmdStreaming,P as runCmdSync,c as spawn,K as stopProcessForTakeover,Y as validateAndNormalizeBatchSteps,F as whichCmd,w as withDiagnosticTimer,p as withDiagnosticsScope};