@ceraph/react-native-mcp 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +116 -15
  2. package/README.md +79 -77
  3. package/assets/default.png +0 -0
  4. package/dist/app-lifecycle.d.ts +50 -0
  5. package/dist/app-lifecycle.js +487 -0
  6. package/dist/camera-image-writer.d.ts +43 -0
  7. package/dist/camera-image-writer.js +280 -0
  8. package/dist/camera-registry-sync.d.ts +18 -0
  9. package/dist/camera-registry-sync.js +117 -0
  10. package/dist/cli.d.ts +0 -7
  11. package/dist/cli.js +41 -9
  12. package/dist/device-autonomy.d.ts +30 -0
  13. package/dist/device-autonomy.js +117 -0
  14. package/dist/error-parser.d.ts +6 -26
  15. package/dist/error-parser.js +4 -74
  16. package/dist/expo-manager.d.ts +2 -74
  17. package/dist/expo-manager.js +11 -125
  18. package/dist/index.d.ts +0 -7
  19. package/dist/index.js +1266 -56
  20. package/dist/init/ast-camera.d.ts +29 -0
  21. package/dist/init/ast-camera.js +267 -0
  22. package/dist/init/ast-layout.d.ts +15 -0
  23. package/dist/init/ast-layout.js +167 -0
  24. package/dist/init/claude-hook-constants.d.ts +9 -0
  25. package/dist/init/claude-hook-constants.js +91 -0
  26. package/dist/init/lan-ip.d.ts +11 -0
  27. package/dist/init/lan-ip.js +51 -0
  28. package/dist/init/monorepo.d.ts +13 -0
  29. package/dist/init/monorepo.js +185 -0
  30. package/dist/init/oauth.d.ts +52 -0
  31. package/dist/init/oauth.js +220 -0
  32. package/dist/init/package-manager.d.ts +11 -0
  33. package/dist/init/package-manager.js +60 -0
  34. package/dist/init/prompt.d.ts +12 -0
  35. package/dist/init/prompt.js +68 -0
  36. package/dist/init/shell-profile.d.ts +22 -0
  37. package/dist/init/shell-profile.js +85 -0
  38. package/dist/init/steps.d.ts +135 -0
  39. package/dist/init/steps.js +399 -0
  40. package/dist/init/url-scheme.d.ts +42 -0
  41. package/dist/init/url-scheme.js +187 -0
  42. package/dist/init/walkthrough.d.ts +76 -0
  43. package/dist/init/walkthrough.js +340 -0
  44. package/dist/init.d.ts +7 -7
  45. package/dist/init.js +280 -120
  46. package/dist/iproxy-manager.d.ts +32 -0
  47. package/dist/iproxy-manager.js +216 -0
  48. package/dist/mac-caffeinate.d.ts +10 -0
  49. package/dist/mac-caffeinate.js +56 -0
  50. package/dist/permission-interceptor.d.ts +29 -0
  51. package/dist/permission-interceptor.js +185 -0
  52. package/dist/prebuild-detector.d.ts +0 -30
  53. package/dist/prebuild-detector.js +1 -42
  54. package/dist/preflight.d.ts +34 -0
  55. package/dist/preflight.js +847 -0
  56. package/dist/screen.d.ts +132 -43
  57. package/dist/screen.js +668 -94
  58. package/dist/shim/boot.d.ts +41 -0
  59. package/dist/shim/boot.js +141 -0
  60. package/dist/shim/camera.d.ts +22 -0
  61. package/dist/shim/camera.js +62 -0
  62. package/dist/shim/config.d.ts +6 -0
  63. package/dist/shim/config.js +56 -0
  64. package/dist/shim/deep-link.d.ts +1 -0
  65. package/dist/shim/deep-link.js +25 -0
  66. package/dist/shim/dev-guard.d.ts +1 -0
  67. package/dist/shim/dev-guard.js +3 -0
  68. package/dist/shim/error-handler.d.ts +20 -0
  69. package/dist/shim/error-handler.js +66 -0
  70. package/dist/shim/fetch-interceptor.d.ts +13 -0
  71. package/dist/shim/fetch-interceptor.js +93 -0
  72. package/dist/shim/index.d.ts +6 -0
  73. package/dist/shim/index.js +6 -0
  74. package/dist/shim/keep-awake.d.ts +13 -0
  75. package/dist/shim/keep-awake.js +118 -0
  76. package/dist/shim/reload.d.ts +23 -0
  77. package/dist/shim/reload.js +76 -0
  78. package/dist/shim/signal-capture.d.ts +11 -0
  79. package/dist/shim/signal-capture.js +15 -0
  80. package/dist/shim/signal-transport.d.ts +17 -0
  81. package/dist/shim/signal-transport.js +43 -0
  82. package/dist/signal-listener.d.ts +27 -0
  83. package/dist/signal-listener.js +135 -0
  84. package/dist/simulator-boot.d.ts +52 -0
  85. package/dist/simulator-boot.js +227 -0
  86. package/dist/target.d.ts +48 -0
  87. package/dist/target.js +267 -0
  88. package/dist/uninstall/cli-runner.d.ts +32 -0
  89. package/dist/uninstall/cli-runner.js +223 -0
  90. package/dist/uninstall/footprint.d.ts +40 -0
  91. package/dist/uninstall/footprint.js +288 -0
  92. package/dist/uninstall/mcp-tools.d.ts +14 -0
  93. package/dist/uninstall/mcp-tools.js +175 -0
  94. package/dist/uninstall/revert-auth.d.ts +22 -0
  95. package/dist/uninstall/revert-auth.js +31 -0
  96. package/dist/uninstall/revert-boot.d.ts +24 -0
  97. package/dist/uninstall/revert-boot.js +242 -0
  98. package/dist/uninstall/revert-camera.d.ts +12 -0
  99. package/dist/uninstall/revert-camera.js +199 -0
  100. package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
  101. package/dist/uninstall/revert-ceraph-dir.js +38 -0
  102. package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
  103. package/dist/uninstall/revert-claude-hooks.js +191 -0
  104. package/dist/uninstall/revert-gitignore.d.ts +17 -0
  105. package/dist/uninstall/revert-gitignore.js +43 -0
  106. package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
  107. package/dist/uninstall/revert-mcp-clients.js +194 -0
  108. package/dist/uninstall/revert-package.d.ts +34 -0
  109. package/dist/uninstall/revert-package.js +98 -0
  110. package/dist/uninstall/revert-scheme.d.ts +36 -0
  111. package/dist/uninstall/revert-scheme.js +139 -0
  112. package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
  113. package/dist/uninstall/revert-signal-host-env.js +61 -0
  114. package/dist/uninstall/walkthrough.d.ts +80 -0
  115. package/dist/uninstall/walkthrough.js +1244 -0
  116. package/dist/utils/atomic-write.d.ts +1 -0
  117. package/dist/utils/atomic-write.js +30 -0
  118. package/dist/wait-for-device.d.ts +68 -0
  119. package/dist/wait-for-device.js +368 -0
  120. package/dist/wda-manager.d.ts +38 -0
  121. package/dist/wda-manager.js +186 -0
  122. package/dist/wda-simulator.d.ts +28 -0
  123. package/dist/wda-simulator.js +257 -0
  124. package/package.json +59 -5
