react-native-ai-debugger 1.1.6 → 1.4.21

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 (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +371 -42
  3. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  4. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  5. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  6. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.js +12 -6
  8. package/build/__tests__/integration/connection-health.test.js.map +1 -1
  9. package/build/__tests__/integration/execute-in-app.test.js +2 -1
  10. package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
  11. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  12. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/tap-strategies.test.js +329 -0
  14. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.js +9 -10
  16. package/build/__tests__/integration/tools.test.js.map +1 -1
  17. package/build/__tests__/unit/burst.test.d.ts +2 -0
  18. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  19. package/build/__tests__/unit/burst.test.js +50 -0
  20. package/build/__tests__/unit/burst.test.js.map +1 -0
  21. package/build/__tests__/unit/connection-health.test.js +26 -16
  22. package/build/__tests__/unit/connection-health.test.js.map +1 -1
  23. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  24. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/fingerprint.test.js +89 -0
  26. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  27. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  28. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  30. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  31. package/build/__tests__/unit/logs.test.js +9 -0
  32. package/build/__tests__/unit/logs.test.js.map +1 -1
  33. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  34. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  35. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  36. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  37. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  38. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  39. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  40. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  41. package/build/__tests__/unit/tap.test.js +430 -25
  42. package/build/__tests__/unit/tap.test.js.map +1 -1
  43. package/build/core/android.d.ts.map +1 -1
  44. package/build/core/android.js +64 -29
  45. package/build/core/android.js.map +1 -1
  46. package/build/core/appDetection.d.ts +8 -0
  47. package/build/core/appDetection.d.ts.map +1 -0
  48. package/build/core/appDetection.js +91 -0
  49. package/build/core/appDetection.js.map +1 -0
  50. package/build/core/bundle.d.ts.map +1 -1
  51. package/build/core/bundle.js +15 -23
  52. package/build/core/bundle.js.map +1 -1
  53. package/build/core/config.d.ts +8 -0
  54. package/build/core/config.d.ts.map +1 -0
  55. package/build/core/config.js +27 -0
  56. package/build/core/config.js.map +1 -0
  57. package/build/core/connection.d.ts +47 -3
  58. package/build/core/connection.d.ts.map +1 -1
  59. package/build/core/connection.js +519 -124
  60. package/build/core/connection.js.map +1 -1
  61. package/build/core/executor.d.ts +57 -14
  62. package/build/core/executor.d.ts.map +1 -1
  63. package/build/core/executor.js +936 -208
  64. package/build/core/executor.js.map +1 -1
  65. package/build/core/fingerprint.d.ts +4 -0
  66. package/build/core/fingerprint.d.ts.map +1 -0
  67. package/build/core/fingerprint.js +47 -0
  68. package/build/core/fingerprint.js.map +1 -0
  69. package/build/core/guides.d.ts.map +1 -1
  70. package/build/core/guides.js +27 -3
  71. package/build/core/guides.js.map +1 -1
  72. package/build/core/httpServer.js.map +1 -1
  73. package/build/core/imageBuffer.d.ts +37 -0
  74. package/build/core/imageBuffer.d.ts.map +1 -0
  75. package/build/core/imageBuffer.js +58 -0
  76. package/build/core/imageBuffer.js.map +1 -0
  77. package/build/core/index.d.ts +21 -17
  78. package/build/core/index.d.ts.map +1 -1
  79. package/build/core/index.js +21 -19
  80. package/build/core/index.js.map +1 -1
  81. package/build/core/ios.d.ts +36 -7
  82. package/build/core/ios.d.ts.map +1 -1
  83. package/build/core/ios.js +553 -323
  84. package/build/core/ios.js.map +1 -1
  85. package/build/core/license.d.ts +38 -0
  86. package/build/core/license.d.ts.map +1 -0
  87. package/build/core/license.js +265 -0
  88. package/build/core/license.js.map +1 -0
  89. package/build/core/logbox.d.ts +51 -0
  90. package/build/core/logbox.d.ts.map +1 -0
  91. package/build/core/logbox.js +258 -0
  92. package/build/core/logbox.js.map +1 -0
  93. package/build/core/logs.d.ts +1 -0
  94. package/build/core/logs.d.ts.map +1 -1
  95. package/build/core/logs.js +5 -0
  96. package/build/core/logs.js.map +1 -1
  97. package/build/core/metro.d.ts +8 -0
  98. package/build/core/metro.d.ts.map +1 -1
  99. package/build/core/metro.js +28 -0
  100. package/build/core/metro.js.map +1 -1
  101. package/build/core/networkInterceptor.d.ts +29 -2
  102. package/build/core/networkInterceptor.d.ts.map +1 -1
  103. package/build/core/networkInterceptor.js +195 -191
  104. package/build/core/networkInterceptor.js.map +1 -1
  105. package/build/core/ocr.d.ts +8 -3
  106. package/build/core/ocr.d.ts.map +1 -1
  107. package/build/core/ocr.js +116 -15
  108. package/build/core/ocr.js.map +1 -1
  109. package/build/core/posthog.d.ts +5 -0
  110. package/build/core/posthog.d.ts.map +1 -0
  111. package/build/core/posthog.js +46 -0
  112. package/build/core/posthog.js.map +1 -0
  113. package/build/core/sdkBridge.d.ts +67 -0
  114. package/build/core/sdkBridge.d.ts.map +1 -0
  115. package/build/core/sdkBridge.js +148 -0
  116. package/build/core/sdkBridge.js.map +1 -0
  117. package/build/core/state.d.ts +25 -5
  118. package/build/core/state.d.ts.map +1 -1
  119. package/build/core/state.js +79 -10
  120. package/build/core/state.js.map +1 -1
  121. package/build/core/telemetry.d.ts +19 -1
  122. package/build/core/telemetry.d.ts.map +1 -1
  123. package/build/core/telemetry.js +163 -8
  124. package/build/core/telemetry.js.map +1 -1
  125. package/build/core/types.d.ts +11 -0
  126. package/build/core/types.d.ts.map +1 -1
  127. package/build/index.js +1302 -302
  128. package/build/index.js.map +1 -1
  129. package/build/pro/screenshot-diff.d.ts +10 -0
  130. package/build/pro/screenshot-diff.d.ts.map +1 -0
  131. package/build/pro/screenshot-diff.js +38 -0
  132. package/build/pro/screenshot-diff.js.map +1 -0
  133. package/build/pro/tap.d.ts +71 -2
  134. package/build/pro/tap.d.ts.map +1 -1
  135. package/build/pro/tap.js +797 -124
  136. package/build/pro/tap.js.map +1 -1
  137. package/build/tools/accountTools.d.ts +44 -0
  138. package/build/tools/accountTools.d.ts.map +1 -0
  139. package/build/tools/accountTools.js +217 -0
  140. package/build/tools/accountTools.js.map +1 -0
  141. package/package.json +9 -7
  142. package/build/core/httpServer.js +0 -2459
  143. package/build/core/httpServerProcess.js +0 -153
  144. package/build/httpServerStandalone.js +0 -31
package/build/core/ios.js CHANGED
@@ -11,8 +11,16 @@ const execFileAsync = promisify(execFile);
11
11
  const SIMCTL_TIMEOUT = 30000;
12
12
  // IDB command timeout in milliseconds
13
13
  const IDB_TIMEOUT = 30000;
14
+ // AXe command timeout in milliseconds
15
+ const AXE_TIMEOUT = 30000;
14
16
  // Valid button types for IDB ui button command
15
- export const IOS_BUTTON_TYPES = ["HOME", "LOCK", "SIDE_BUTTON", "SIRI", "APPLE_PAY"];
17
+ export const IOS_BUTTON_TYPES = [
18
+ "HOME",
19
+ "LOCK",
20
+ "SIDE_BUTTON",
21
+ "SIRI",
22
+ "APPLE_PAY",
23
+ ];
16
24
  // Track connected IDB simulators to avoid redundant connect calls
17
25
  const connectedIdbSimulators = new Set();
18
26
  /**
@@ -22,6 +30,36 @@ const connectedIdbSimulators = new Set();
22
30
  function getIdbPath() {
23
31
  return process.env.IDB_PATH || "idb";
24
32
  }
33
+ /**
34
+ * Get the active iOS UI driver.
35
+ * Set IOS_DRIVER=axe in MCP server env to use AXe CLI instead of IDB.
36
+ */
37
+ function getIosDriver() {
38
+ const driver = process.env.IOS_DRIVER?.toLowerCase();
39
+ if (driver === "axe")
40
+ return "axe";
41
+ return "idb";
42
+ }
43
+ /**
44
+ * Get the AXe executable path.
45
+ * Supports AXE_PATH environment variable for custom installations.
46
+ */
47
+ function getAxePath() {
48
+ return process.env.AXE_PATH || "axe";
49
+ }
50
+ /**
51
+ * Run AXe command with execFile (no shell) for proper argument handling.
52
+ */
53
+ async function runAxe(...args) {
54
+ const axePath = getAxePath();
55
+ const { stdout, stderr } = await execFileAsync(axePath, args, {
56
+ timeout: AXE_TIMEOUT,
57
+ });
58
+ return {
59
+ stdout: stdout.trim(),
60
+ stderr: stderr.trim(),
61
+ };
62
+ }
25
63
  /**
26
64
  * Run IDB command with execFile (no shell) for proper argument handling
27
65
  * This matches the original ios-simulator-mcp implementation
@@ -29,11 +67,11 @@ function getIdbPath() {
29
67
  async function runIdb(...args) {
30
68
  const idbPath = getIdbPath();
31
69
  const { stdout, stderr } = await execFileAsync(idbPath, args, {
32
- timeout: IDB_TIMEOUT
70
+ timeout: IDB_TIMEOUT,
33
71
  });
34
72
  return {
35
73
  stdout: stdout.trim(),
36
- stderr: stderr.trim()
74
+ stderr: stderr.trim(),
37
75
  };
38
76
  }
39
77
  /**
@@ -48,6 +86,49 @@ export async function isIdbAvailable() {
48
86
  return false;
49
87
  }
50
88
  }
89
+ /**
90
+ * Check if AXe CLI is available
91
+ */
92
+ export async function isAxeAvailable() {
93
+ try {
94
+ await runAxe("--version");
95
+ return true;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ /**
102
+ * Check if the active iOS UI driver is available.
103
+ * Used by tap.ts for early fail-fast checks.
104
+ */
105
+ export async function isUiDriverAvailable() {
106
+ const driver = getIosDriver();
107
+ return driver === "axe" ? isAxeAvailable() : isIdbAvailable();
108
+ }
109
+ /**
110
+ * Get install instructions for the active iOS UI driver.
111
+ */
112
+ export function getUiDriverInstallHint() {
113
+ const driver = getIosDriver();
114
+ if (driver === "axe") {
115
+ return "AXe is not installed. Install with: brew install cameroncooke/axe/axe";
116
+ }
117
+ return "IDB is not installed. Install with: brew install idb-companion";
118
+ }
119
+ /**
120
+ * Map IDB button names (uppercase/underscores) to AXe button names (lowercase/hyphens).
121
+ */
122
+ function toAxeButtonName(idbButton) {
123
+ const map = {
124
+ HOME: "home",
125
+ LOCK: "lock",
126
+ SIDE_BUTTON: "side-button",
127
+ SIRI: "siri",
128
+ APPLE_PAY: "apple-pay",
129
+ };
130
+ return map[idbButton];
131
+ }
51
132
  /**
52
133
  * Ensure IDB is connected to the specified simulator
53
134
  * IDB requires `idb connect <UDID>` before any UI commands work
@@ -65,15 +146,61 @@ async function ensureIdbConnected(udid) {
65
146
  catch (error) {
66
147
  const errorMessage = error instanceof Error ? error.message : String(error);
67
148
  // "Already connected" is not an error
68
- if (errorMessage.includes("already connected") || errorMessage.includes("Connected")) {
149
+ if (errorMessage.includes("already connected") ||
150
+ errorMessage.includes("Connected")) {
69
151
  connectedIdbSimulators.add(udid);
70
152
  return { success: true };
71
153
  }
72
154
  return {
73
155
  success: false,
74
- error: `Failed to connect IDB to simulator: ${errorMessage}`
156
+ error: `Failed to connect IDB to simulator: ${errorMessage}`,
157
+ };
158
+ }
159
+ }
160
+ /**
161
+ * Shared pre-flight check for all UI driver functions:
162
+ * 1. Check driver availability
163
+ * 2. Resolve target simulator UDID
164
+ * 3. Ensure IDB connection (IDB only)
165
+ */
166
+ async function ensureUiDriverReady(udid) {
167
+ const driver = getIosDriver();
168
+ if (driver === "axe") {
169
+ if (!(await isAxeAvailable())) {
170
+ return {
171
+ ready: false,
172
+ error: { success: false, error: getUiDriverInstallHint() },
173
+ };
174
+ }
175
+ }
176
+ else {
177
+ if (!(await isIdbAvailable())) {
178
+ return {
179
+ ready: false,
180
+ error: { success: false, error: getUiDriverInstallHint() },
181
+ };
182
+ }
183
+ }
184
+ const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
185
+ if (!targetUdid) {
186
+ return {
187
+ ready: false,
188
+ error: {
189
+ success: false,
190
+ error: "No iOS simulator is currently running. Start a simulator first.",
191
+ },
75
192
  };
76
193
  }
194
+ if (driver === "idb") {
195
+ const connectResult = await ensureIdbConnected(targetUdid);
196
+ if (!connectResult.success) {
197
+ return {
198
+ ready: false,
199
+ error: { success: false, error: connectResult.error },
200
+ };
201
+ }
202
+ }
203
+ return { ready: true, driver, targetUdid };
77
204
  }
78
205
  /**
79
206
  * Check if simctl is available
@@ -96,11 +223,11 @@ export async function listIOSSimulators(onlyBooted = false) {
96
223
  if (!simctlAvailable) {
97
224
  return {
98
225
  success: false,
99
- error: "Xcode command line tools not available. Install Xcode from the App Store."
226
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
100
227
  };
101
228
  }
102
229
  const { stdout } = await execAsync("xcrun simctl list devices -j", {
103
- timeout: SIMCTL_TIMEOUT
230
+ timeout: SIMCTL_TIMEOUT,
104
231
  });
105
232
  const data = JSON.parse(stdout);
106
233
  const simulators = [];
@@ -115,14 +242,16 @@ export async function listIOSSimulators(onlyBooted = false) {
115
242
  continue;
116
243
  // Extract iOS version from runtime string
117
244
  const runtimeMatch = runtime.match(/iOS[- ](\d+[.-]\d+)/i);
118
- const runtimeVersion = runtimeMatch ? `iOS ${runtimeMatch[1].replace("-", ".")}` : runtime;
245
+ const runtimeVersion = runtimeMatch
246
+ ? `iOS ${runtimeMatch[1].replace("-", ".")}`
247
+ : runtime;
119
248
  simulators.push({
120
249
  udid: device.udid,
121
250
  name: device.name,
122
251
  state: device.state,
123
252
  runtime: runtimeVersion,
124
253
  deviceType: device.deviceTypeIdentifier,
125
- isAvailable: device.isAvailable
254
+ isAvailable: device.isAvailable,
126
255
  });
127
256
  }
128
257
  }
@@ -132,7 +261,7 @@ export async function listIOSSimulators(onlyBooted = false) {
132
261
  result: onlyBooted
133
262
  ? "No booted iOS simulators. Start a simulator first."
134
263
  : "No available iOS simulators found.",
135
- simulators: []
264
+ simulators: [],
136
265
  };
137
266
  }
138
267
  // Sort: Booted first, then by name
@@ -152,13 +281,13 @@ export async function listIOSSimulators(onlyBooted = false) {
152
281
  return {
153
282
  success: true,
154
283
  result: `iOS Simulators:\n\n${formatted}`,
155
- simulators
284
+ simulators,
156
285
  };
157
286
  }
158
287
  catch (error) {
159
288
  return {
160
289
  success: false,
161
- error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`
290
+ error: `Failed to list simulators: ${error instanceof Error ? error.message : String(error)}`,
162
291
  };
163
292
  }
164
293
  }
@@ -168,7 +297,7 @@ export async function listIOSSimulators(onlyBooted = false) {
168
297
  export async function getBootedSimulatorUdid() {
169
298
  try {
170
299
  const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
171
- timeout: SIMCTL_TIMEOUT
300
+ timeout: SIMCTL_TIMEOUT,
172
301
  });
173
302
  const data = JSON.parse(stdout);
174
303
  for (const devices of Object.values(data.devices)) {
@@ -193,7 +322,7 @@ export async function getBootedSimulatorUdid() {
193
322
  export async function findSimulatorByName(deviceName) {
194
323
  try {
195
324
  const { stdout } = await execAsync("xcrun simctl list devices booted -j", {
196
- timeout: SIMCTL_TIMEOUT
325
+ timeout: SIMCTL_TIMEOUT,
197
326
  });
198
327
  const data = JSON.parse(stdout);
199
328
  const normalizedDeviceName = deviceName.toLowerCase().trim();
@@ -249,7 +378,7 @@ export async function iosScreenshot(outputPath, udid) {
249
378
  if (!simctlAvailable) {
250
379
  return {
251
380
  success: false,
252
- error: "Xcode command line tools not available. Install Xcode from the App Store."
381
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
253
382
  };
254
383
  }
255
384
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -257,14 +386,14 @@ export async function iosScreenshot(outputPath, udid) {
257
386
  if (!targetUdid) {
258
387
  return {
259
388
  success: false,
260
- error: "No iOS simulator is currently running. Start a simulator first."
389
+ error: "No iOS simulator is currently running. Start a simulator first.",
261
390
  };
262
391
  }
263
392
  // Generate output path if not provided
264
393
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
265
394
  const finalOutputPath = outputPath || path.join(os.tmpdir(), `ios-screenshot-${timestamp}.png`);
266
395
  await execAsync(`xcrun simctl io ${targetUdid} screenshot "${finalOutputPath}"`, {
267
- timeout: SIMCTL_TIMEOUT
396
+ timeout: SIMCTL_TIMEOUT,
268
397
  });
269
398
  // Resize image if needed (API limit: 2000px max for multi-image requests)
270
399
  // Return scale factor so AI can convert image coords to device coords
@@ -281,15 +410,13 @@ export async function iosScreenshot(outputPath, udid) {
281
410
  imageData = await image
282
411
  .resize(MAX_DIMENSION, MAX_DIMENSION, {
283
412
  fit: "inside",
284
- withoutEnlargement: true
413
+ withoutEnlargement: true,
285
414
  })
286
415
  .jpeg({ quality: 85 })
287
416
  .toBuffer();
288
417
  }
289
418
  else {
290
- imageData = await image
291
- .jpeg({ quality: 85 })
292
- .toBuffer();
419
+ imageData = await image.jpeg({ quality: 85 }).toBuffer();
293
420
  }
294
421
  return {
295
422
  success: true,
@@ -297,13 +424,13 @@ export async function iosScreenshot(outputPath, udid) {
297
424
  data: imageData,
298
425
  scaleFactor,
299
426
  originalWidth,
300
- originalHeight
427
+ originalHeight,
301
428
  };
302
429
  }
303
430
  catch (error) {
304
431
  return {
305
432
  success: false,
306
- error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`
433
+ error: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
307
434
  };
308
435
  }
309
436
  }
@@ -316,14 +443,14 @@ export async function iosInstallApp(appPath, udid) {
316
443
  if (!simctlAvailable) {
317
444
  return {
318
445
  success: false,
319
- error: "Xcode command line tools not available. Install Xcode from the App Store."
446
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
320
447
  };
321
448
  }
322
449
  // Verify app exists
323
450
  if (!existsSync(appPath)) {
324
451
  return {
325
452
  success: false,
326
- error: `App bundle not found: ${appPath}`
453
+ error: `App bundle not found: ${appPath}`,
327
454
  };
328
455
  }
329
456
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -331,21 +458,21 @@ export async function iosInstallApp(appPath, udid) {
331
458
  if (!targetUdid) {
332
459
  return {
333
460
  success: false,
334
- error: "No iOS simulator is currently running. Start a simulator first."
461
+ error: "No iOS simulator is currently running. Start a simulator first.",
335
462
  };
336
463
  }
337
464
  await execAsync(`xcrun simctl install ${targetUdid} "${appPath}"`, {
338
- timeout: 120000 // 2 minute timeout for install
465
+ timeout: 120000, // 2 minute timeout for install
339
466
  });
340
467
  return {
341
468
  success: true,
342
- result: `Successfully installed ${path.basename(appPath)}`
469
+ result: `Successfully installed ${path.basename(appPath)}`,
343
470
  };
344
471
  }
345
472
  catch (error) {
346
473
  return {
347
474
  success: false,
348
- error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`
475
+ error: `Failed to install app: ${error instanceof Error ? error.message : String(error)}`,
349
476
  };
350
477
  }
351
478
  }
@@ -358,7 +485,7 @@ export async function iosLaunchApp(bundleId, udid) {
358
485
  if (!simctlAvailable) {
359
486
  return {
360
487
  success: false,
361
- error: "Xcode command line tools not available. Install Xcode from the App Store."
488
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
362
489
  };
363
490
  }
364
491
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -366,21 +493,27 @@ export async function iosLaunchApp(bundleId, udid) {
366
493
  if (!targetUdid) {
367
494
  return {
368
495
  success: false,
369
- error: "No iOS simulator is currently running. Start a simulator first."
496
+ error: "No iOS simulator is currently running. Start a simulator first.",
370
497
  };
371
498
  }
372
499
  await execAsync(`xcrun simctl launch ${targetUdid} ${bundleId}`, {
373
- timeout: SIMCTL_TIMEOUT
500
+ timeout: SIMCTL_TIMEOUT,
374
501
  });
375
502
  return {
376
503
  success: true,
377
- result: `Launched ${bundleId}`
504
+ result: `Launched ${bundleId}`,
378
505
  };
379
506
  }
380
507
  catch (error) {
508
+ const msg = error instanceof Error ? error.message : String(error);
509
+ let hint = "";
510
+ if (msg.includes("FBSOpenApplicationErrorDomain")) {
511
+ hint =
512
+ "\n\nCommon causes:\n- App is not installed on this simulator (install it first with ios_install_app)\n- Bundle ID is incorrect (check with: xcrun simctl listapps booted)";
513
+ }
381
514
  return {
382
515
  success: false,
383
- error: `Failed to launch app: ${error instanceof Error ? error.message : String(error)}`
516
+ error: `Failed to launch app: ${msg}${hint}`,
384
517
  };
385
518
  }
386
519
  }
@@ -393,7 +526,7 @@ export async function iosOpenUrl(url, udid) {
393
526
  if (!simctlAvailable) {
394
527
  return {
395
528
  success: false,
396
- error: "Xcode command line tools not available. Install Xcode from the App Store."
529
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
397
530
  };
398
531
  }
399
532
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -401,21 +534,28 @@ export async function iosOpenUrl(url, udid) {
401
534
  if (!targetUdid) {
402
535
  return {
403
536
  success: false,
404
- error: "No iOS simulator is currently running. Start a simulator first."
537
+ error: "No iOS simulator is currently running. Start a simulator first.",
405
538
  };
406
539
  }
407
540
  await execAsync(`xcrun simctl openurl ${targetUdid} "${url}"`, {
408
- timeout: SIMCTL_TIMEOUT
541
+ timeout: SIMCTL_TIMEOUT,
409
542
  });
410
543
  return {
411
544
  success: true,
412
- result: `Opened URL: ${url}`
545
+ result: `Opened URL: ${url}`,
413
546
  };
414
547
  }
415
548
  catch (error) {
549
+ const msg = error instanceof Error ? error.message : String(error);
550
+ let hint = "";
551
+ if (msg.includes("LSApplicationWorkspaceError") ||
552
+ msg.includes("OpenApplicationErrorDomain")) {
553
+ hint =
554
+ "\n\nCommon causes:\n- Custom URL scheme not registered in the app's Info.plist\n- App that handles this URL scheme is not installed\n- For Expo apps, use the exp:// URL format";
555
+ }
416
556
  return {
417
557
  success: false,
418
- error: `Failed to open URL: ${error instanceof Error ? error.message : String(error)}`
558
+ error: `Failed to open URL: ${msg}${hint}`,
419
559
  };
420
560
  }
421
561
  }
@@ -428,7 +568,7 @@ export async function iosTerminateApp(bundleId, udid) {
428
568
  if (!simctlAvailable) {
429
569
  return {
430
570
  success: false,
431
- error: "Xcode command line tools not available. Install Xcode from the App Store."
571
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
432
572
  };
433
573
  }
434
574
  // Resolve target UDID (prefer Metro-connected simulator)
@@ -436,21 +576,21 @@ export async function iosTerminateApp(bundleId, udid) {
436
576
  if (!targetUdid) {
437
577
  return {
438
578
  success: false,
439
- error: "No iOS simulator is currently running. Start a simulator first."
579
+ error: "No iOS simulator is currently running. Start a simulator first.",
440
580
  };
441
581
  }
442
582
  await execAsync(`xcrun simctl terminate ${targetUdid} ${bundleId}`, {
443
- timeout: SIMCTL_TIMEOUT
583
+ timeout: SIMCTL_TIMEOUT,
444
584
  });
445
585
  return {
446
586
  success: true,
447
- result: `Terminated ${bundleId}`
587
+ result: `Terminated ${bundleId}`,
448
588
  };
449
589
  }
450
590
  catch (error) {
451
591
  return {
452
592
  success: false,
453
- error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`
593
+ error: `Failed to terminate app: ${error instanceof Error ? error.message : String(error)}`,
454
594
  };
455
595
  }
456
596
  }
@@ -463,11 +603,11 @@ export async function iosBootSimulator(udid) {
463
603
  if (!simctlAvailable) {
464
604
  return {
465
605
  success: false,
466
- error: "Xcode command line tools not available. Install Xcode from the App Store."
606
+ error: "Xcode command line tools not available. Install Xcode from the App Store.",
467
607
  };
468
608
  }
469
609
  await execAsync(`xcrun simctl boot ${udid}`, {
470
- timeout: 60000 // 1 minute timeout for boot
610
+ timeout: 60000, // 1 minute timeout for boot
471
611
  });
472
612
  // Open Simulator app
473
613
  await execAsync("open -a Simulator", { timeout: 10000 }).catch(() => {
@@ -475,7 +615,7 @@ export async function iosBootSimulator(udid) {
475
615
  });
476
616
  return {
477
617
  success: true,
478
- result: `Simulator ${udid} is now booting`
618
+ result: `Simulator ${udid} is now booting`,
479
619
  };
480
620
  }
481
621
  catch (error) {
@@ -484,12 +624,12 @@ export async function iosBootSimulator(udid) {
484
624
  if (errorMessage.includes("Unable to boot device in current state: Booted")) {
485
625
  return {
486
626
  success: true,
487
- result: "Simulator is already booted"
627
+ result: "Simulator is already booted",
488
628
  };
489
629
  }
490
630
  return {
491
631
  success: false,
492
- error: `Failed to boot simulator: ${errorMessage}`
632
+ error: `Failed to boot simulator: ${errorMessage}`,
493
633
  };
494
634
  }
495
635
  }
@@ -503,282 +643,256 @@ export async function iosBootSimulator(udid) {
503
643
  */
504
644
  export async function iosTap(x, y, options) {
505
645
  try {
506
- const idbAvailable = await isIdbAvailable();
507
- if (!idbAvailable) {
508
- return {
509
- success: false,
510
- error: "IDB is not installed. Install with: brew install idb-companion"
511
- };
512
- }
513
- // Get simulator UDID (prefer Metro-connected, then fall back to booted)
514
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
515
- if (!targetUdid) {
516
- return {
517
- success: false,
518
- error: "No iOS simulator is currently running. Start a simulator first."
519
- };
520
- }
521
- // Ensure IDB is connected to the simulator
522
- const connectResult = await ensureIdbConnected(targetUdid);
523
- if (!connectResult.success) {
524
- return { success: false, error: connectResult.error };
525
- }
646
+ const preflight = await ensureUiDriverReady(options?.udid);
647
+ if (!preflight.ready)
648
+ return preflight.error;
649
+ const { driver, targetUdid } = preflight;
526
650
  const xRounded = Math.round(x);
527
651
  const yRounded = Math.round(y);
528
- // Build args array for execFile (no shell)
529
- const args = ["ui", "tap", "--udid", targetUdid];
530
- if (options?.duration !== undefined) {
531
- args.push("--duration", String(options.duration));
532
- }
533
- args.push("--json", "--", String(xRounded), String(yRounded));
534
- const { stderr } = await runIdb(...args);
535
- if (stderr)
536
- throw new Error(stderr);
652
+ if (driver === "axe") {
653
+ if (options?.duration !== undefined) {
654
+ // Long press: use axe touch with delay (convert ms to seconds)
655
+ const delaySecs = String(options.duration / 1000);
656
+ await runAxe("touch", "-x", String(xRounded), "-y", String(yRounded), "--down", "--up", "--delay", delaySecs, "--udid", targetUdid);
657
+ }
658
+ else {
659
+ await runAxe("tap", "-x", String(xRounded), "-y", String(yRounded), "--udid", targetUdid);
660
+ }
661
+ }
662
+ else {
663
+ const args = ["ui", "tap", "--udid", targetUdid];
664
+ if (options?.duration !== undefined) {
665
+ args.push("--duration", String(options.duration));
666
+ }
667
+ args.push("--json", "--", String(xRounded), String(yRounded));
668
+ const { stderr } = await runIdb(...args);
669
+ if (stderr)
670
+ throw new Error(stderr);
671
+ }
537
672
  return {
538
673
  success: true,
539
- result: `Tapped at (${xRounded}, ${yRounded})`
674
+ result: `Tapped at (${xRounded}, ${yRounded})`,
540
675
  };
541
676
  }
542
677
  catch (error) {
543
678
  return {
544
679
  success: false,
545
- error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`
680
+ error: `Failed to tap: ${error instanceof Error ? error.message : String(error)}`,
546
681
  };
547
682
  }
548
683
  }
549
684
  /**
550
- * Swipe gesture on an iOS simulator using IDB
685
+ * Swipe gesture on an iOS simulator
551
686
  */
552
687
  export async function iosSwipe(startX, startY, endX, endY, options) {
553
688
  try {
554
- const idbAvailable = await isIdbAvailable();
555
- if (!idbAvailable) {
556
- return {
557
- success: false,
558
- error: "IDB is not installed. Install with: brew install idb-companion"
559
- };
560
- }
561
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
562
- if (!targetUdid) {
563
- return {
564
- success: false,
565
- error: "No iOS simulator is currently running. Start a simulator first."
566
- };
567
- }
568
- // Ensure IDB is connected to the simulator
569
- const connectResult = await ensureIdbConnected(targetUdid);
570
- if (!connectResult.success) {
571
- return { success: false, error: connectResult.error };
572
- }
689
+ const preflight = await ensureUiDriverReady(options?.udid);
690
+ if (!preflight.ready)
691
+ return preflight.error;
692
+ const { driver, targetUdid } = preflight;
573
693
  const x1 = Math.round(startX);
574
694
  const y1 = Math.round(startY);
575
695
  const x2 = Math.round(endX);
576
696
  const y2 = Math.round(endY);
577
- // Build args array for execFile (no shell)
578
- const args = ["ui", "swipe", "--udid", targetUdid];
579
- if (options?.duration !== undefined) {
580
- args.push("--duration", String(options.duration));
581
- }
582
- if (options?.delta !== undefined) {
583
- args.push("--delta", String(options.delta));
584
- }
585
- args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
586
- const { stderr } = await runIdb(...args);
587
- if (stderr)
588
- throw new Error(stderr);
697
+ if (driver === "axe") {
698
+ const args = [
699
+ "swipe",
700
+ "--start-x",
701
+ String(x1),
702
+ "--start-y",
703
+ String(y1),
704
+ "--end-x",
705
+ String(x2),
706
+ "--end-y",
707
+ String(y2),
708
+ ];
709
+ if (options?.duration !== undefined) {
710
+ args.push("--duration", String(options.duration / 1000));
711
+ }
712
+ if (options?.delta !== undefined) {
713
+ args.push("--delta", String(options.delta));
714
+ }
715
+ args.push("--udid", targetUdid);
716
+ await runAxe(...args);
717
+ }
718
+ else {
719
+ const args = ["ui", "swipe", "--udid", targetUdid];
720
+ if (options?.duration !== undefined) {
721
+ args.push("--duration", String(options.duration));
722
+ }
723
+ if (options?.delta !== undefined) {
724
+ args.push("--delta", String(options.delta));
725
+ }
726
+ args.push("--json", "--", String(x1), String(y1), String(x2), String(y2));
727
+ const { stderr } = await runIdb(...args);
728
+ if (stderr)
729
+ throw new Error(stderr);
730
+ }
589
731
  return {
590
732
  success: true,
591
- result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`
733
+ result: `Swiped from (${x1}, ${y1}) to (${x2}, ${y2})`,
592
734
  };
593
735
  }
594
736
  catch (error) {
595
737
  return {
596
738
  success: false,
597
- error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`
739
+ error: `Failed to swipe: ${error instanceof Error ? error.message : String(error)}`,
598
740
  };
599
741
  }
600
742
  }
601
743
  /**
602
- * Input text into the active field on an iOS simulator using IDB
744
+ * Input text into the active field on an iOS simulator
603
745
  */
604
746
  export async function iosInputText(text, udid) {
605
747
  try {
606
- const idbAvailable = await isIdbAvailable();
607
- if (!idbAvailable) {
608
- return {
609
- success: false,
610
- error: "IDB is not installed. Install with: brew install idb-companion"
611
- };
612
- }
613
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
614
- if (!targetUdid) {
615
- return {
616
- success: false,
617
- error: "No iOS simulator is currently running. Start a simulator first."
618
- };
748
+ const preflight = await ensureUiDriverReady(udid);
749
+ if (!preflight.ready)
750
+ return preflight.error;
751
+ const { driver, targetUdid } = preflight;
752
+ if (driver === "axe") {
753
+ await runAxe("type", text, "--udid", targetUdid);
619
754
  }
620
- // Ensure IDB is connected to the simulator
621
- const connectResult = await ensureIdbConnected(targetUdid);
622
- if (!connectResult.success) {
623
- return { success: false, error: connectResult.error };
755
+ else {
756
+ const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
757
+ if (stderr)
758
+ throw new Error(stderr);
624
759
  }
625
- // Use execFile with args array (no shell escaping needed)
626
- const { stderr } = await runIdb("ui", "text", "--udid", targetUdid, text);
627
- if (stderr)
628
- throw new Error(stderr);
629
760
  return {
630
761
  success: true,
631
- result: `Typed text: "${text}"`
762
+ result: `Typed text: "${text}"`,
632
763
  };
633
764
  }
634
765
  catch (error) {
635
766
  return {
636
767
  success: false,
637
- error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`
768
+ error: `Failed to input text: ${error instanceof Error ? error.message : String(error)}`,
638
769
  };
639
770
  }
640
771
  }
641
772
  /**
642
- * Press a hardware button on an iOS simulator using IDB
773
+ * Press a hardware button on an iOS simulator
643
774
  */
644
775
  export async function iosButton(button, options) {
645
776
  try {
646
- const idbAvailable = await isIdbAvailable();
647
- if (!idbAvailable) {
648
- return {
649
- success: false,
650
- error: "IDB is not installed. Install with: brew install idb-companion"
651
- };
652
- }
653
777
  // Validate button type
654
778
  if (!IOS_BUTTON_TYPES.includes(button)) {
655
779
  return {
656
780
  success: false,
657
- error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`
781
+ error: `Invalid button type: ${button}. Valid options: ${IOS_BUTTON_TYPES.join(", ")}`,
658
782
  };
659
783
  }
660
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
661
- if (!targetUdid) {
662
- return {
663
- success: false,
664
- error: "No iOS simulator is currently running. Start a simulator first."
665
- };
666
- }
667
- // Ensure IDB is connected to the simulator
668
- const connectResult = await ensureIdbConnected(targetUdid);
669
- if (!connectResult.success) {
670
- return { success: false, error: connectResult.error };
784
+ const preflight = await ensureUiDriverReady(options?.udid);
785
+ if (!preflight.ready)
786
+ return preflight.error;
787
+ const { driver, targetUdid } = preflight;
788
+ if (driver === "axe") {
789
+ const args = ["button", toAxeButtonName(button)];
790
+ if (options?.duration !== undefined) {
791
+ args.push("--duration", String(options.duration));
792
+ }
793
+ args.push("--udid", targetUdid);
794
+ await runAxe(...args);
671
795
  }
672
- // Build args array for execFile (no shell)
673
- const args = ["ui", "button", "--udid", targetUdid];
674
- if (options?.duration !== undefined) {
675
- args.push("--duration", String(options.duration));
796
+ else {
797
+ const args = ["ui", "button", "--udid", targetUdid];
798
+ if (options?.duration !== undefined) {
799
+ args.push("--duration", String(options.duration));
800
+ }
801
+ args.push(button);
802
+ const { stderr } = await runIdb(...args);
803
+ if (stderr)
804
+ throw new Error(stderr);
676
805
  }
677
- args.push(button);
678
- const { stderr } = await runIdb(...args);
679
- if (stderr)
680
- throw new Error(stderr);
681
806
  return {
682
807
  success: true,
683
- result: `Pressed ${button} button`
808
+ result: `Pressed ${button} button`,
684
809
  };
685
810
  }
686
811
  catch (error) {
687
812
  return {
688
813
  success: false,
689
- error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`
814
+ error: `Failed to press button: ${error instanceof Error ? error.message : String(error)}`,
690
815
  };
691
816
  }
692
817
  }
693
818
  /**
694
- * Send a key event to an iOS simulator using IDB
819
+ * Send a key event to an iOS simulator
695
820
  */
696
821
  export async function iosKeyEvent(keycode, options) {
697
822
  try {
698
- const idbAvailable = await isIdbAvailable();
699
- if (!idbAvailable) {
700
- return {
701
- success: false,
702
- error: "IDB is not installed. Install with: brew install idb-companion"
703
- };
704
- }
705
- const targetUdid = options?.udid || (await getActiveOrBootedSimulatorUdid());
706
- if (!targetUdid) {
707
- return {
708
- success: false,
709
- error: "No iOS simulator is currently running. Start a simulator first."
710
- };
711
- }
712
- // Ensure IDB is connected to the simulator
713
- const connectResult = await ensureIdbConnected(targetUdid);
714
- if (!connectResult.success) {
715
- return { success: false, error: connectResult.error };
823
+ const preflight = await ensureUiDriverReady(options?.udid);
824
+ if (!preflight.ready)
825
+ return preflight.error;
826
+ const { driver, targetUdid } = preflight;
827
+ if (driver === "axe") {
828
+ const args = ["key", String(keycode)];
829
+ if (options?.duration !== undefined) {
830
+ args.push("--duration", String(options.duration));
831
+ }
832
+ args.push("--udid", targetUdid);
833
+ await runAxe(...args);
716
834
  }
717
- // Build args array for execFile (no shell)
718
- const args = ["ui", "key", "--udid", targetUdid];
719
- if (options?.duration !== undefined) {
720
- args.push("--duration", String(options.duration));
835
+ else {
836
+ const args = ["ui", "key", "--udid", targetUdid];
837
+ if (options?.duration !== undefined) {
838
+ args.push("--duration", String(options.duration));
839
+ }
840
+ args.push(String(keycode));
841
+ const { stderr } = await runIdb(...args);
842
+ if (stderr)
843
+ throw new Error(stderr);
721
844
  }
722
- args.push(String(keycode));
723
- const { stderr } = await runIdb(...args);
724
- if (stderr)
725
- throw new Error(stderr);
726
845
  return {
727
846
  success: true,
728
- result: `Sent key event: ${keycode}`
847
+ result: `Sent key event: ${keycode}`,
729
848
  };
730
849
  }
731
850
  catch (error) {
732
851
  return {
733
852
  success: false,
734
- error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`
853
+ error: `Failed to send key event: ${error instanceof Error ? error.message : String(error)}`,
735
854
  };
736
855
  }
737
856
  }
738
857
  /**
739
- * Send a sequence of key events to an iOS simulator using IDB
858
+ * Send a sequence of key events to an iOS simulator
740
859
  */
741
860
  export async function iosKeySequence(keycodes, udid) {
742
861
  try {
743
- const idbAvailable = await isIdbAvailable();
744
- if (!idbAvailable) {
745
- return {
746
- success: false,
747
- error: "IDB is not installed. Install with: brew install idb-companion"
748
- };
749
- }
750
862
  if (!keycodes || keycodes.length === 0) {
751
863
  return {
752
864
  success: false,
753
- error: "At least one keycode is required"
865
+ error: "At least one keycode is required",
754
866
  };
755
867
  }
756
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
757
- if (!targetUdid) {
758
- return {
759
- success: false,
760
- error: "No iOS simulator is currently running. Start a simulator first."
761
- };
868
+ const preflight = await ensureUiDriverReady(udid);
869
+ if (!preflight.ready)
870
+ return preflight.error;
871
+ const { driver, targetUdid } = preflight;
872
+ if (driver === "axe") {
873
+ await runAxe("key-sequence", "--keycodes", keycodes.join(","), "--udid", targetUdid);
762
874
  }
763
- // Ensure IDB is connected to the simulator
764
- const connectResult = await ensureIdbConnected(targetUdid);
765
- if (!connectResult.success) {
766
- return { success: false, error: connectResult.error };
875
+ else {
876
+ const args = [
877
+ "ui",
878
+ "key-sequence",
879
+ "--udid",
880
+ targetUdid,
881
+ ...keycodes.map(String),
882
+ ];
883
+ const { stderr } = await runIdb(...args);
884
+ if (stderr)
885
+ throw new Error(stderr);
767
886
  }
768
- // Build args array for execFile (no shell)
769
- const args = ["ui", "key-sequence", "--udid", targetUdid, ...keycodes.map(String)];
770
- const { stderr } = await runIdb(...args);
771
- if (stderr)
772
- throw new Error(stderr);
773
887
  return {
774
888
  success: true,
775
- result: `Sent key sequence: ${keycodes.join(", ")}`
889
+ result: `Sent key sequence: ${keycodes.join(", ")}`,
776
890
  };
777
891
  }
778
892
  catch (error) {
779
893
  return {
780
894
  success: false,
781
- error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`
895
+ error: `Failed to send key sequence: ${error instanceof Error ? error.message : String(error)}`,
782
896
  };
783
897
  }
784
898
  }
@@ -812,80 +926,65 @@ function formatAccessibilityTree(elements, depth = 0) {
812
926
  return lines.join("\n");
813
927
  }
814
928
  /**
815
- * Get accessibility info for the entire screen using IDB
929
+ * Get accessibility info for the entire screen
816
930
  */
817
931
  export async function iosDescribeAll(udid) {
818
932
  try {
819
- const idbAvailable = await isIdbAvailable();
820
- if (!idbAvailable) {
821
- return {
822
- success: false,
823
- error: "IDB is not installed. Install with: brew install idb-companion"
824
- };
825
- }
826
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
827
- if (!targetUdid) {
828
- return {
829
- success: false,
830
- error: "No iOS simulator is currently running. Start a simulator first."
831
- };
933
+ const preflight = await ensureUiDriverReady(udid);
934
+ if (!preflight.ready)
935
+ return preflight.error;
936
+ const { driver, targetUdid } = preflight;
937
+ let stdout;
938
+ if (driver === "axe") {
939
+ const result = await runAxe("describe-ui", "--udid", targetUdid);
940
+ stdout = result.stdout;
832
941
  }
833
- // Ensure IDB is connected to the simulator
834
- const connectResult = await ensureIdbConnected(targetUdid);
835
- if (!connectResult.success) {
836
- return { success: false, error: connectResult.error };
942
+ else {
943
+ const result = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
944
+ if (result.stderr)
945
+ throw new Error(result.stderr);
946
+ stdout = result.stdout;
837
947
  }
838
- // Use execFile with args array (no shell)
839
- const { stdout, stderr } = await runIdb("ui", "describe-all", "--udid", targetUdid, "--json", "--nested");
840
- if (stderr)
841
- throw new Error(stderr);
842
948
  // Parse JSON response
843
949
  const elements = JSON.parse(stdout);
844
950
  // Format for human-readable output
845
951
  const formatted = formatAccessibilityTree(elements);
952
+ const header = 'Note: All coordinates are in iOS points. Use tap(text="...") for tapping — do not pass these coordinates to tap(x, y) directly.\n\n';
846
953
  return {
847
954
  success: true,
848
- result: formatted || "No accessibility elements found",
849
- elements
955
+ result: header + (formatted || "No accessibility elements found"),
956
+ elements,
850
957
  };
851
958
  }
852
959
  catch (error) {
853
960
  return {
854
961
  success: false,
855
- error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`
962
+ error: `Failed to describe screen: ${error instanceof Error ? error.message : String(error)}`,
856
963
  };
857
964
  }
858
965
  }
859
966
  /**
860
- * Get accessibility info at a specific point using IDB
967
+ * Get accessibility info at a specific point
861
968
  */
862
969
  export async function iosDescribePoint(x, y, udid) {
863
970
  try {
864
- const idbAvailable = await isIdbAvailable();
865
- if (!idbAvailable) {
866
- return {
867
- success: false,
868
- error: "IDB is not installed. Install with: brew install idb-companion"
869
- };
870
- }
871
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
872
- if (!targetUdid) {
873
- return {
874
- success: false,
875
- error: "No iOS simulator is currently running. Start a simulator first."
876
- };
877
- }
878
- // Ensure IDB is connected to the simulator
879
- const connectResult = await ensureIdbConnected(targetUdid);
880
- if (!connectResult.success) {
881
- return { success: false, error: connectResult.error };
882
- }
971
+ const preflight = await ensureUiDriverReady(udid);
972
+ if (!preflight.ready)
973
+ return preflight.error;
974
+ const { driver, targetUdid } = preflight;
883
975
  const xRounded = Math.round(x);
884
976
  const yRounded = Math.round(y);
885
- // Use execFile with args array (no shell)
886
- const { stdout, stderr } = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
887
- if (stderr)
888
- throw new Error(stderr);
977
+ let stdout;
978
+ if (driver === "axe") {
979
+ const result = await runAxe("describe-ui", "--point", `${xRounded},${yRounded}`, "--udid", targetUdid);
980
+ stdout = result.stdout;
981
+ }
982
+ else {
983
+ const result = await runIdb("ui", "describe-point", "--udid", targetUdid, "--json", "--", String(xRounded), String(yRounded));
984
+ if (result.stderr)
985
+ throw new Error(result.stderr);
986
+ stdout = result.stdout;
987
+ }
889
988
  // Parse JSON response - may be single element or array
890
989
  let element;
891
990
  try {
@@ -896,7 +995,7 @@ export async function iosDescribePoint(x, y, udid) {
896
995
  return {
897
996
  success: true,
898
997
  result: `No accessibility element found at (${xRounded}, ${yRounded})`,
899
- elements: []
998
+ elements: [],
900
999
  };
901
1000
  }
902
1001
  // Format for human-readable output
@@ -919,13 +1018,13 @@ export async function iosDescribePoint(x, y, udid) {
919
1018
  result: parts.length > 0
920
1019
  ? `Element at (${xRounded}, ${yRounded}):\n${parts.join("\n")}`
921
1020
  : `No accessibility element found at (${xRounded}, ${yRounded})`,
922
- elements: element ? [element] : []
1021
+ elements: element ? [element] : [],
923
1022
  };
924
1023
  }
925
1024
  catch (error) {
926
1025
  return {
927
1026
  success: false,
928
- error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`
1027
+ error: `Failed to describe point: ${error instanceof Error ? error.message : String(error)}`,
929
1028
  };
930
1029
  }
931
1030
  }
@@ -952,7 +1051,7 @@ export async function iosTapElement(options) {
952
1051
  if (!label && !labelContains) {
953
1052
  return {
954
1053
  success: false,
955
- error: "Either 'label' (exact match) or 'labelContains' (partial match) is required"
1054
+ error: "Either 'label' (exact match) or 'labelContains' (partial match) is required",
956
1055
  };
957
1056
  }
958
1057
  // Get all accessibility elements
@@ -960,12 +1059,12 @@ export async function iosTapElement(options) {
960
1059
  if (!describeResult.success || !describeResult.elements) {
961
1060
  return {
962
1061
  success: false,
963
- error: describeResult.error || "Failed to get accessibility elements"
1062
+ error: describeResult.error || "Failed to get accessibility elements",
964
1063
  };
965
1064
  }
966
1065
  // Flatten the tree and find matching elements
967
1066
  const allElements = flattenElements(describeResult.elements);
968
- const matches = allElements.filter(el => {
1067
+ const matches = allElements.filter((el) => {
969
1068
  if (!el.AXLabel)
970
1069
  return false;
971
1070
  if (label)
@@ -975,17 +1074,19 @@ export async function iosTapElement(options) {
975
1074
  return false;
976
1075
  });
977
1076
  if (matches.length === 0) {
978
- const searchTerm = label ? `label="${label}"` : `labelContains="${labelContains}"`;
1077
+ const searchTerm = label
1078
+ ? `label="${label}"`
1079
+ : `labelContains="${labelContains}"`;
979
1080
  return {
980
1081
  success: false,
981
- error: `No element found with ${searchTerm}`
1082
+ error: `No element found with ${searchTerm}`,
982
1083
  };
983
1084
  }
984
1085
  // Select element by index (default 0 = first match)
985
1086
  if (index >= matches.length) {
986
1087
  return {
987
1088
  success: false,
988
- error: `Index ${index} out of range. Found ${matches.length} matching element(s).`
1089
+ error: `Index ${index} out of range. Found ${matches.length} matching element(s).`,
989
1090
  };
990
1091
  }
991
1092
  const element = matches[index];
@@ -993,7 +1094,7 @@ export async function iosTapElement(options) {
993
1094
  if (!element.frame) {
994
1095
  return {
995
1096
  success: false,
996
- error: `Element "${element.AXLabel}" has no frame coordinates`
1097
+ error: `Element "${element.AXLabel}" has no frame coordinates`,
997
1098
  };
998
1099
  }
999
1100
  // Calculate center
@@ -1004,7 +1105,7 @@ export async function iosTapElement(options) {
1004
1105
  if (tapResult.success) {
1005
1106
  return {
1006
1107
  success: true,
1007
- result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`
1108
+ result: `Tapped "${element.AXLabel}" at (${centerX}, ${centerY})`,
1008
1109
  };
1009
1110
  }
1010
1111
  return tapResult;
@@ -1012,7 +1113,7 @@ export async function iosTapElement(options) {
1012
1113
  catch (error) {
1013
1114
  return {
1014
1115
  success: false,
1015
- error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`
1116
+ error: `Failed to tap element: ${error instanceof Error ? error.message : String(error)}`,
1016
1117
  };
1017
1118
  }
1018
1119
  }
@@ -1030,20 +1131,27 @@ function parseIdbAccessibilityForFindElement(output) {
1030
1131
  label: node.AXLabel || node.label || "",
1031
1132
  value: node.AXValue || node.value || "",
1032
1133
  type: node.type || node.AXType || "",
1134
+ identifier: node.AXIdentifier ||
1135
+ node.identifier ||
1136
+ node.AXUniqueId ||
1137
+ "",
1033
1138
  frame: {
1034
1139
  x: frame.x || 0,
1035
1140
  y: frame.y || 0,
1036
1141
  width: frame.width || 0,
1037
- height: frame.height || 0
1142
+ height: frame.height || 0,
1038
1143
  },
1039
1144
  center: {
1040
1145
  x: Math.round((frame.x || 0) + (frame.width || 0) / 2),
1041
- y: Math.round((frame.y || 0) + (frame.height || 0) / 2)
1146
+ y: Math.round((frame.y || 0) + (frame.height || 0) / 2),
1042
1147
  },
1043
1148
  enabled: node.enabled !== false,
1044
- traits: node.traits || []
1149
+ traits: node.traits || [],
1045
1150
  };
1046
- if (element.label || element.value || element.type) {
1151
+ if (element.label ||
1152
+ element.value ||
1153
+ element.type ||
1154
+ element.identifier) {
1047
1155
  elements.push(element);
1048
1156
  }
1049
1157
  }
@@ -1068,6 +1176,35 @@ function parseIdbAccessibilityForFindElement(output) {
1068
1176
  }
1069
1177
  return elements;
1070
1178
  }
1179
+ /**
1180
+ * Check if an element's center is within the screen viewport.
1181
+ * Filters out off-screen elements in scroll views, horizontal pagers, etc.
1182
+ */
1183
+ function isIOSElementInViewport(element, screenWidth, screenHeight) {
1184
+ const { center } = element;
1185
+ return (center.x >= 0 &&
1186
+ center.x <= screenWidth &&
1187
+ center.y >= 0 &&
1188
+ center.y <= screenHeight);
1189
+ }
1190
+ /**
1191
+ * Extract screen dimensions from the parsed UI tree.
1192
+ * The root Application element's frame represents the screen size.
1193
+ */
1194
+ function extractIOSScreenSize(rawOutput) {
1195
+ try {
1196
+ const data = JSON.parse(rawOutput);
1197
+ const root = Array.isArray(data) ? data[0] : data;
1198
+ const frame = root?.frame;
1199
+ if (frame?.width && frame?.height) {
1200
+ return { width: frame.width, height: frame.height };
1201
+ }
1202
+ }
1203
+ catch {
1204
+ // Fall through
1205
+ }
1206
+ return null;
1207
+ }
1071
1208
  /**
1072
1209
  * Match iOS element against find options
1073
1210
  */
@@ -1092,6 +1229,16 @@ function matchesIOSFindElement(element, options) {
1092
1229
  if (!element.type.toLowerCase().includes(options.type.toLowerCase()))
1093
1230
  return false;
1094
1231
  }
1232
+ if (options.identifier !== undefined) {
1233
+ if (element.identifier !== options.identifier)
1234
+ return false;
1235
+ }
1236
+ if (options.identifierContains !== undefined) {
1237
+ if (!element.identifier
1238
+ .toLowerCase()
1239
+ .includes(options.identifierContains.toLowerCase()))
1240
+ return false;
1241
+ }
1095
1242
  return true;
1096
1243
  }
1097
1244
  /**
@@ -1099,36 +1246,30 @@ function matchesIOSFindElement(element, options) {
1099
1246
  */
1100
1247
  export async function iosGetUITree(udid) {
1101
1248
  try {
1102
- const idbAvailable = await isIdbAvailable();
1103
- if (!idbAvailable) {
1104
- return {
1105
- success: false,
1106
- error: "IDB is not installed. Install with: brew install idb-companion"
1107
- };
1249
+ const preflight = await ensureUiDriverReady(udid);
1250
+ if (!preflight.ready)
1251
+ return preflight.error;
1252
+ const { driver, targetUdid } = preflight;
1253
+ let stdout;
1254
+ if (driver === "axe") {
1255
+ const result = await runAxe("describe-ui", "--udid", targetUdid);
1256
+ stdout = result.stdout;
1108
1257
  }
1109
- const targetUdid = udid || (await getActiveOrBootedSimulatorUdid());
1110
- if (!targetUdid) {
1111
- return {
1112
- success: false,
1113
- error: "No iOS simulator is currently running. Start a simulator first."
1114
- };
1115
- }
1116
- const connectResult = await ensureIdbConnected(targetUdid);
1117
- if (!connectResult.success) {
1118
- return { success: false, error: connectResult.error };
1258
+ else {
1259
+ const result = await runIdb("ui", "describe-all", "--udid", targetUdid);
1260
+ stdout = result.stdout;
1119
1261
  }
1120
- const { stdout } = await runIdb("ui", "describe-all", "--udid", targetUdid);
1121
1262
  const elements = parseIdbAccessibilityForFindElement(stdout);
1122
1263
  return {
1123
1264
  success: true,
1124
1265
  elements,
1125
- rawOutput: stdout
1266
+ rawOutput: stdout,
1126
1267
  };
1127
1268
  }
1128
1269
  catch (error) {
1129
1270
  return {
1130
1271
  success: false,
1131
- error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`
1272
+ error: `Failed to get UI tree: ${error instanceof Error ? error.message : String(error)}`,
1132
1273
  };
1133
1274
  }
1134
1275
  }
@@ -1137,12 +1278,17 @@ export async function iosGetUITree(udid) {
1137
1278
  */
1138
1279
  export async function iosFindElement(options, udid) {
1139
1280
  try {
1140
- if (!options.label && !options.labelContains && !options.value &&
1141
- !options.valueContains && !options.type) {
1281
+ if (!options.label &&
1282
+ !options.labelContains &&
1283
+ !options.value &&
1284
+ !options.valueContains &&
1285
+ !options.type &&
1286
+ !options.identifier &&
1287
+ !options.identifierContains) {
1142
1288
  return {
1143
1289
  success: false,
1144
1290
  found: false,
1145
- error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1291
+ error: "At least one search criteria (label, labelContains, value, valueContains, identifier, identifierContains, or type) must be provided",
1146
1292
  };
1147
1293
  }
1148
1294
  const treeResult = await iosGetUITree(udid);
@@ -1150,15 +1296,28 @@ export async function iosFindElement(options, udid) {
1150
1296
  return {
1151
1297
  success: false,
1152
1298
  found: false,
1153
- error: treeResult.error
1299
+ error: treeResult.error,
1154
1300
  };
1155
1301
  }
1156
- const matches = treeResult.elements.filter(el => matchesIOSFindElement(el, options));
1302
+ // Filter to viewport-visible elements first, then match criteria.
1303
+ // This prevents selecting off-screen duplicates in scroll views / pagers.
1304
+ const screenSize = treeResult.rawOutput
1305
+ ? extractIOSScreenSize(treeResult.rawOutput)
1306
+ : null;
1307
+ const visibleElements = screenSize
1308
+ ? treeResult.elements.filter((el) => isIOSElementInViewport(el, screenSize.width, screenSize.height))
1309
+ : treeResult.elements;
1310
+ let matches = visibleElements.filter((el) => matchesIOSFindElement(el, options));
1311
+ // If no visible matches, fall back to all elements (element may be partially visible
1312
+ // or the screen size extraction failed)
1313
+ if (matches.length === 0) {
1314
+ matches = treeResult.elements.filter((el) => matchesIOSFindElement(el, options));
1315
+ }
1157
1316
  if (matches.length === 0) {
1158
1317
  return {
1159
1318
  success: true,
1160
1319
  found: false,
1161
- matchCount: 0
1320
+ matchCount: 0,
1162
1321
  };
1163
1322
  }
1164
1323
  const index = options.index ?? 0;
@@ -1168,7 +1327,7 @@ export async function iosFindElement(options, udid) {
1168
1327
  success: true,
1169
1328
  found: false,
1170
1329
  matchCount: matches.length,
1171
- error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`
1330
+ error: `Index ${index} out of bounds. Found ${matches.length} matching element(s).`,
1172
1331
  };
1173
1332
  }
1174
1333
  return {
@@ -1176,17 +1335,85 @@ export async function iosFindElement(options, udid) {
1176
1335
  found: true,
1177
1336
  element: selectedElement,
1178
1337
  allMatches: matches,
1179
- matchCount: matches.length
1338
+ matchCount: matches.length,
1180
1339
  };
1181
1340
  }
1182
1341
  catch (error) {
1183
1342
  return {
1184
1343
  success: false,
1185
1344
  found: false,
1186
- error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`
1345
+ error: `Failed to find element: ${error instanceof Error ? error.message : String(error)}`,
1187
1346
  };
1188
1347
  }
1189
1348
  }
1349
+ // DPR cache: UDID → device pixel ratio (never changes during session)
1350
+ const dprCache = new Map();
1351
+ /**
1352
+ * Pure calculation: DPR = screenshotPixelWidth / screenPointWidth.
1353
+ * Exported for unit testing.
1354
+ */
1355
+ export function calculateDPR(screenshotPixelWidth, screenPointWidth) {
1356
+ return Math.round(screenshotPixelWidth / screenPointWidth);
1357
+ }
1358
+ /**
1359
+ * Query the actual device pixel ratio for an iOS simulator.
1360
+ * Uses screenshot pixel width vs accessibility root frame width.
1361
+ * Result is cached per UDID for the session.
1362
+ */
1363
+ export async function getDevicePixelRatio(udid) {
1364
+ const cacheKey = udid || "__default__";
1365
+ const cached = dprCache.get(cacheKey);
1366
+ if (cached)
1367
+ return cached;
1368
+ const resolvedUdid = udid || (await getActiveOrBootedSimulatorUdid());
1369
+ if (!resolvedUdid) {
1370
+ throw new Error("No iOS simulator is currently running. Start a simulator first.");
1371
+ }
1372
+ // Take screenshot to get pixel dimensions
1373
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1374
+ const screenshotPath = path.join(os.tmpdir(), `ios-dpr-screenshot-${timestamp}.png`);
1375
+ await execAsync(`xcrun simctl io ${resolvedUdid} screenshot "${screenshotPath}"`, {
1376
+ timeout: SIMCTL_TIMEOUT,
1377
+ });
1378
+ const metadata = await sharp(screenshotPath).metadata();
1379
+ const pixelWidth = metadata.width;
1380
+ const pixelHeight = metadata.height;
1381
+ // Try accessibility tree for exact DPR, fall back to inference
1382
+ const driver = getIosDriver();
1383
+ const driverOk = driver === "axe" ? await isAxeAvailable() : await isIdbAvailable();
1384
+ if (driverOk) {
1385
+ try {
1386
+ let stdout;
1387
+ if (driver === "axe") {
1388
+ const result = await runAxe("describe-ui", "--udid", resolvedUdid);
1389
+ stdout = result.stdout;
1390
+ }
1391
+ else {
1392
+ await ensureIdbConnected(resolvedUdid);
1393
+ const result = await runIdb("ui", "describe-all", "--udid", resolvedUdid, "--json", "--nested");
1394
+ stdout = result.stdout;
1395
+ }
1396
+ const screenSize = extractIOSScreenSize(stdout);
1397
+ if (screenSize) {
1398
+ const dpr = calculateDPR(pixelWidth, screenSize.width);
1399
+ dprCache.set(cacheKey, dpr);
1400
+ return dpr;
1401
+ }
1402
+ }
1403
+ catch {
1404
+ // driver failed — fall through to inference
1405
+ }
1406
+ }
1407
+ // Fallback: infer DPR from screenshot pixel dimensions
1408
+ const { inferIOSDevicePixelRatio } = await import("./ocr.js");
1409
+ const dpr = inferIOSDevicePixelRatio(pixelWidth, pixelHeight);
1410
+ dprCache.set(cacheKey, dpr);
1411
+ return dpr;
1412
+ }
1413
+ /** Clear DPR cache (for testing or when devices change) */
1414
+ export function clearDPRCache() {
1415
+ dprCache.clear();
1416
+ }
1190
1417
  /**
1191
1418
  * Wait for element to appear on iOS screen with polling
1192
1419
  */
@@ -1194,13 +1421,16 @@ export async function iosWaitForElement(options, udid) {
1194
1421
  const timeoutMs = options.timeoutMs ?? 10000;
1195
1422
  const pollIntervalMs = options.pollIntervalMs ?? 500;
1196
1423
  const startTime = Date.now();
1197
- if (!options.label && !options.labelContains && !options.value &&
1198
- !options.valueContains && !options.type) {
1424
+ if (!options.label &&
1425
+ !options.labelContains &&
1426
+ !options.value &&
1427
+ !options.valueContains &&
1428
+ !options.type) {
1199
1429
  return {
1200
1430
  success: false,
1201
1431
  found: false,
1202
1432
  timedOut: false,
1203
- error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided"
1433
+ error: "At least one search criteria (label, labelContains, value, valueContains, or type) must be provided",
1204
1434
  };
1205
1435
  }
1206
1436
  while (Date.now() - startTime < timeoutMs) {
@@ -1209,24 +1439,24 @@ export async function iosWaitForElement(options, udid) {
1209
1439
  return {
1210
1440
  ...result,
1211
1441
  elapsedMs: Date.now() - startTime,
1212
- timedOut: false
1442
+ timedOut: false,
1213
1443
  };
1214
1444
  }
1215
1445
  if (!result.success) {
1216
1446
  return {
1217
1447
  ...result,
1218
1448
  elapsedMs: Date.now() - startTime,
1219
- timedOut: false
1449
+ timedOut: false,
1220
1450
  };
1221
1451
  }
1222
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
1452
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1223
1453
  }
1224
1454
  return {
1225
1455
  success: true,
1226
1456
  found: false,
1227
1457
  elapsedMs: Date.now() - startTime,
1228
1458
  timedOut: true,
1229
- error: `Timed out after ${timeoutMs}ms waiting for element`
1459
+ error: `Timed out after ${timeoutMs}ms waiting for element`,
1230
1460
  };
1231
1461
  }
1232
1462
  //# sourceMappingURL=ios.js.map