@houwert/conductor 0.5.0 → 0.6.0

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 (52) hide show
  1. package/README.md +12 -9
  2. package/dist/commands/assert-not-visible.js +4 -0
  3. package/dist/commands/assert-visible.js +4 -0
  4. package/dist/commands/back.js +4 -0
  5. package/dist/commands/cheat-sheet.js +11 -8
  6. package/dist/commands/delete-device.js +222 -0
  7. package/dist/commands/device-pool.js +33 -22
  8. package/dist/commands/download-app.js +87 -0
  9. package/dist/commands/erase-text.js +4 -0
  10. package/dist/commands/focused.js +39 -0
  11. package/dist/commands/foreground-app.js +4 -0
  12. package/dist/commands/hide-keyboard.js +4 -0
  13. package/dist/commands/inspect.js +8 -0
  14. package/dist/commands/install.js +103 -27
  15. package/dist/commands/launch-app.js +6 -0
  16. package/dist/commands/list-devices.js +39 -2
  17. package/dist/commands/logs.js +193 -0
  18. package/dist/commands/press-key.js +16 -0
  19. package/dist/commands/screenshot.js +1 -1
  20. package/dist/commands/scroll-until-visible.js +11 -0
  21. package/dist/commands/scroll.js +6 -0
  22. package/dist/commands/start-device.js +38 -4
  23. package/dist/commands/stop-app.js +4 -0
  24. package/dist/commands/swipe.js +22 -0
  25. package/dist/commands/tap.js +10 -3
  26. package/dist/commands/type.js +2 -2
  27. package/dist/commands/uninstall-app.js +4 -0
  28. package/dist/daemon/client.js +72 -9
  29. package/dist/daemon/log-collector.js +408 -0
  30. package/dist/daemon/server.js +110 -32
  31. package/dist/daemon/web-server.js +812 -0
  32. package/dist/device-picker.js +7 -2
  33. package/dist/drivers/bootstrap.js +124 -1
  34. package/dist/drivers/element-resolver.js +241 -30
  35. package/dist/drivers/flow-runner.js +63 -21
  36. package/dist/drivers/log-sources/android.js +156 -0
  37. package/dist/drivers/log-sources/daemon.js +112 -0
  38. package/dist/drivers/log-sources/ios.js +106 -0
  39. package/dist/drivers/log-sources/metro.js +252 -0
  40. package/dist/drivers/log-sources/types.js +13 -0
  41. package/dist/drivers/log-sources/web.js +96 -0
  42. package/dist/drivers/wait.js +57 -0
  43. package/dist/drivers/web.js +173 -0
  44. package/dist/index.js +62 -12
  45. package/dist/runner.js +32 -2
  46. package/drivers/ios/conductor-driver-ios.zip +0 -0
  47. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  48. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  50. package/package.json +5 -2
  51. package/skills/conductor/SKILL.md +72 -41
  52. package/skills/skills.yaml +1 -1
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ const run_parallel_js_1 = require("./commands/run-parallel.js");
32
32
  const foreground_app_js_1 = require("./commands/foreground-app.js");
33
33
  const list_apps_js_1 = require("./commands/list-apps.js");
34
34
  const copy_app_js_1 = require("./commands/copy-app.js");
35
+ const download_app_js_1 = require("./commands/download-app.js");
35
36
  const install_app_js_1 = require("./commands/install-app.js");
36
37
  const erase_text_js_1 = require("./commands/erase-text.js");
37
38
  const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
@@ -41,21 +42,25 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
41
42
  const set_location_js_1 = require("./commands/set-location.js");
42
43
  const set_orientation_js_1 = require("./commands/set-orientation.js");
43
44
  const start_device_js_1 = require("./commands/start-device.js");
45
+ const delete_device_js_1 = require("./commands/delete-device.js");
46
+ const logs_js_1 = require("./commands/logs.js");
44
47
  const device_picker_js_1 = require("./device-picker.js");
45
48
  const update_check_js_1 = require("./update-check.js");