package/dist/screen.js CHANGED
@@ -1,67 +1,90 @@
1
- /**
2
- * WebDriverAgent client with pixel ratio correction.
3
- *
4
- * Talks to the WDA HTTP server running on localhost:8100 to perform
5
- * taps and element lookups on a connected iOS device.
6
- */
7
- const WDA_BASE_URL = "http://localhost:8100";
1
+ import { DEFAULT_DEVICE_WDA_BASE_URL, TargetResolver } from "./target.js";
8
2
  const WDA_TIMEOUT_MS = 5000;
9
- /**
10
- * Fetch wrapper with timeout for WDA requests.
11
- */
12
- async function wdaFetch(path, options = {}) {
13
- const url = `${WDA_BASE_URL}${path}`;
14
- const controller = new AbortController();
15
- const timeout = setTimeout(() => controller.abort(), WDA_TIMEOUT_MS);
16
- try {
17
- const response = await fetch(url, {
18
- ...options,
19
- signal: controller.signal,
20
- headers: {
21
- "Content-Type": "application/json",
22
- ...options.headers,
23
- },
24
- });
25
- return response;
26
- }
27
- finally {
28
- clearTimeout(timeout);
29
- }
30
- }
31
3
  export class ScreenManager {
32
4
  sessionId = null;
33
5
  pixelRatio = null;
34
- /**
35
- * Check whether WebDriverAgent is reachable.
36
- */
6
+ targetResolver;
7
+ sessionBaseUrl = null;
8
+ constructor(opts = {}) {
9
+ this.targetResolver = opts.targetResolver ?? new TargetResolver();
10
+ }
11
+ async snapshotTarget() {
12
+ try {
13
+ const info = await this.targetResolver.resolve();
14
+ return { baseUrl: info.baseUrl, ready: info.wdaReady };
15
+ }
16
+ catch {
17
+ return { baseUrl: DEFAULT_DEVICE_WDA_BASE_URL, ready: true };
18
+ }
19
+ }
20
+ async wdaFetchAt(baseUrl, path, options = {}, timeoutMs = WDA_TIMEOUT_MS) {
21
+ const url = `${baseUrl}${path}`;
22
+ const controller = new AbortController();
23
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
24
+ try {
25
+ const response = await fetch(url, {
26
+ ...options,
27
+ signal: controller.signal,
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ ...options.headers,
31
+ },
32
+ });
33
+ return response;
34
+ }
35
+ finally {
36
+ clearTimeout(timeout);
37
+ }
38
+ }
39
+ async wdaFetch(path, options = {}, timeoutMs = WDA_TIMEOUT_MS) {
40
+ const snapshot = await this.snapshotTarget();
41
+ if (!snapshot.ready) {
42
+ throw new Error("WebDriverAgent is not ready for the selected target. If you " +
43
+ "set CERAPH_TARGET=simulator (or are using a simulator with " +
44
+ "auto-detection), call `rn_wda_start` first to build and " +
45
+ "launch WDA against the simulator.");
46
+ }
47
+ const baseUrl = this.sessionBaseUrl ?? snapshot.baseUrl;
48
+ return this.wdaFetchAt(baseUrl, path, options, timeoutMs);
49
+ }
37
50
  async isAvailable() {
51
+ const snapshot = await this.snapshotTarget();
52
+ if (!snapshot.ready)
53
+ return false;
38
54
  try {
39
- const res = await wdaFetch("/status");
55
+ const res = await this.wdaFetchAt(snapshot.baseUrl, "/status");
40
56
  return res.ok;
41
57
  }
42
58
  catch {
43
59
  return false;
44
60
  }
45
61
  }
