@houwert/conductor 0.4.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 (53) hide show
  1. package/README.md +13 -10
  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 +12 -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-app.js +51 -0
  15. package/dist/commands/install.js +103 -27
  16. package/dist/commands/launch-app.js +6 -0
  17. package/dist/commands/list-devices.js +39 -2
  18. package/dist/commands/logs.js +193 -0
  19. package/dist/commands/press-key.js +16 -0
  20. package/dist/commands/screenshot.js +1 -1
  21. package/dist/commands/scroll-until-visible.js +11 -0
  22. package/dist/commands/scroll.js +6 -0
  23. package/dist/commands/start-device.js +38 -4
  24. package/dist/commands/stop-app.js +4 -0
  25. package/dist/commands/swipe.js +22 -0
  26. package/dist/commands/tap.js +10 -3
  27. package/dist/commands/type.js +2 -2
  28. package/dist/commands/uninstall-app.js +4 -0
  29. package/dist/daemon/client.js +72 -9
  30. package/dist/daemon/log-collector.js +408 -0
  31. package/dist/daemon/server.js +110 -32
  32. package/dist/daemon/web-server.js +812 -0
  33. package/dist/device-picker.js +7 -2
  34. package/dist/drivers/bootstrap.js +124 -1
  35. package/dist/drivers/element-resolver.js +241 -30
  36. package/dist/drivers/flow-runner.js +63 -21
  37. package/dist/drivers/log-sources/android.js +156 -0
  38. package/dist/drivers/log-sources/daemon.js +112 -0
  39. package/dist/drivers/log-sources/ios.js +106 -0
  40. package/dist/drivers/log-sources/metro.js +252 -0
  41. package/dist/drivers/log-sources/types.js +13 -0
  42. package/dist/drivers/log-sources/web.js +96 -0
  43. package/dist/drivers/wait.js +57 -0
  44. package/dist/drivers/web.js +173 -0
  45. package/dist/index.js +69 -12
  46. package/dist/runner.js +32 -2
  47. package/drivers/ios/conductor-driver-ios.zip +0 -0
  48. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  50. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  51. package/package.json +5 -2
  52. package/skills/conductor/SKILL.md +83 -41
  53. package/skills/skills.yaml +1 -1
package/dist/index.js CHANGED
@@ -32,6 +32,8 @@ 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");
36
+ const install_app_js_1 = require("./commands/install-app.js");
35
37
  const erase_text_js_1 = require("./commands/erase-text.js");
36
38
  const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
37
39
  const open_link_js_1 = require("./commands/open-link.js");
@@ -40,20 +42,25 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
40
42
  const set_location_js_1 = require("./commands/set-location.js");
41
43
  const set_orientation_js_1 = require("./commands/set-orientation.js");
42
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");
43
47
  const device_picker_js_1 = require("./device-picker.js");
44
48
  const update_check_js_1 = require("./update-check.js");