46
49
  const COMMAND_HELP = {
47
50
  'start-device': start_device_js_1.HELP,
51
+ 'delete-device': delete_device_js_1.HELP,
48
52
  'list-devices': list_devices_js_1.HELP,
49
53
  'foreground-app': foreground_app_js_1.HELP,
50
54
  'list-apps': list_apps_js_1.HELP,
51
55
  'copy-app': copy_app_js_1.HELP,
56
+ 'download-app': download_app_js_1.HELP,
52
57
  'install-app': install_app_js_1.HELP,
53
58
  'launch-app': launch_app_js_1.HELP,
54
59
  'stop-app': stop_app_js_1.HELP,
55
60
  'clear-state': clear_state_js_1.HELP,
56
61
  'uninstall-app': uninstall_app_js_1.HELP,
57
- tap: tap_js_1.HELP,
58
- type: type_js_1.HELP,
62
+ 'tap-on': tap_js_1.HELP,
63
+ 'input-text': type_js_1.HELP,
59
64
  'erase-text': erase_text_js_1.HELP,
60
65
  back: back_js_1.HELP,
61
66
  'press-key': press_key_js_1.HELP,
@@ -68,23 +73,27 @@ const COMMAND_HELP = {
68
73
  'open-link': open_link_js_1.HELP,
69
74
  'set-location': set_location_js_1.HELP,
70
75
  'set-orientation': set_orientation_js_1.HELP,
71
- screenshot: screenshot_js_1.HELP,
76
+ 'take-screenshot': screenshot_js_1.HELP,
72
77
  inspect: inspect_js_1.HELP,
73
78
  focused: focused_js_1.HELP,
74
79
  'run-flow': run_flow_js_1.HELP,
75
80
  'run-flow-inline': run_flow_inline_js_1.HELP,
76
81
  session: session_js_1.HELP,
77
82
  'cheat-sheet': cheat_sheet_js_1.HELP,
78
- install: install_js_1.HELP,
83
+ 'install-plugin': install_js_1.HELP_INSTALL_PLUGIN,
84
+ 'install-skills': install_js_1.HELP_INSTALL_SKILLS,
85
+ 'install-web': install_js_1.HELP_INSTALL_WEB,
79
86
  'daemon-start': daemon_js_1.HELP_DAEMON_START,
80
87
  'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
81
88
  'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
82
89
  'device-pool': device_pool_js_1.HELP,
83
90
  'run-parallel': run_parallel_js_1.HELP,
91
+ logs: logs_js_1.HELP,
84
92
  };
85
93
  const OPTIONS_HELP = `Options:
86
94
  --device <id> Target device ID (also keys the session and daemon)
87
95
  --device-name <n> Target a booted device by name (resolved to ID from booted devices)
96
+ --platform <p> Filter to devices of this platform (ios, android, tvos, web)
88
97
  --json Output as machine-readable JSON
89
98
  --verbose, -v Log daemon calls, fallbacks, and raw output
90
99
  --help, -h Show this help`;
@@ -103,7 +112,6 @@ async function main() {
103
112
  'clear',
104
113
  'list',
105
114
  'verbose',
106
- 'skills',
107
115
  'check',
108
116
  'all',
109
117
  'acquire',
@@ -140,8 +148,12 @@ async function main() {
140
148
  'name',
141
149
  'device-name',
142
150
  'device-type',
151
+ 'browser',
143
152
  'from',
144
153
  'to',
154
+ 'source',
155
+ 'metro-port',
156
+ 'level',
145
157
  ],
146
158
  alias: { h: 'help', v: 'verbose' },
147
159
  });
@@ -164,11 +176,18 @@ async function main() {
164
176
  const NO_DEVICE_COMMANDS = new Set([
165
177
  'list-devices',
166
178
  'start-device',
179
+ 'delete-device',
167
180
  'cheat-sheet',
168
- 'install',
181
+ 'install-plugin',
182
+ 'install-skills',
183
+ 'install-web',
169
184
  'copy-app',
170
185
  'device-pool',
171
186
  'run-parallel',
187
+ // `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
188
+ ...(command === 'logs' && (argv['list'] || argv['source'] === 'metro') ? ['logs'] : []),
189
+ // `daemon-stop --all` stops every daemon — no device needed
190
+ ...(command === 'daemon-stop' && argv['all'] ? ['daemon-stop'] : []),
172
191
  ]);
173
192
  if (!NO_DEVICE_COMMANDS.has(command) && !COMMAND_HELP[command]) {
174
193
  console.error(`Unknown command: ${command}`);
@@ -196,7 +215,8 @@ async function main() {
196
215
  sessionName = match.id;
197
216
  }
198
217
  else {
199
- sessionName = explicitDevice ?? (await (0, device_picker_js_1.pickDevice)()) ?? 'default';
218
+ sessionName =
219
+ explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
200
220
  }
201
221
  }
202
222
  let exitCode = 0;
@@ -207,11 +227,18 @@ async function main() {
207
227
  avd: argv['avd'],
208
228
  name: argv['name'],
209
229
  deviceType: argv['device-type'],
230
+ browser: argv['browser'],
210
231
  });