46
- /**
47
- * Get or create a WDA session. Sessions are cached and revalidated.
48
- */
49
62
  async ensureSession() {
50
- // Try to reuse cached session
63
+ const snapshot = await this.snapshotTarget();
64
+ if (!snapshot.ready) {
65
+ throw new Error("WebDriverAgent is not ready for the selected target. If you " +
66
+ "set CERAPH_TARGET=simulator (or are using a simulator with " +
67
+ "auto-detection), call `rn_wda_start` first to build and " +
68
+ "launch WDA against the simulator.");
69
+ }
70
+ const currentBaseUrl = snapshot.baseUrl;
71
+ if (this.sessionBaseUrl && this.sessionBaseUrl !== currentBaseUrl) {
72
+ this.sessionId = null;
73
+ this.pixelRatio = null;
74
+ }
75
+ this.sessionBaseUrl = currentBaseUrl;
51
76
  if (this.sessionId) {
52
77
  try {
53
- const res = await wdaFetch(`/session/${this.sessionId}`);
78
+ const res = await this.wdaFetchAt(currentBaseUrl, `/session/${this.sessionId}`);
54
79
  if (res.ok)
55
80
  return this.sessionId;
56
81
  }
57
82
  catch {
58
- // Session is stale, create a new one
59
83
  }
60
84
  this.sessionId = null;
61
85
  }
62
- // Try to find an existing session from /status
63
86
  try {
64
- const statusRes = await wdaFetch("/status");
87
+ const statusRes = await this.wdaFetchAt(currentBaseUrl, "/status");
65
88
  if (statusRes.ok) {
66
89
  const statusData = (await statusRes.json());
67
90
  const existingId = statusData.sessionId || statusData.value?.sessionId;
@@ -72,10 +95,8 @@ export class ScreenManager {
72
95
  }
73
96
  }
74
97
  catch {
75
- // Fall through to session creation
76
98
  }
77
- // Create a new session
78
- const res = await wdaFetch("/session", {
99
+ const res = await this.wdaFetchAt(currentBaseUrl, "/session", {
79
100
  method: "POST",
80
101
  body: JSON.stringify({
81
102
  capabilities: {
@@ -95,63 +116,51 @@ export class ScreenManager {
95
116
  this.sessionId = sessionId;
96
117
  return sessionId;
97
118
  }
98
- /**
99
- * Determine the pixel ratio between screenshot coordinates and
100
- * device logical coordinates.
101
- *
102
- * Screenshot images are in physical pixels; WDA taps use logical points.
103
- * The ratio is typically 2 (for @2x Retina) or 3 (for @3x).
104
- */
105
119
  async getPixelRatio() {
106
120
  if (this.pixelRatio)
107
121
  return this.pixelRatio;
108
122
  const sessionId = await this.ensureSession();
109
- // Get the logical window size from WDA
110
- const windowRes = await wdaFetch(`/session/${sessionId}/window/size`);
123
+ const windowRes = await this.wdaFetch(`/session/${sessionId}/window/size`);
111
124
  if (!windowRes.ok) {
112
125
  throw new Error(`Failed to get window size: ${windowRes.status}`);
113
126
  }
114
127
  const windowData = (await windowRes.json());
115
128
  const logicalWidth = windowData.value.width;
116
- // Get a screenshot to measure actual pixel dimensions
117
- const screenshotRes = await wdaFetch(`/session/${sessionId}/screenshot`);
129
+ const screenshotRes = await this.wdaFetch(`/session/${sessionId}/screenshot`);
118
130
  if (!screenshotRes.ok) {
119
- // Fallback: assume 3x for modern iPhones
120
131
  this.pixelRatio = 3;
121
132
  return this.pixelRatio;
122
133
  }
123
134
  const screenshotData = (await screenshotRes.json());
124
135
  const base64 = screenshotData.value;
125
- // Decode the PNG header to get image width.
126
- // PNG stores width as a big-endian 32-bit int at byte offset 16.
127
136
  const pngBuffer = Buffer.from(base64, "base64");
128
137
  if (pngBuffer.length < 24) {
129
- // Not enough data to read PNG header; fallback
130
138
  this.pixelRatio = 3;
131
139
  return this.pixelRatio;
132
140
  }
133
141
  const imageWidth = pngBuffer.readUInt32BE(16);
134
142
  this.pixelRatio = Math.round(imageWidth / logicalWidth);
135
- // Sanity check
136
143
  if (this.pixelRatio < 1 || this.pixelRatio > 4) {
137
- this.pixelRatio = 3; // safe default for modern iPhones
144
+ this.pixelRatio = 3;
138
145
  }
139
146
  return this.pixelRatio;
140
147
  }
141
- /**
142
- * Tap at the given coordinates.
143
- *
144
- * When `fromScreenshot` is true (the default), the coordinates are
145
- * assumed to come from a screenshot image and are divided by the
146
- * pixel ratio to convert to logical device points.
147
- */
148
+ async getWindowSize() {
149
+ const sessionId = await this.ensureSession();
150
+ const res = await this.wdaFetch(`/session/${sessionId}/window/size`);
151
+ if (!res.ok) {
152
+ throw new Error(`Failed to get window size: ${res.status}`);
153
+ }
154
+ const data = (await res.json());
155
+ return data.value;
156
+ }
148
157
  async tap(x, y, fromScreenshot = true) {
149
158
  try {
150
159
  const sessionId = await this.ensureSession();
151
160
  const ratio = await this.getPixelRatio();
152
161
  const correctedX = fromScreenshot ? x / ratio : x;
153
162
  const correctedY = fromScreenshot ? y / ratio : y;
154
- const res = await wdaFetch(`/session/${sessionId}/wda/tap/0`, {
163
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/tap/0`, {
155
164
  method: "POST",
156
165
  body: JSON.stringify({ x: correctedX, y: correctedY }),
157
166
  });
@@ -186,14 +195,10 @@ export class ScreenManager {
186
195
  };
187
196
  }
188
197
  }
189
- /**
190
- * Find an element in the UI tree and tap its center.
191
- */
192
198
  async findAndTap(query) {
193
199
  try {
194
200
  const sessionId = await this.ensureSession();
195
- // Fetch the full element source tree
196
- const sourceRes = await wdaFetch(`/session/${sessionId}/source?format=json`);
201
+ const sourceRes = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
197
202
  if (!sourceRes.ok) {
198
203
  return {
199
204
  success: false,
@@ -202,10 +207,8 @@ export class ScreenManager {
202
207
  }
203
208
  const sourceData = (await sourceRes.json());
204
209
  const root = sourceData.value;
205
- // Find matching elements
206
210
  const matches = this.searchElements(root, query);
207
211
  if (matches.length === 0) {
208
- // Collect a summary of visible elements for debugging
209
212
  const visible = this.collectVisibleElements(root, 50);
210
213
  return {
211
214
  success: false,
@@ -213,7 +216,6 @@ export class ScreenManager {
213
216
  error: `No element found matching query: ${JSON.stringify(query)}`,
214
217
  };
215
218
  }
216
- // Select the element (by index or first match)
217
219
  const index = query.index ?? 0;
218
220
  if (index >= matches.length) {
219
221
  return {
@@ -234,11 +236,9 @@ export class ScreenManager {
234
236
  error: "Matched element has no bounds/rect information.",
235
237
  };
236
238
  }
237
- // Calculate center point (already in device/logical coordinates)
238
239
  const centerX = rect.x + rect.width / 2;
239
240
  const centerY = rect.y + rect.height / 2;
240
- // Tap at center -- these are already logical coordinates, no correction
241
- const tapRes = await wdaFetch(`/session/${sessionId}/wda/tap/0`, {
241
+ const tapRes = await this.wdaFetch(`/session/${sessionId}/wda/tap/0`, {
242
242
  method: "POST",
243
243
  body: JSON.stringify({ x: centerX, y: centerY }),
244
244
  });
@@ -274,9 +274,593 @@ export class ScreenManager {
274
274
  };
275
275
  }
276
276
  }
277
- /**
278
- * Recursively search the WDA element tree for elements matching the query.
279
- */
277
+ async findElement(query) {
278
+ try {
279
+ const sessionId = await this.ensureSession();
280
+ const sourceRes = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
281
+ if (!sourceRes.ok) {
282
+ return {
283
+ success: false,
284
+ error: `Failed to get element tree: ${sourceRes.status}`,
285
+ };
286
+ }
287
+ const sourceData = (await sourceRes.json());
288
+ const matches = this.searchElements(sourceData.value, query);
289
+ if (matches.length === 0) {
290
+ return { success: false, matchCount: 0 };
291
+ }
292
+ const index = query.index ?? 0;
293
+ if (index >= matches.length) {
294
+ return { success: false, matchCount: matches.length };
295
+ }
296
+ return { success: true, element: matches[index], matchCount: matches.length };
297
+ }
298
+ catch (err) {
299
+ return {
300
+ success: false,
301
+ error: err instanceof Error ? err.message : "Unknown error during findElement",
302
+ };
303
+ }
304
+ }
305
+ async swipe(opts) {
306
+ try {
307
+ const sessionId = await this.ensureSession();
308
+ const windowSize = await this.getWindowSize();
309
+ const ratio = await this.getPixelRatio();
310
+ const fromScreenshot = (opts.coordinateSource ?? "device") === "screenshot";
311
+ let fromX = opts.from?.x ?? windowSize.width / 2;
312
+ let fromY = opts.from?.y ?? windowSize.height / 2;
313
+ if (fromScreenshot) {
314
+ fromX = fromX / ratio;
315
+ fromY = fromY / ratio;
316
+ }
317
+ const axis = opts.direction === "up" || opts.direction === "down"
318
+ ? windowSize.height
319
+ : windowSize.width;
320
+ const distance = opts.distancePx ?? Math.floor(axis * 0.6);
321
+ let toX = fromX;
322
+ let toY = fromY;
323
+ switch (opts.direction) {
324
+ case "up":
325
+ toY = fromY - distance;
326
+ break;
327
+ case "down":
328
+ toY = fromY + distance;
329
+ break;
330
+ case "left":
331
+ toX = fromX - distance;
332
+ break;
333
+ case "right":
334
+ toX = fromX + distance;
335
+ break;
336
+ }
337
+ toX = Math.max(0, Math.min(windowSize.width - 1, toX));
338
+ toY = Math.max(0, Math.min(windowSize.height - 1, toY));
339
+ fromX = Math.max(0, Math.min(windowSize.width - 1, fromX));
340
+ fromY = Math.max(0, Math.min(windowSize.height - 1, fromY));
341
+ const durationMs = opts.durationMs ?? 300;
342
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/dragfromtoforduration`, {
343
+ method: "POST",
344
+ body: JSON.stringify({
345
+ fromX,
346
+ fromY,
347
+ toX,
348
+ toY,
349
+ duration: durationMs / 1000,
350
+ }),
351
+ });
352
+ if (!res.ok) {
353
+ const text = await res.text().catch(() => "unknown");
354
+ return {
355
+ success: false,
356
+ from: { x: fromX, y: fromY },
357
+ to: { x: toX, y: toY },
358
+ durationMs,
359
+ error: `WDA swipe failed: ${res.status} ${text}`,
360
+ };
361
+ }
362
+ return {
363
+ success: true,
364
+ from: { x: fromX, y: fromY },
365
+ to: { x: toX, y: toY },
366
+ durationMs,
367
+ };
368
+ }
369
+ catch (err) {
370
+ return {
371
+ success: false,
372
+ from: { x: 0, y: 0 },
373
+ to: { x: 0, y: 0 },
374
+ durationMs: opts.durationMs ?? 300,
375
+ error: err instanceof Error ? err.message : "Unknown error during swipe",
376
+ };
377
+ }
378
+ }
379
+ async scrollToElement(query, opts = {}) {
380
+ const maxSwipes = opts.maxSwipes ?? 10;
381
+ const direction = opts.direction ?? "up";
382
+ for (let i = 0; i <= maxSwipes; i++) {
383
+ const found = await this.findElement(query);
384
+ if (found.success && found.element?.rect) {
385
+ return {
386
+ success: true,
387
+ swipes: i,
388
+ element: {
389
+ type: found.element.type ?? "unknown",
390
+ label: found.element.label ?? found.element.name ?? "",
391
+ bounds: found.element.rect,
392
+ },
393
+ };
394
+ }
395
+ if (i === maxSwipes)
396
+ break;
397
+ const swipeRes = await this.swipe({
398
+ direction,
399
+ distancePx: opts.distancePx,
400
+ });
401
+ if (!swipeRes.success) {
402
+ return {
403
+ success: false,
404
+ swipes: i,
405
+ error: swipeRes.error ?? "Swipe failed during scrollToElement",
406
+ };
407
+ }
408
+ }
409
+ return {
410
+ success: false,
411
+ swipes: maxSwipes,
412
+ error: `Element not found after ${maxSwipes} swipes: ${JSON.stringify(query)}`,
413
+ };
414
+ }
415
+ async longPress(x, y, durationMs = 1000, fromScreenshot = false) {
416
+ try {
417
+ const sessionId = await this.ensureSession();
418
+ let pressX = x;
419
+ let pressY = y;
420
+ if (fromScreenshot) {
421
+ const ratio = await this.getPixelRatio();
422
+ pressX = x / ratio;
423
+ pressY = y / ratio;
424
+ }
425
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/touchAndHold`, {
426
+ method: "POST",
427
+ body: JSON.stringify({
428
+ x: pressX,
429
+ y: pressY,
430
+ duration: durationMs / 1000,
431
+ }),
432
+ });
433
+ if (!res.ok) {
434
+ const text = await res.text().catch(() => "unknown");
435
+ return {
436
+ success: false,
437
+ error: `WDA touchAndHold failed: ${res.status} ${text}`,
438
+ };
439
+ }
440
+ return { success: true, details: { x: pressX, y: pressY, durationMs } };
441
+ }
442
+ catch (err) {
443
+ return {
444
+ success: false,
445
+ error: err instanceof Error ? err.message : "Unknown error during longPress",
446
+ };
447
+ }
448
+ }
449
+ async longPressElement(query, durationMs = 1000) {
450
+ const found = await this.findElement(query);
451
+ if (!found.success || !found.element?.rect) {
452
+ return {
453
+ success: false,
454
+ error: found.error ?? `No element found matching ${JSON.stringify(query)}`,
455
+ };
456
+ }
457
+ const rect = found.element.rect;
458
+ const centerX = rect.x + rect.width / 2;
459
+ const centerY = rect.y + rect.height / 2;
460
+ const res = await this.longPress(centerX, centerY, durationMs, false);
461
+ return {
462
+ ...res,
463
+ element: {
464
+ type: found.element.type ?? "unknown",
465
+ label: found.element.label ?? found.element.name ?? "",
466
+ bounds: rect,
467
+ },
468
+ };
469
+ }
470
+ async type(text, opts = {}) {
471
+ try {
472
+ const sessionId = await this.ensureSession();
473
+ const chars = Array.from(text);
474
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
475
+ method: "POST",
476
+ body: JSON.stringify({ value: chars }),
477
+ });
478
+ if (!res.ok) {
479
+ const t = await res.text().catch(() => "unknown");
480
+ return {
481
+ success: false,
482
+ error: `WDA keys failed: ${res.status} ${t}`,
483
+ };
484
+ }
485
+ if (opts.hideKeyboardAfter) {
486
+ await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
487
+ method: "POST",
488
+ body: JSON.stringify({ value: ["\n"] }),
489
+ }).catch(() => undefined);
490
+ }
491
+ return { success: true };
492
+ }
493
+ catch (err) {
494
+ return {
495
+ success: false,
496
+ error: err instanceof Error ? err.message : "Unknown error during type",
497
+ };
498
+ }
499
+ }
500
+ async clearText(query) {
501
+ try {
502
+ const found = await this.findElement(query);
503
+ if (!found.success || !found.element?.rect) {
504
+ return {
505
+ success: false,
506
+ error: found.error ?? `No element found matching ${JSON.stringify(query)}`,
507
+ };
508
+ }
509
+ const rect = found.element.rect;
510
+ const currentValue = String(found.element.value ?? "");
511
+ const tapRes = await this.tap(rect.x + rect.width / 2, rect.y + rect.height / 2, false);
512
+ if (!tapRes.success) {
513
+ return {
514
+ success: false,
515
+ error: tapRes.error ?? "Failed to focus element before clearing",
516
+ };
517
+ }
518
+ const backspaces = new Array(Math.max(1, currentValue.length)).fill("\uE003");
519
+ const sessionId = await this.ensureSession();
520
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
521
+ method: "POST",
522
+ body: JSON.stringify({ value: backspaces }),
523
+ });
524
+ if (!res.ok) {
525
+ const t = await res.text().catch(() => "unknown");
526
+ return {
527
+ success: false,
528
+ error: `WDA keys (backspace) failed: ${res.status} ${t}`,
529
+ };
530
+ }
531
+ return { success: true, details: { cleared: currentValue.length } };
532
+ }
533
+ catch (err) {
534
+ return {
535
+ success: false,
536
+ error: err instanceof Error ? err.message : "Unknown error during clearText",
537
+ };
538
+ }
539
+ }
540
+ async pressKey(key) {
541
+ try {
542
+ const sessionId = await this.ensureSession();
543
+ if (key === "lock") {
544
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/lock`, {
545
+ method: "POST",
546
+ body: JSON.stringify({}),
547
+ });
548
+ if (!res.ok) {
549
+ const t = await res.text().catch(() => "unknown");
550
+ return { success: false, error: `WDA lock failed: ${res.status} ${t}` };
551
+ }
552
+ return { success: true };
553
+ }
554
+ const name = key === "home" ? "home" : key === "volumeUp" ? "volumeup" : "volumedown";
555
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/pressButton`, {
556
+ method: "POST",
557
+ body: JSON.stringify({ name }),
558
+ });
559
+ if (!res.ok) {
560
+ const t = await res.text().catch(() => "unknown");
561
+ return {
562
+ success: false,
563
+ error: `WDA pressButton failed: ${res.status} ${t}`,
564
+ };
565
+ }
566
+ return { success: true };
567
+ }
568
+ catch (err) {
569
+ return {
570
+ success: false,
571
+ error: err instanceof Error ? err.message : "Unknown error during pressKey",
572
+ };
573
+ }
574
+ }
575
+ async unlock() {
576
+ try {
577
+ const sessionId = await this.ensureSession();
578
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/unlock`, {
579
+ method: "POST",
580
+ body: JSON.stringify({}),
581
+ });
582
+ if (!res.ok) {
583
+ const t = await res.text().catch(() => "unknown");
584
+ return { success: false, error: `WDA unlock failed: ${res.status} ${t}` };
585
+ }
586
+ return { success: true };
587
+ }
588
+ catch (err) {
589
+ return {
590
+ success: false,
591
+ error: err instanceof Error ? err.message : "Unknown error during unlock",
592
+ };
593
+ }
594
+ }
595
+ async isLocked() {
596
+ try {
597
+ const sessionId = await this.ensureSession();
598
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/locked`);
599
+ if (!res.ok)
600
+ return null;
601
+ const data = (await res.json());
602
+ return typeof data.value === "boolean" ? data.value : null;
603
+ }
604
+ catch {
605
+ return null;
606
+ }
607
+ }
608
+ async screenshot() {
609
+ try {
610
+ const sessionId = await this.ensureSession();
611
+ const res = await this.wdaFetch(`/session/${sessionId}/screenshot`);
612
+ if (!res.ok) {
613
+ const t = await res.text().catch(() => "unknown");
614
+ return { success: false, error: `WDA screenshot failed: ${res.status} ${t}` };
615
+ }
616
+ const data = (await res.json());
617
+ return { success: true, base64: data.value };
618
+ }
619
+ catch (err) {
620
+ return {
621
+ success: false,
622
+ error: err instanceof Error ? err.message : "Unknown error during screenshot",
623
+ };
624
+ }
625
+ }
626
+ async getSource() {
627
+ try {
628
+ const sessionId = await this.ensureSession();
629
+ const res = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
630
+ if (!res.ok) {
631
+ const t = await res.text().catch(() => "unknown");
632
+ return {
633
+ success: false,
634
+ error: `WDA source failed: ${res.status} ${t}`,
635
+ };
636
+ }
637
+ const data = (await res.json());
638
+ return { success: true, source: data.value };
639
+ }
640
+ catch (err) {
641
+ return {
642
+ success: false,
643
+ error: err instanceof Error ? err.message : "Unknown error during getSource",
644
+ };
645
+ }
646
+ }
647
+ async waitFor(query, opts = {}) {
648
+ const timeoutMs = opts.timeoutMs ?? 5000;
649
+ const pollIntervalMs = opts.pollIntervalMs ?? 500;
650
+ const disappear = opts.disappear ?? false;
651
+ const start = Date.now();
652
+ while (Date.now() - start < timeoutMs) {
653
+ const found = await this.findElement(query);
654
+ const matched = found.success && !!found.element;
655
+ if (!disappear && matched) {
656
+ const rect = found.element.rect;
657
+ return {
658
+ success: true,
659
+ found: true,
660
+ elapsedMs: Date.now() - start,
661
+ element: rect
662
+ ? {
663
+ type: found.element.type ?? "unknown",
664
+ label: found.element.label ?? found.element.name ?? "",
665
+ bounds: rect,
666
+ }
667
+ : undefined,
668
+ };
669
+ }
670
+ if (disappear && !matched) {
671
+ return {
672
+ success: true,
673
+ found: false,
674
+ elapsedMs: Date.now() - start,
675
+ };
676
+ }
677
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
678
+ }
679
+ return {
680
+ success: false,
681
+ found: !disappear,
682
+ elapsedMs: Date.now() - start,
683
+ error: disappear
684
+ ? `Element still visible after ${timeoutMs}ms: ${JSON.stringify(query)}`
685
+ : `Element not visible after ${timeoutMs}ms: ${JSON.stringify(query)}`,
686
+ };
687
+ }
688
+ async assertVisible(query) {
689
+ const found = await this.findElement(query);
690
+ if (found.error) {
691
+ return { success: false, visible: false, error: found.error };
692
+ }
693
+ if (found.success && found.element?.rect) {
694
+ return {
695
+ success: true,
696
+ visible: true,
697
+ element: {
698
+ type: found.element.type ?? "unknown",
699
+ label: found.element.label ?? found.element.name ?? "",
700
+ bounds: found.element.rect,
701
+ },
702
+ };
703
+ }
704
+ return { success: true, visible: false };
705
+ }
706
+ async assertNotVisible(query) {
707
+ const found = await this.findElement(query);
708
+ if (found.error) {
709
+ return { success: false, visible: false, error: found.error };
710
+ }
711
+ const visible = found.success && !!found.element;
712
+ return { success: true, visible };
713
+ }
714
+ async getOrientation() {
715
+ try {
716
+ const sessionId = await this.ensureSession();
717
+ const res = await this.wdaFetch(`/session/${sessionId}/orientation`);
718
+ if (!res.ok)
719
+ return null;
720
+ const data = (await res.json());
721
+ return data.value ?? null;
722
+ }
723
+ catch {
724
+ return null;
725
+ }
726
+ }
727
+ async setOrientation(orientation) {
728
+ try {
729
+ const sessionId = await this.ensureSession();
730
+ const res = await this.wdaFetch(`/session/${sessionId}/orientation`, {
731
+ method: "POST",
732
+ body: JSON.stringify({
733
+ orientation: orientation.toUpperCase(),
734
+ }),
735
+ });
736
+ if (!res.ok) {
737
+ const t = await res.text().catch(() => "unknown");
738
+ return {
739
+ success: false,
740
+ error: `WDA setOrientation failed: ${res.status} ${t}`,
741
+ };
742
+ }
743
+ return { success: true };
744
+ }
745
+ catch (err) {
746
+ return {
747
+ success: false,
748
+ error: err instanceof Error
749
+ ? err.message
750
+ : "Unknown error during setOrientation",
751
+ };
752
+ }
753
+ }
754
+ async getActiveAppInfo() {
755
+ try {
756
+ const sessionId = await this.ensureSession();
757
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/activeAppInfo`);
758
+ if (!res.ok) {
759
+ const t = await res.text().catch(() => "unknown");
760
+ return {
761
+ success: false,
762
+ error: `WDA activeAppInfo failed: ${res.status} ${t}`,
763
+ };
764
+ }
765
+ const data = (await res.json());
766
+ return {
767
+ success: true,
768
+ bundleId: data.value?.bundleId,
769
+ pid: data.value?.pid,
770
+ name: data.value?.name,
771
+ };
772
+ }
773
+ catch (err) {
774
+ return {
775
+ success: false,
776
+ error: err instanceof Error
777
+ ? err.message
778
+ : "Unknown error during getActiveAppInfo",
779
+ };
780
+ }
781
+ }
782
+ async activateApp(bundleId) {
783
+ try {
784
+ const sessionId = await this.ensureSession();
785
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/apps/launch`, {
786
+ method: "POST",
787
+ body: JSON.stringify({ bundleId }),
788
+ });
789
+ if (!res.ok) {
790
+ const t = await res.text().catch(() => "unknown");
791
+ return {
792
+ success: false,
793
+ error: `WDA apps/launch failed: ${res.status} ${t}`,
794
+ };
795
+ }
796
+ return { success: true };
797
+ }
798
+ catch (err) {
799
+ return {
800
+ success: false,
801
+ error: err instanceof Error
802
+ ? err.message
803
+ : "Unknown error during activateApp",
804
+ };
805
+ }
806
+ }
807
+ async openUrl(url) {
808
+ try {
809
+ const sessionId = await this.ensureSession();
810
+ const res = await this.wdaFetch(`/session/${sessionId}/url`, {
811
+ method: "POST",
812
+ body: JSON.stringify({ url }),
813
+ });
814
+ if (!res.ok) {
815
+ const t = await res.text().catch(() => "unknown");
816
+ return {
817
+ success: false,
818
+ error: `WDA openUrl failed: ${res.status} ${t}`,
819
+ };
820
+ }
821
+ return { success: true };
822
+ }
823
+ catch (err) {
824
+ return {
825
+ success: false,
826
+ error: err instanceof Error ? err.message : "Unknown error during openUrl",
827
+ };
828
+ }
829
+ }
830
+ async terminateAppViaWDA(bundleId) {
831
+ try {
832
+ const sessionId = await this.ensureSession();
833
+ const res = await this.wdaFetch(`/session/${sessionId}/wda/apps/terminate`, {
834
+ method: "POST",
835
+ body: JSON.stringify({ bundleId }),
836
+ });
837
+ if (!res.ok) {
838
+ const t = await res.text().catch(() => "unknown");
839
+ return {
840
+ success: false,
841
+ error: `WDA apps/terminate failed: ${res.status} ${t}`,
842
+ };
843
+ }
844
+ return { success: true };
845
+ }
846
+ catch (err) {
847
+ return {
848
+ success: false,
849
+ error: err instanceof Error
850
+ ? err.message
851
+ : "Unknown error during terminateAppViaWDA",
852
+ };
853
+ }
854
+ }
855
+ async pingStatus() {
856
+ try {
857
+ const res = await this.wdaFetch("/status", {}, 2000);
858
+ return res.ok;
859
+ }
860
+ catch {
861
+ return false;
862
+ }
863
+ }
280
864
  searchElements(element, query) {
281
865
  const results = [];
282
866
  if (this.elementMatches(element, query)) {
@@ -290,9 +874,6 @@ export class ScreenManager {
290
874
  }
291
875
  return results;
292
876
  }
293
- /**
294
- * Check whether a single element matches the given query.
295
- */
296
877
  elementMatches(element, query) {
297
878
  if (query.text !== undefined) {
298
879
  const searchText = query.text.toLowerCase();
@@ -317,14 +898,10 @@ export class ScreenManager {
317
898
  return false;
318
899
  }
319
900
  }
320
- // At least one query field must be specified
321
901
  return (query.text !== undefined ||
322
902
  query.accessibilityLabel !== undefined ||
323
903
  query.type !== undefined);
324
904
  }
325
- /**
326
- * Collect a summary of visible elements for debugging (when no match found).
327
- */
328
905
  collectVisibleElements(element, limit) {
329
906
  const results = [];
330
907
  if (limit <= 0)
@@ -332,7 +909,6 @@ export class ScreenManager {
332
909
  const label = String(element.label ?? element.name ?? "");
333
910
  const text = String(element.value ?? "");
334
911
  const type = String(element.type ?? "");
335
- // Only include elements that have some identifying info
336
912
  if (label || text) {
337
913
  results.push({ type, label, text });
338
914
  }
@@ -347,11 +923,9 @@ export class ScreenManager {
347
923
  }
348
924
  return results;
349
925
  }
350
- /**
351
- * Invalidate cached session and pixel ratio (e.g., after app restart).
352
- */
353
926
  reset() {
354
927
  this.sessionId = null;
355
928
  this.pixelRatio = null;
929
+ this.sessionBaseUrl = null;
356
930
  }
357
931
  }