45
49
  const COMMAND_HELP = {
46
50
  'start-device': start_device_js_1.HELP,
51
+ 'delete-device': delete_device_js_1.HELP,
47
52
  'list-devices': list_devices_js_1.HELP,
48
53
  'foreground-app': foreground_app_js_1.HELP,
49
54
  'list-apps': list_apps_js_1.HELP,
50
55
  'copy-app': copy_app_js_1.HELP,
56
+ 'download-app': download_app_js_1.HELP,
57
+ 'install-app': install_app_js_1.HELP,
51
58
  'launch-app': launch_app_js_1.HELP,
52
59
  'stop-app': stop_app_js_1.HELP,
53
60
  'clear-state': clear_state_js_1.HELP,
54
61
  'uninstall-app': uninstall_app_js_1.HELP,
55
- tap: tap_js_1.HELP,
56
- type: type_js_1.HELP,
62
+ 'tap-on': tap_js_1.HELP,
63
+ 'input-text': type_js_1.HELP,
57
64
  'erase-text': erase_text_js_1.HELP,
58
65
  back: back_js_1.HELP,
59
66
  'press-key': press_key_js_1.HELP,
@@ -66,23 +73,27 @@ const COMMAND_HELP = {
66
73
  'open-link': open_link_js_1.HELP,
67
74
  'set-location': set_location_js_1.HELP,
68
75
  'set-orientation': set_orientation_js_1.HELP,
69
- screenshot: screenshot_js_1.HELP,
76
+ 'take-screenshot': screenshot_js_1.HELP,
70
77
  inspect: inspect_js_1.HELP,
71
78
  focused: focused_js_1.HELP,
72
79
  'run-flow': run_flow_js_1.HELP,
73
80
  'run-flow-inline': run_flow_inline_js_1.HELP,
74
81
  session: session_js_1.HELP,
75
82
  'cheat-sheet': cheat_sheet_js_1.HELP,
76
- 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,
77
86
  'daemon-start': daemon_js_1.HELP_DAEMON_START,
78
87
  'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
79
88
  'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
80
89
  'device-pool': device_pool_js_1.HELP,
81
90
  'run-parallel': run_parallel_js_1.HELP,
91
+ logs: logs_js_1.HELP,
82
92
  };
83
93
  const OPTIONS_HELP = `Options:
84
94
  --device <id> Target device ID (also keys the session and daemon)
85
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)
86
97
  --json Output as machine-readable JSON
87
98
  --verbose, -v Log daemon calls, fallbacks, and raw output
88
99
  --help, -h Show this help`;
@@ -101,7 +112,6 @@ async function main() {
101
112
  'clear',
102
113
  'list',
103
114
  'verbose',
104
- 'skills',
105
115
  'check',
106
116
  'all',
107
117
  'acquire',
@@ -138,8 +148,12 @@ async function main() {
138
148
  'name',
139
149
  'device-name',
140
150
  'device-type',
151
+ 'browser',
141
152
  'from',
142
153
  'to',
154
+ 'source',
155
+ 'metro-port',
156
+ 'level',
143
157
  ],
144
158
  alias: { h: 'help', v: 'verbose' },
145
159
  });
@@ -162,11 +176,18 @@ async function main() {
162
176
  const NO_DEVICE_COMMANDS = new Set([
163
177
  'list-devices',
164
178
  'start-device',
179
+ 'delete-device',
165
180
  'cheat-sheet',
166
- 'install',
181
+ 'install-plugin',
182
+ 'install-skills',
183
+ 'install-web',
167
184
  'copy-app',
168
185
  'device-pool',
169
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'] : []),
170
191
  ]);
171
192
  if (!NO_DEVICE_COMMANDS.has(command) && !COMMAND_HELP[command]) {
172
193
  console.error(`Unknown command: ${command}`);
@@ -194,7 +215,8 @@ async function main() {
194
215
  sessionName = match.id;
195
216
  }
196
217
  else {
197
- sessionName = explicitDevice ?? (await (0, device_picker_js_1.pickDevice)()) ?? 'default';
218
+ sessionName =
219
+ explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
198
220
  }
199
221
  }
200
222
  let exitCode = 0;
@@ -205,11 +227,18 @@ async function main() {
205
227
  avd: argv['avd'],
206
228
  name: argv['name'],
207
229
  deviceType: argv['device-type'],
230
+ browser: argv['browser'],
208
231
  });
209
232
  break;
210
233
  case 'list-devices':
211
234
  exitCode = await (0, list_devices_js_1.listDevices)(opts);
212
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;
213
242
  case 'foreground-app':
214
243
  exitCode = await (0, foreground_app_js_1.foregroundApp)(opts, sessionName);
215
244
  break;