211
232
  break;
212
233
  case 'list-devices':
213
234
  exitCode = await (0, list_devices_js_1.listDevices)(opts);
214
235
  break;
236
+ case 'delete-device':
237
+ exitCode = await (0, delete_device_js_1.deleteDevice)(rest[0], opts, {
238
+ platform: argv['platform'],
239
+ all: argv['all'],
240
+ });
241
+ break;
215
242
  case 'foreground-app':
216
243
  exitCode = await (0, foreground_app_js_1.foregroundApp)(opts, sessionName);
217
244
  break;
@@ -225,6 +252,11 @@ async function main() {
225
252
  exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
226
253
  break;
227
254
  }
255
+ case 'download-app': {
256
+ const appId = rest[0] ?? '';
257
+ exitCode = await (0, download_app_js_1.downloadApp)(appId, argv['output'], opts, sessionName);
258
+ break;
259
+ }
228
260
  case 'install-app': {
229
261
  const appPath = rest[0] ?? '';
230
262
  exitCode = await (0, install_app_js_1.installApp)(appPath, opts, sessionName);
@@ -261,7 +293,7 @@ async function main() {
261
293
  exitCode = await (0, uninstall_app_js_1.uninstallApp)(appId, opts, sessionName);
262
294
  break;
263
295
  }
264
- case 'tap': {
296
+ case 'tap-on': {
265
297
  const element = rest.join(' ');
266
298
  exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
267
299
  id: argv['id'],
@@ -281,7 +313,7 @@ async function main() {
281
313
  });
282
314
  break;
283
315
  }
284
- case 'type': {
316
+ case 'input-text': {
285
317
  const text = rest.join(' ');
286
318
  exitCode = await (0, type_js_1.typeText)(text, opts, sessionName);
287
319
  break;
@@ -397,7 +429,7 @@ async function main() {
397
429
  exitCode = await (0, set_orientation_js_1.setOrientation)(orientation, opts, sessionName);
398
430
  break;
399
431
  }
400
- case 'screenshot': {
432
+ case 'take-screenshot': {
401
433
  const outPath = argv['output'];
402
434
  exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
403
435
  break;
@@ -411,6 +443,18 @@ async function main() {
411
443
  interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
412
444
  });
413
445
  break;
446
+ case 'logs':
447
+ exitCode = await (0, logs_js_1.logs)(opts, sessionName, {
448
+ source: argv['source'],
449
+ level: argv['level'],
450
+ metro: argv['metro'] || argv['metro-port'] !== undefined,
451
+ metroPort: argv['metro-port'] !== undefined ? Number(argv['metro-port']) : undefined,
452
+ target: argv['target'] !== undefined ? Number(argv['target']) : undefined,
453
+ list: argv['list'],
454
+ recent: argv['recent'] !== undefined ? Number(argv['recent']) : undefined,
455
+ duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
456
+ });
457
+ break;
414
458
  case 'run-flow': {
415
459
  const file = rest[0] ?? '';
416
460
  const rawEnv = argv['env'];
@@ -430,8 +474,14 @@ async function main() {
430
474
  case 'cheat-sheet':
431
475
  exitCode = await (0, cheat_sheet_js_1.cheatSheet)();
432
476
  break;
433
- case 'install':
434
- exitCode = await (0, install_js_1.installSkills)(opts, argv['skills'], argv['check']);
477
+ case 'install-plugin':
478
+ exitCode = await (0, install_js_1.installPluginCli)(opts, argv['check']);
479
+ break;
480
+ case 'install-skills':
481
+ exitCode = await (0, install_js_1.installSkillsCli)(opts, argv['check']);
482
+ break;
483
+ case 'install-web':
484
+ exitCode = await (0, install_js_1.installWebCli)(opts, argv['check'], rest[0]);
435
485
  break;
436
486
  case 'daemon-start':
437
487
  exitCode = await (0, daemon_js_1.daemonStart)(opts, sessionName);
package/dist/runner.js CHANGED
@@ -11,6 +11,7 @@ const flow_runner_js_1 = require("./drivers/flow-runner.js");
11
11
  const verbose_js_1 = require("./verbose.js");
12
12
  const ios_js_1 = require("./drivers/ios.js");
13
13
  const android_js_1 = require("./drivers/android.js");
14
+ const web_js_1 = require("./drivers/web.js");
14
15
  const bootstrap_js_1 = require("./drivers/bootstrap.js");
15
16
  const client_js_1 = require("./daemon/client.js");
16
17
  /**
@@ -92,8 +93,10 @@ async function getDriver(sessionName = 'default') {
92
93
  throw new Error('No device found. Connect a device or start a simulator, then run again.');
93
94
  }
94
95
  const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
95
- const port = await (0, bootstrap_js_1.getDriverPort)(platform, deviceId);
96
- (0, verbose_js_1.log)(`getDriver: platform=${platform} deviceId=${deviceId} port=${port}`);
96
+ // Web daemons resolve their own port after generating a unique session ID,
97
+ // so we defer getDriverPort for them to avoid allocating an unused port.
98
+ const port = platform !== 'web' ? await (0, bootstrap_js_1.getDriverPort)(platform, deviceId) : 0;
99
+ (0, verbose_js_1.log)(`getDriver: platform=${platform} deviceId=${deviceId} port=${port || '(deferred)'}`);
97
100
  let driver;
98
101
  if (platform === 'ios') {
99
102
  if (!(await (0, bootstrap_js_1.isPortOpen)(port))) {
@@ -121,6 +124,33 @@ async function getDriver(sessionName = 'default') {
121
124
  }
122
125
  driver = tvosDriver;
123
126
  }
127
+ else if (platform === 'web') {
128
+ const browser = (0, bootstrap_js_1.webBrowserName)(deviceId);
129
+ let webSession = deviceId;
130
+ if ((0, bootstrap_js_1.isUnqualifiedWebId)(deviceId)) {
131
+ const existing = await (0, client_js_1.findRunningWebSession)(browser);
132
+ if (existing) {
133
+ webSession = existing;
134
+ (0, verbose_js_1.log)(`Reusing running web session ${webSession}`);
135
+ }
136
+ else {
137
+ webSession = (0, bootstrap_js_1.generateWebSessionId)(browser);
138
+ (0, verbose_js_1.log)(`Generated new web session ${webSession}`);
139
+ }
140
+ }
141
+ const webPort = await (0, bootstrap_js_1.getDriverPort)('web', webSession);
142
+ if (!(await (0, bootstrap_js_1.isPortOpen)(webPort))) {
143
+ (0, verbose_js_1.log)(`Web driver not running — starting daemon for ${webSession}...`);
144
+ await (0, client_js_1.startDaemon)(webSession);
145
+ await waitForPort(webPort);
146
+ }
147
+ const webDriver = new web_js_1.WebDriver(webPort, '127.0.0.1', webSession);
148
+ if (!(await webDriver.isAlive())) {
149
+ throw new Error(`Web browser driver on port ${webPort} is not responding.\n` +
150
+ `Run: conductor daemon-start --device ${webSession}`);
151
+ }
152
+ driver = webDriver;
153
+ }
124
154
  else {
125
155
  // Ensure the daemon is running — it handles APK install and driver startup.
126
156
  await (0, client_js_1.startDaemon)(deviceId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,12 +35,15 @@
35
35
  "@grpc/grpc-js": "^1.12.0",
36
36
  "@grpc/proto-loader": "^0.7.15",
37
37
  "js-yaml": "^4.1.1",
38
- "minimist": "^1.2.8"
38
+ "minimist": "^1.2.8",
39
+ "playwright-core": "^1.52.0",
40
+ "ws": "^8.20.0"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/js-yaml": "^4.0.9",
42
44
  "@types/minimist": "^1.2.5",
43
45
  "@types/node": "^20.0.0",
46
+ "@types/ws": "^8.18.1",
44
47
  "@typescript-eslint/eslint-plugin": "^8.56.1",
45
48
  "@typescript-eslint/parser": "^8.56.1",
46
49
  "eslint": "^10.0.2",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor
3
- version: 0.2.0
3
+ version: 0.6.0
4
4
  description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
5
5
  metadata.openclaw:
6
6
  category: service
@@ -157,21 +157,21 @@ conductor uninstall-app com.example.myapp --device emulator-5554
157
157
 
158
158
  ---
159
159
 
160
- ### `tap <element>`
160
+ ### `tap-on <element>`
161
161
 
162
162
  Tap a UI element by its text label or accessibility ID. **Try the most obvious text label or ID first** — e.g. `"Sign In"`, `"Submit"`, `"btn_login"`. Only run `inspect` to look up the exact identifier if your first attempt fails. **For icon-only buttons (no visible text label), always run `inspect` first** to find the accessibility ID before tapping.
163
163
 
164
164
  ```bash
165
- conductor tap "Sign In"
166
- conductor tap --id "btn_login" # match by accessibility ID instead of text
167
- conductor tap --text "Edit" # match by text only (not id)
168
- conductor tap "Next" --index 1 # pick the 2nd match (0-based)
169
- conductor tap "Add to cart" --long-press
170
- conductor tap "Like" --double-tap
171
- conductor tap "Delete" --optional # do not fail if not found
172
- conductor tap "Edit" --below "Username" # tap "Edit" that is below the "Username" element
173
- conductor tap "Submit" --above "Footer"
174
- conductor tap ">" --right-of "Email"
165
+ conductor tap-on "Sign In"
166
+ conductor tap-on --id "btn_login" # match by accessibility ID instead of text
167
+ conductor tap-on --text "Edit" # match by text only (not id)
168
+ conductor tap-on "Next" --index 1 # pick the 2nd match (0-based)
169
+ conductor tap-on "Add to cart" --long-press
170
+ conductor tap-on "Like" --double-tap
171
+ conductor tap-on "Delete" --optional # do not fail if not found
172
+ conductor tap-on "Edit" --below "Username" # tap "Edit" that is below the "Username" element
173
+ conductor tap-on "Submit" --above "Footer"
174
+ conductor tap-on ">" --right-of "Email"
175
175
  ```
176
176
 
177
177
  Flags:
@@ -192,13 +192,13 @@ Flags:
192
192
 
193
193
  ---
194
194
 
195
- ### `type <text>`
195
+ ### `input-text <text>`
196
196
 
197
197
  Type text into the currently focused input field.
198
198
 
199
199
  ```bash
200
- conductor type "hello@example.com"
201
- conductor type "my password"
200
+ conductor input-text "hello@example.com"
201
+ conductor input-text "my password"
202
202
  ```
203
203
 
204
204
  ---
@@ -216,7 +216,7 @@ conductor erase-text 10 # erase 10 characters
216
216
 
217
217
  ### `back`
218
218
 
219
- Press the Android back button. **Android only** — iOS has no back button. On iOS, always run `inspect` first to find the exact label or accessibility ID of the in-app back button, then use `tap` to press it.
219
+ Press the Android back button. **Android only** — iOS has no back button. On iOS, always run `inspect` first to find the exact label or accessibility ID of the in-app back button, then use `tap-on` to press it.
220
220
 
221
221
  ```bash
222
222
  conductor back
@@ -386,14 +386,14 @@ conductor set-orientation landscape
386
386
 
387
387
  ---
388
388
 
389
- ### `screenshot`
389
+ ### `take-screenshot`
390
390
 
391
- Take a screenshot of the current screen. **Prefer `inspect` over `screenshot`** when you need to understand what is on screen — the view hierarchy tells you element text, IDs, and structure without consuming a vision token. Use `screenshot` only for visual evidence, debugging rendering issues, or when a human needs to see the screen.
391
+ Take a screenshot of the current screen. **Prefer `inspect` over `take-screenshot`** when you need to understand what is on screen — the view hierarchy tells you element text, IDs, and structure without consuming a vision token. Use `take-screenshot` only for visual evidence, debugging rendering issues, or when a human needs to see the screen.
392
392
 
393
393
  ```bash
394
- conductor screenshot
395
- conductor screenshot --output /tmp/screen.png
396
- conductor screenshot --output ./screenshots/login.png
394
+ conductor take-screenshot
395
+ conductor take-screenshot --output /tmp/screen.png
396
+ conductor take-screenshot --output ./screenshots/login.png
397
397
  ```
398
398
 
399
399
  Default output: `./screenshot-<timestamp>.png`
@@ -404,9 +404,9 @@ Default output: `./screenshot-<timestamp>.png`
404
404
 
405
405
  ### `inspect`
406
406
 
407
- Print the UI element hierarchy of the current screen. Use this when a `tap` or `assert-visible` fails and you need to discover the exact element text or accessibility ID.
407
+ Print the UI element hierarchy of the current screen. Use this when a `tap-on` or `assert-visible` fails and you need to discover the exact element text or accessibility ID.
408
408
 
409
- The hierarchy shows each element's type, text, and accessibility ID (resourceId / accessibilityIdentifier). Use the `text` value or `id` value as the argument to `tap` / `assert-visible`.
409
+ The hierarchy shows each element's type, text, and accessibility ID (resourceId / accessibilityIdentifier). Use the `text` value or `id` value as the argument to `tap-on` / `assert-visible`.
410
410
 
411
411
  ```bash
412
412
  conductor inspect
@@ -414,6 +414,37 @@ conductor inspect
414
414
 
415
415
  ---
416
416
 
417
+ ### `logs`
418
+
419
+ Stream app console/log output in real time. Auto-detects the best log source based on the platform:
420
+
421
+ - **Web** — captures Playwright browser console events
422
+ - **React Native (iOS/Android)** — connects to the Metro dev server via Chrome DevTools Protocol
423
+ - **Native iOS** — streams from the simulator via `simctl log stream`
424
+ - **Native Android** — streams from the device via `adb logcat`
425
+
426
+ ```bash
427
+ conductor logs # auto-detect source and stream
428
+ conductor logs --source metro # force Metro CDP (for React Native JS logs)
429
+ conductor logs --source device # force platform-native logs (simctl/logcat)
430
+ conductor logs --level warn # only show warnings and errors
431
+ conductor logs --level error # only show errors
432
+ conductor logs --metro-port 8082 # custom Metro dev server port (default: 8081)
433
+ conductor logs --target 1 # connect to a specific Metro debugger target (0-based)
434
+ conductor logs --list # list available Metro debugger targets and exit
435
+ conductor logs --list --json # list targets as JSON (includes appId, logicalDeviceId)
436
+ conductor logs --json # NDJSON output (one JSON object per line)
437
+ ```
438
+
439
+ For iOS/Android, auto-detection tries Metro first (2s timeout), then falls back to device logs. When multiple devices share one Metro instance, use `--list` to see available targets, then `--target <n>` to connect to a specific one. `--list` and `--source metro` don't require a running device session.
440
+
441
+ Exits on Ctrl+C. Each line in `--json` mode is a complete JSON object:
442
+ ```json
443
+ {"timestamp":"...","level":"log","message":"User tapped sign-in","stackTrace":null,"source":"metro"}
444
+ ```
445
+
446
+ ---
447
+
417
448
  ### `run-flow <file>`
418
449
 
419
450
  Execute a Maestro YAML flow file.
@@ -460,12 +491,12 @@ conductor cheat-sheet
460
491
 
461
492
  ---
462
493
 
463
- ### `install`
494
+ ### `install-skills`
464
495
 
465
496
  Copy the conductor skill files into the current project's `.claude/skills/conductor/` directory, so AI agents in that project can use the `cheat-sheet` command locally.
466
497
 
467
498
  ```bash
468
- conductor install --skills
499
+ conductor install-skills
469
500
  ```
470
501
 
471
502
  ---
@@ -541,13 +572,13 @@ Exits with code 0 if all flows pass, 1 if any fail.
541
572
 
542
573
  ## Typical Agent Workflow
543
574
 
544
- **Prefer individual CLI commands over `run-flow` / `run-flow-inline`.** Use `tap`, `type`, `scroll`, `swipe`, `assert-visible`, etc. directly — they are faster, give immediate feedback per step, and make failures easier to diagnose. Only reach for `run-flow` or `run-flow-inline` when you need Maestro-specific YAML features (conditional logic, `runScript`, retryTapIfNoChange`, etc.) that have no CLI equivalent.
575
+ **Prefer individual CLI commands over `run-flow` / `run-flow-inline`.** Use `tap-on`, `input-text`, `scroll`, `swipe`, `assert-visible`, etc. directly — they are faster, give immediate feedback per step, and make failures easier to diagnose. Only reach for `run-flow` or `run-flow-inline` when you need Maestro-specific YAML features (conditional logic, `runScript`, retryTapIfNoChange`, etc.) that have no CLI equivalent.
545
576
 
546
- **Always `launch-app` before interacting.** Never attempt to `tap`, `type`, `scroll`, or navigate before the app is launched and the session is set. `launch-app` both opens the app and saves the `appId`/`deviceId` to the session so all subsequent commands know which device and app to target.
577
+ **Always `launch-app` before interacting.** Never attempt to `tap-on`, `input-text`, `scroll`, or navigate before the app is launched and the session is set. `launch-app` both opens the app and saves the `appId`/`deviceId` to the session so all subsequent commands know which device and app to target.
547
578
 
548
- **To understand what is on screen, run `inspect` first** — it gives you element text, IDs, and structure. Only take a `screenshot` when you need visual evidence or are debugging a rendering issue.
579
+ **To understand what is on screen, run `inspect` first** — it gives you element text, IDs, and structure. Only take a `take-screenshot` when you need visual evidence or are debugging a rendering issue.
549
580
 
550
- **Try obvious text labels and IDs first. Only run `inspect` when a `tap` or `assert-visible` fails and you need to discover the exact identifier.**
581
+ **Try obvious text labels and IDs first. Only run `inspect` when a `tap-on` or `assert-visible` fails and you need to discover the exact identifier.**
551
582
 
552
583
  Single-agent (default session, no flag needed):
553
584
  ```bash
@@ -558,17 +589,17 @@ conductor list-devices
558
589
  conductor launch-app com.example.myapp --device emulator-5554
559
590
 
560
591
  # 3. Interact using the most likely text labels or IDs
561
- conductor tap "Sign In" # try the obvious label first
562
- conductor tap "username_field" # or a guessed test ID
563
- conductor type "user@example.com"
564
- conductor tap "password_field"
565
- conductor type "secret123"
566
- conductor tap "Login"
592
+ conductor tap-on "Sign In" # try the obvious label first
593
+ conductor tap-on "username_field" # or a guessed test ID
594
+ conductor input-text "user@example.com"
595
+ conductor tap-on "password_field"
596
+ conductor input-text "secret123"
597
+ conductor tap-on "Login"
567
598
 
568
599
  # → If a tap fails, run inspect to find the real identifier:
569
600
  conductor inspect
570
601
  # → hierarchy shows text="Log in", id="btn_login" — retry with correct value
571
- conductor tap "Log in"
602
+ conductor tap-on "Log in"
572
603
 
573
604
  # 4. Assert — try expected text directly
574
605
  conductor assert-visible "Dashboard"
@@ -577,7 +608,7 @@ conductor assert-visible "Dashboard"
577
608
  conductor inspect
578
609
 
579
610
  # 6. Screenshot only for visual evidence or rendering checks
580
- conductor screenshot --output /tmp/tester-ios-iphone-16/post-login.png
611
+ conductor take-screenshot --output /tmp/tester-ios-iphone-16/post-login.png
581
612
 
582
613
  # 7. Run a full flow only when CLI commands aren't sufficient
583
614
  # Write the YAML to /tmp/<agent-name>/ first — never pass YAML inline, it doesn't work
@@ -674,13 +705,13 @@ conductor launch-app com.example.myapp --device emulator-5554
674
705
  **Every subsequent command carries `--device`:**
675
706
  ```bash
676
707
  # Agent tester-ios-iphone-16
677
- conductor tap "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
678
- conductor type "user@a.com" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
708
+ conductor tap-on "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
709
+ conductor input-text "user@a.com" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
679
710
  conductor inspect --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
680
711
 
681
712
  # Agent tester-android-pixel-6 (runs fully in parallel)
682
- conductor tap "Sign In" --device emulator-5554
683
- conductor type "user@b.com" --device emulator-5554
713
+ conductor tap-on "Sign In" --device emulator-5554
714
+ conductor input-text "user@b.com" --device emulator-5554
684
715
  conductor inspect --device emulator-5554
685
716
  ```
686
717
 
@@ -3,6 +3,6 @@ skills:
3
3
  path: conductor/SKILL.md
4
4
  description: "Token-efficient CLI for mobile UI testing, designed for AI agents"
5
5
  category: service
6
- version: 0.2.0
6
+ version: 0.6.0
7
7
  requires:
8
8
  bins: [conductor]