@@ -223,6 +252,16 @@ async function main() {
223
252
  exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
224
253
  break;
225
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
+ }
260
+ case 'install-app': {
261
+ const appPath = rest[0] ?? '';
262
+ exitCode = await (0, install_app_js_1.installApp)(appPath, opts, sessionName);
263
+ break;
264
+ }
226
265
  case 'launch-app': {
227
266
  const appId = rest[0] ?? '';
228
267
  // --argument key=value (repeatable) → Record<string, string>
@@ -254,7 +293,7 @@ async function main() {
254
293
  exitCode = await (0, uninstall_app_js_1.uninstallApp)(appId, opts, sessionName);
255
294
  break;
256
295
  }
257
- case 'tap': {
296
+ case 'tap-on': {
258
297
  const element = rest.join(' ');
259
298
  exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
260
299
  id: argv['id'],
@@ -274,7 +313,7 @@ async function main() {
274
313
  });
275
314
  break;
276
315
  }
277
- case 'type': {
316
+ case 'input-text': {
278
317
  const text = rest.join(' ');
279
318
  exitCode = await (0, type_js_1.typeText)(text, opts, sessionName);
280
319
  break;
@@ -390,7 +429,7 @@ async function main() {
390
429
  exitCode = await (0, set_orientation_js_1.setOrientation)(orientation, opts, sessionName);
391
430
  break;
392
431
  }
393
- case 'screenshot': {
432
+ case 'take-screenshot': {
394
433
  const outPath = argv['output'];
395
434
  exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
396
435
  break;
@@ -404,6 +443,18 @@ async function main() {
404
443
  interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
405
444
  });
406
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;
407
458
  case 'run-flow': {
408
459
  const file = rest[0] ?? '';
409
460
  const rawEnv = argv['env'];
@@ -423,8 +474,14 @@ async function main() {
423
474
  case 'cheat-sheet':
424
475
  exitCode = await (0, cheat_sheet_js_1.cheatSheet)();
425
476
  break;
426
- case 'install':
427
- 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]);
428
485
  break;
429
486
  case 'daemon-start':
430
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.4.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
@@ -90,6 +90,17 @@ Both flags are required. The command reads the `.app` bundle path from the sourc
90
90
 
91
91
  ---
92
92
 
93
+ ### `install-app <path>`
94
+
95
+ Install an app from a local `.app` bundle, `.ipa`, or `.apk` file onto the device.
96
+
97
+ ```bash
98
+ conductor install-app ./build/MyApp.app
99
+ conductor install-app ./build/app-debug.apk --device emulator-5554
100
+ ```
101
+
102
+ ---
103
+
93
104
  ### `launch-app <appId>`
94
105
 
95
106
  Launch an app by bundle ID and save it to the session.
@@ -146,21 +157,21 @@ conductor uninstall-app com.example.myapp --device emulator-5554
146
157
 
147
158
  ---
148
159
 
149
- ### `tap <element>`
160
+ ### `tap-on <element>`
150
161
 
151
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.
152
163
 
153
164
  ```bash
154
- conductor tap "Sign In"
155
- conductor tap --id "btn_login" # match by accessibility ID instead of text
156
- conductor tap --text "Edit" # match by text only (not id)
157
- conductor tap "Next" --index 1 # pick the 2nd match (0-based)
158
- conductor tap "Add to cart" --long-press
159
- conductor tap "Like" --double-tap
160
- conductor tap "Delete" --optional # do not fail if not found
161
- conductor tap "Edit" --below "Username" # tap "Edit" that is below the "Username" element
162
- conductor tap "Submit" --above "Footer"
163
- 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"
164
175
  ```
165
176
 
166
177
  Flags:
@@ -181,13 +192,13 @@ Flags:
181
192
 
182
193
  ---
183
194
 
184
- ### `type <text>`
195
+ ### `input-text <text>`
185
196
 
186
197
  Type text into the currently focused input field.
187
198
 
188
199
  ```bash
189
- conductor type "hello@example.com"
190
- conductor type "my password"
200
+ conductor input-text "hello@example.com"
201
+ conductor input-text "my password"
191
202
  ```
192
203
 
193
204
  ---
@@ -205,7 +216,7 @@ conductor erase-text 10 # erase 10 characters
205
216
 
206
217
  ### `back`
207
218
 
208
- 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.
209
220
 
210
221
  ```bash
211
222
  conductor back
@@ -375,14 +386,14 @@ conductor set-orientation landscape
375
386
 
376
387
  ---
377
388
 
378
- ### `screenshot`
389
+ ### `take-screenshot`
379
390
 
380
- 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.
381
392
 
382
393
  ```bash
383
- conductor screenshot
384
- conductor screenshot --output /tmp/screen.png
385
- 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
386
397
  ```
387
398
 
388
399
  Default output: `./screenshot-<timestamp>.png`
@@ -393,9 +404,9 @@ Default output: `./screenshot-<timestamp>.png`
393
404
 
394
405
  ### `inspect`
395
406
 
396
- 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.
397
408
 
398
- 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`.
399
410
 
400
411
  ```bash
401
412
  conductor inspect
@@ -403,6 +414,37 @@ conductor inspect
403
414
 
404
415
  ---
405
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
+
406
448
  ### `run-flow <file>`
407
449
 
408
450
  Execute a Maestro YAML flow file.
@@ -449,12 +491,12 @@ conductor cheat-sheet
449
491
 
450
492
  ---
451
493
 
452
- ### `install`
494
+ ### `install-skills`
453
495
 
454
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.
455
497
 
456
498
  ```bash
457
- conductor install --skills
499
+ conductor install-skills
458
500
  ```
459
501
 
460
502
  ---
@@ -530,13 +572,13 @@ Exits with code 0 if all flows pass, 1 if any fail.
530
572
 
531
573
  ## Typical Agent Workflow
532
574
 
533
- **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.
534
576
 
535
- **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.
536
578
 
537
- **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.
538
580
 
539
- **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.**
540
582
 
541
583
  Single-agent (default session, no flag needed):
542
584
  ```bash
@@ -547,17 +589,17 @@ conductor list-devices
547
589
  conductor launch-app com.example.myapp --device emulator-5554
548
590
 
549
591
  # 3. Interact using the most likely text labels or IDs
550
- conductor tap "Sign In" # try the obvious label first
551
- conductor tap "username_field" # or a guessed test ID
552
- conductor type "user@example.com"
553
- conductor tap "password_field"
554
- conductor type "secret123"
555
- 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"
556
598
 
557
599
  # → If a tap fails, run inspect to find the real identifier:
558
600
  conductor inspect
559
601
  # → hierarchy shows text="Log in", id="btn_login" — retry with correct value
560
- conductor tap "Log in"
602
+ conductor tap-on "Log in"
561
603
 
562
604
  # 4. Assert — try expected text directly
563
605
  conductor assert-visible "Dashboard"
@@ -566,7 +608,7 @@ conductor assert-visible "Dashboard"
566
608
  conductor inspect
567
609
 
568
610
  # 6. Screenshot only for visual evidence or rendering checks
569
- conductor screenshot --output /tmp/tester-ios-iphone-16/post-login.png
611
+ conductor take-screenshot --output /tmp/tester-ios-iphone-16/post-login.png
570
612
 
571
613
  # 7. Run a full flow only when CLI commands aren't sufficient
572
614
  # Write the YAML to /tmp/<agent-name>/ first — never pass YAML inline, it doesn't work
@@ -663,13 +705,13 @@ conductor launch-app com.example.myapp --device emulator-5554
663
705
  **Every subsequent command carries `--device`:**
664
706
  ```bash
665
707
  # Agent tester-ios-iphone-16
666
- conductor tap "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
667
- 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
668
710
  conductor inspect --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
669
711
 
670
712
  # Agent tester-android-pixel-6 (runs fully in parallel)
671
- conductor tap "Sign In" --device emulator-5554
672
- 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
673
715
  conductor inspect --device emulator-5554
674
716
  ```
675
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]