@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.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 (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,2934 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { CommandError, executionFields } = require('./command-errors');
5
+ const { createHash, randomUUID } = require('node:crypto');
6
+ const net = require('net');
7
+ const path = require('path');
8
+ const { runExecution, checkExecution, executionSleep, withoutExecution } = require('./shared-kernel/execution-scope');
9
+ const { execFileBounded, httpRequestBounded } = require('./shared-kernel/execution-io');
10
+ const { nativeWindow, nativeNodeIdentity, nativeWindowIdentity, nativeTargetRequest, selectNativeNode, revalidateNativeNode, explicitlyHidden, pointInsideBounds, validBounds } = require('./shared-kernel/native-target');
11
+ const { selectFlutterNode, flutterNodeIdentity, flutterTargetRequest, bindFlutterAction } = require('./shared-kernel/flutter-target');
12
+ const { executeFlutterAction, settlementProof: flutterSettlementProof } = require('./shared-kernel/flutter-execution');
13
+ const { executeNativeAction, settlementProof: nativeSettlementProof } = require('./shared-kernel/native-execution');
14
+ const { executeAndroidShell } = require('./shared-kernel/android-shell-execution');
15
+ const { createUiaRuntimePort } = require('./shared-kernel/uia-runtime-port');
16
+ const { executeUiaAction, observedTarget: observedUiaTarget, snapshotIdentity: uiaSnapshotIdentity,
17
+ nodeTargetRef: uiaNodeTargetRef, bindingFromTargetRef: uiaBindingFromTargetRef } = require('./shared-kernel/uia-execution');
18
+ const { selectUiaNode } = require('./shared-kernel/uia-target');
19
+ const { schema: h5ExecutionSchema, executeH5Action, settlementProof: h5SettlementProof } = require('./shared-kernel/h5-execution');
20
+ const h5Target = require('./shared-kernel/android-h5-target');
21
+ const { isDeepStrictEqual } = require('node:util');
22
+ const { runDeviceEffect } = require('./shared-kernel/device-mutation-lease');
23
+ const { IOSBridgeProvider } = require('./ios-provider');
24
+ const { discoverAndroidSdkEndpoint } = require('./shared-kernel/android-sdk-endpoint');
25
+ const { createBridgeForward, verifyBridgeForward, removeBridgeForward } = require('./bridge-forward');
26
+ const { decodeXmlAttribute, parseXmlAttributes } = require('./shared-kernel/xml-attributes');
27
+ const {
28
+ artifactTimestamp,
29
+ defaultArtifactDirectory,
30
+ defaultArtifactPath,
31
+ pruneGeneratedArtifacts,
32
+ sanitizeArtifactExtension,
33
+ sanitizeArtifactName,
34
+ } = require('./artifact-paths');
35
+
36
+ const defaults = {
37
+ adb: process.env.ADB || 'adb',
38
+ adbTimeoutMs: Number(process.env.AI_APP_BRIDGE_ADB_TIMEOUT_MS || 15000),
39
+ serial: '',
40
+ };
41
+
42
+ function createBridgeContext(options = {}) {
43
+ return {
44
+ adb: options.adb || defaults.adb,
45
+ adbTimeoutMs: Number(options.adbTimeoutMs || defaults.adbTimeoutMs),
46
+ httpTimeoutMs: options.timeoutMs ?? 10000,
47
+ serial: options.serial || defaults.serial,
48
+ port: options.port,
49
+ explicitPort: options.port !== undefined,
50
+ packageName: options.packageName,
51
+ explicitPackageName: options.packageName !== undefined,
52
+ actionId: options.runtimeActionId ?? options.requestId,
53
+ };
54
+ }
55
+
56
+ async function executeProviderCommand(resolvedCommand, options) {
57
+ if (isIOSCommand(resolvedCommand)) {
58
+ return new IOSBridgeProvider().run(resolvedCommand, options);
59
+ }
60
+
61
+ const { isMutationCommand, executionTimeoutMs } = require('./command-registry');
62
+ return runExecution({ timeoutMs: executionTimeoutMs(resolvedCommand, options), mutation: isMutationCommand(resolvedCommand, options) },
63
+ () => runCommand(resolvedCommand, options, createBridgeContext(options)));
64
+ }
65
+
66
+ function isIOSCommand(command) {
67
+ return String(command || '').startsWith('ios-');
68
+ }
69
+
70
+ async function runCommand(command, options, ctx) {
71
+ switch (command) {
72
+ case 'device-ownership':
73
+ return require('./shared-kernel/device-ownership-recovery').deviceOwnership(options);
74
+ case 'uia-runtime':
75
+ return createUiaRuntimePort({ adb: ctx.adb, serial: ctx.serial, timeoutMs: ctx.httpTimeoutMs }).control(options.operation);
76
+ case 'forward':
77
+ return ensureForward(ctx);
78
+ case 'remove-forward':
79
+ return removeBridgeForward(ctx, adb);
80
+ case 'status':
81
+ return bridgeStatus(ctx, options);
82
+ case 'tree':
83
+ return bridgeTree(ctx, options);
84
+ case 'flutter-tree': {
85
+ const status = await bridgeGet(ctx, '/v1/status');
86
+ return status.flutter?.layout || null;
87
+ }
88
+ case 'flutter-nodes':
89
+ return flutterNodes(ctx);
90
+ case 'tap-flutter':
91
+ return flutterAction(ctx, options.selector ? { action: 'tapTarget', selector: options.selector } : flutterTapPayload(options), options);
92
+ case 'tap-flutter-text':
93
+ return tapText(ctx, requiredString(options.targetText, 'targetText'), { ...options, provider: 'flutter' });
94
+ case 'input-flutter-text': {
95
+ const result = await flutterAction(ctx, {
96
+ action: 'inputText',
97
+ text: requiredInputString(options.text, 'text'),
98
+ ...(options.selector ? { selector: options.selector } : {}),
99
+ ...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
100
+ }, options);
101
+ if (result.ok && booleanOption(options.hideKeyboard)) {
102
+ result.keyboard = await hideKeyboard(ctx, options);
103
+ }
104
+ return result;
105
+ }
106
+ case 'scroll-flutter':
107
+ return options.targetText
108
+ ? flutterAction(ctx, { action: 'scrollUntilText', text: options.targetText, maxSwipes: options.maxSwipes ?? 12, ...(options.selector ? { selector: options.selector } : {}) }, options)
109
+ : flutterAction(ctx, { action: 'scrollBy', delta: options.delta ?? 420, ...(options.selector ? { selector: options.selector } : {}) }, options);
110
+ case 'flutter-action':
111
+ return flutterAction(ctx, options.payload, options);
112
+ case 'logs':
113
+ return bridgeGet(ctx, withQuery('/v1/logs', captureQuery(options)));
114
+ case 'network':
115
+ return networkRecords(ctx, options);
116
+ case 'state':
117
+ return bridgeGet(ctx, withQuery('/v1/state', captureQuery(options)));
118
+ case 'events':
119
+ return bridgeGet(ctx, withQuery('/v1/events', captureQuery(options)));
120
+ case 'freeze-app':
121
+ return freezeApp(ctx, options);
122
+ case 'thaw-app':
123
+ return thawApp(ctx, options);
124
+ case 'h5-dom':
125
+ return h5Dom(ctx, options);
126
+ case 'h5-eval':
127
+ return h5Eval(ctx, options);
128
+ case 'h5-click':
129
+ return h5Click(ctx, options);
130
+ case 'h5-input':
131
+ return h5Input(ctx, options);
132
+ case 'h5-wait':
133
+ return h5Wait(ctx, options);
134
+ case 'h5-scroll':
135
+ return h5Scroll(ctx, options);
136
+ case 'flutter-h5-dom':
137
+ return flutterH5Dom(ctx, options);
138
+ case 'flutter-h5-eval':
139
+ return flutterH5Eval(ctx, options);
140
+ case 'flutter-h5-click':
141
+ return flutterH5Click(ctx, options);
142
+ case 'flutter-h5-input':
143
+ return flutterH5Input(ctx, options);
144
+ case 'flutter-h5-wait':
145
+ return flutterH5Wait(ctx, options);
146
+ case 'flutter-h5-scroll':
147
+ return flutterH5Scroll(ctx, options);
148
+ case 'uia-tree':
149
+ return uiaTreeCommand(ctx, options);
150
+ case 'screenshot':
151
+ return screenshot(ctx, screenshotOutputPath(options, 'ai_app_bridge_screenshot'), options);
152
+ case 'tap':
153
+ return tap(
154
+ ctx,
155
+ requiredNumber(options.tapX, 'tapX'),
156
+ requiredNumber(options.tapY, 'tapY'),
157
+ options,
158
+ );
159
+ case 'tap-text':
160
+ return tapText(ctx, requiredString(options.targetText, 'targetText'), options);
161
+ case 'tap-native':
162
+ return tapNative(ctx, options);
163
+ case 'wait-text':
164
+ return waitText(ctx, options.targetText, options);
165
+ case 'input-text':
166
+ return options.selector
167
+ ? inputNativeText(ctx, requiredInputString(options.text, 'text'), options)
168
+ : inputText(ctx, requiredInputString(options.text, 'text'), options);
169
+ case 'keyboard-state':
170
+ return keyboardState(ctx);
171
+ case 'hide-keyboard':
172
+ return hideKeyboard(ctx, options);
173
+ case 'clear-app-data':
174
+ return clearAppData(ctx, options);
175
+ case 'webview-pages':
176
+ return webviewPages(ctx, options);
177
+ case 'webview-network':
178
+ return webviewCdpCapture(ctx, { ...options, captureNetwork: true, captureConsole: true });
179
+ case 'webview-console':
180
+ return webviewCdpCapture(ctx, { ...options, captureNetwork: false, captureConsole: true });
181
+ case 'swipe':
182
+ return swipe(
183
+ ctx,
184
+ requiredNumber(options.startX, 'startX'),
185
+ requiredNumber(options.startY, 'startY'),
186
+ requiredNumber(options.endX, 'endX'),
187
+ requiredNumber(options.endY, 'endY'),
188
+ options.durationMs ?? 300,
189
+ );
190
+ case 'native-gesture':
191
+ return gestureNative(ctx, options.payload, options);
192
+ case 'keyevent':
193
+ return keyevent(ctx, options.keyCode ?? 4);
194
+ case 'logcat':
195
+ return logcat(ctx, options);
196
+ case 'permission-state':
197
+ return require('./android-permissions').readPermissionState(options);
198
+ case 'permission-grant':
199
+ return require('./android-permissions').changePermission(options, 'grant');
200
+ case 'permission-revoke':
201
+ return require('./android-permissions').changePermission(options, 'revoke');
202
+ case 'appops-set':
203
+ return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
204
+ case 'tap-uia-text':
205
+ return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
206
+ case 'tap-uia':
207
+ return tapUia(ctx, options);
208
+ case 'launch-app':
209
+ return launchApp(ctx, options);
210
+ case 'launch-activity':
211
+ return launchActivity(ctx, options);
212
+ default:
213
+ throw new Error(`unknown command: ${command}`);
214
+ }
215
+ }
216
+
217
+ async function adb(ctx, args, { binary = false, mutation = false } = {}) {
218
+ if (mutation && args[0] === 'shell') {
219
+ return executeAndroidShell({ adb: ctx.adb, serial: ctx.serial, argv: args.slice(1),
220
+ timeoutMs: ctx.adbTimeoutMs, actionId: ctx.actionId });
221
+ }
222
+ const allArgs = adbArgs(ctx, args);
223
+ const execute = () => execFileBounded(ctx.adb, allArgs, {
224
+ encoding: binary ? 'buffer' : 'utf8',
225
+ maxBuffer: 64 * 1024 * 1024,
226
+ timeoutMs: ctx.adbTimeoutMs || defaults.adbTimeoutMs,
227
+ mutation,
228
+ windowsHide: true,
229
+ });
230
+ return mutation ? runDeviceEffect({ kind: 'adb', commandSha256: createHash('sha256').update(JSON.stringify(allArgs)).digest('hex') }, execute) : execute();
231
+ }
232
+
233
+ async function adbBinaryToFile(ctx, args, outFile) {
234
+ const { stdout } = await adb(ctx, args, { binary: true });
235
+ checkExecution();
236
+ await fs.promises.mkdir(path.dirname(path.resolve(outFile)), { recursive: true });
237
+ await fs.promises.writeFile(outFile, stdout);
238
+ return path.resolve(outFile);
239
+ }
240
+
241
+ function adbArgs(ctx, args) {
242
+ const allArgs = [];
243
+ if (ctx.serial) allArgs.push('-s', ctx.serial);
244
+ allArgs.push(...args);
245
+ return allArgs;
246
+ }
247
+
248
+ async function ensureForward(ctx) {
249
+ // Resolve the device before reading its App-private endpoint. Every request observes
250
+ // the current runtime; no cached port or retry can select a previous App process.
251
+ if (!ctx.serial) ctx.serial = (await adb(ctx, ['get-serialno'])).stdout.trim();
252
+ ctx.endpoint = null;
253
+ ctx.hostPort = null;
254
+ const endpoint = await discoverAndroidSdkEndpoint(ctx, adb);
255
+ ctx.endpoint = endpoint;
256
+ const connection = { ...await createBridgeForward(ctx, endpoint, adb), endpoint };
257
+ await verifyBridgeForward(ctx, connection, adb);
258
+ ctx.hostPort = connection.hostPort;
259
+ return {
260
+ ok: true,
261
+ forward: `tcp:${connection.hostPort} -> localabstract:${endpoint.socketName}`,
262
+ hostPort: connection.hostPort,
263
+ endpoint,
264
+ reused: connection.reused,
265
+ };
266
+ }
267
+
268
+ async function bridgeStatus(ctx, options = {}) {
269
+ try {
270
+ const status = await bridgeGet(ctx, '/v1/status');
271
+ return booleanOption(options.full) ? status : compactStatus(status);
272
+ } catch (error) {
273
+ return buildBridgeFailureResult(ctx, 'status', '/v1/status', error);
274
+ }
275
+ }
276
+
277
+ function compactStatus(status) {
278
+ if (!status || typeof status !== 'object') return status;
279
+ const next = { ...status };
280
+ if (status.flutter && typeof status.flutter === 'object') {
281
+ next.flutter = compactFlutterStatus(status.flutter);
282
+ }
283
+ return next;
284
+ }
285
+
286
+ function compactFlutterStatus(flutterStatus) {
287
+ const next = { ...flutterStatus };
288
+ if (flutterStatus.layout && typeof flutterStatus.layout === 'object') {
289
+ next.layout = compactFlutterLayout(flutterStatus.layout);
290
+ }
291
+ return next;
292
+ }
293
+
294
+ function compactFlutterLayout(layout) {
295
+ return {
296
+ updatedAtMs: layout.updatedAtMs,
297
+ widgetInspector: layout.widgetInspector ? compactDiagnosticTreeNode(layout.widgetInspector) : undefined,
298
+ widgetDump: compactWidgetDump(layout.widgetDump),
299
+ semantics: layout.semantics ? compactSemantics(layout.semantics) : undefined,
300
+ operable: layout.operable ? compactFlutterOperable(layout.operable) : undefined,
301
+ };
302
+ }
303
+
304
+ function compactDiagnosticTreeNode(node) {
305
+ if (!node || typeof node !== 'object') return node;
306
+ return {
307
+ description: shortStatusString(node.description),
308
+ type: shortStatusString(node.type),
309
+ hasChildren: Boolean(node.hasChildren),
310
+ childCount: Array.isArray(node.children) ? node.children.length : undefined,
311
+ createdByLocalProject: Boolean(node.createdByLocalProject),
312
+ };
313
+ }
314
+
315
+ function compactWidgetDump(widgetDump) {
316
+ if (!widgetDump || typeof widgetDump !== 'object') return widgetDump;
317
+ return {
318
+ ok: widgetDump.ok,
319
+ error: widgetDump.error,
320
+ truncated: widgetDump.truncated,
321
+ length: widgetDump.length,
322
+ };
323
+ }
324
+
325
+ function compactSemantics(semantics) {
326
+ if (!semantics || typeof semantics !== 'object') return semantics;
327
+ return {
328
+ ok: semantics.ok,
329
+ error: semantics.error,
330
+ semanticsEnabled: semantics.semanticsEnabled,
331
+ nodeCount: semantics.nodeCount,
332
+ };
333
+ }
334
+
335
+ function compactFlutterOperable(operable) {
336
+ if (!operable || typeof operable !== 'object') return operable;
337
+ return {
338
+ ok: operable.ok,
339
+ error: operable.error,
340
+ count: operable.count,
341
+ visitedCount: operable.visitedCount,
342
+ textCount: operable.textCount,
343
+ actionCount: operable.actionCount,
344
+ sampleWidgetTypes: Array.isArray(operable.sampleWidgetTypes)
345
+ ? operable.sampleWidgetTypes.slice(0, 20)
346
+ : operable.sampleWidgetTypes,
347
+ nodes: Array.isArray(operable.nodes)
348
+ ? operable.nodes.slice(0, 12).map(compactFlutterOperableNode)
349
+ : operable.nodes,
350
+ truncated: operable.truncated,
351
+ viewport: operable.viewport,
352
+ updatedAtMs: operable.updatedAtMs,
353
+ };
354
+ }
355
+
356
+ function compactFlutterOperableNode(node) {
357
+ if (!node || typeof node !== 'object') return node;
358
+ return {
359
+ id: node.id,
360
+ widgetType: shortStatusString(node.widgetType),
361
+ text: shortStatusString(node.text),
362
+ bounds: node.bounds,
363
+ actions: node.actions,
364
+ depth: node.depth,
365
+ };
366
+ }
367
+
368
+ function shortStatusString(value, maxLength = 160) {
369
+ if (value === undefined || value === null) return value;
370
+ const text = String(value);
371
+ return text.length > maxLength ? `${text.slice(0, maxLength)}...[truncated]` : text;
372
+ }
373
+
374
+ function buildBridgeFailureResult(ctx, command, requestPath, error) {
375
+ const normalized = normalizeBridgeError(error);
376
+ return {
377
+ ok: false,
378
+ command,
379
+ requestPath,
380
+ error: normalized.code,
381
+ message: normalized.message,
382
+ dispatched: error.dispatched ?? false,
383
+ ambiguous: error.ambiguous ?? false,
384
+ packageName: ctx.packageName,
385
+ attempted: {
386
+ host: '127.0.0.1',
387
+ localPort: ctx.hostPort ?? null,
388
+ requestedLocalPort: ctx.port ?? null,
389
+ endpoint: ctx.endpoint ?? null,
390
+ url: ctx.hostPort ? bridgeUrl(ctx, requestPath) : null,
391
+ },
392
+ suggestion: normalized.suggestion,
393
+ };
394
+ }
395
+
396
+ function normalizeBridgeError(error) {
397
+ if (error instanceof CommandError) return { code: error.code, message: error.message };
398
+ const message = firstErrorLine(error);
399
+ const code = error?.code || '';
400
+ const lower = message.toLowerCase();
401
+ if (
402
+ code === 'ECONNRESET' ||
403
+ lower.includes('socket hang up') ||
404
+ lower.includes('connection reset') ||
405
+ lower.includes('http timeout')
406
+ ) {
407
+ return {
408
+ code: 'bridge_not_ready',
409
+ message,
410
+ suggestion: 'Launch the target app, wait for the debug bridge to start, then retry status.',
411
+ };
412
+ }
413
+ if (code === 'ECONNREFUSED' || lower.includes('econnrefused') || lower.includes('connection refused')) {
414
+ return {
415
+ code: 'bridge_connection_refused',
416
+ message,
417
+ suggestion: 'Confirm the target app is running and its current SDK endpoint is ready.',
418
+ };
419
+ }
420
+ if (lower.includes('adb timed out')) {
421
+ return {
422
+ code: 'adb_timeout',
423
+ message,
424
+ suggestion: 'Check the device connection. SDK endpoint discovery must finish before any HTTP request.',
425
+ };
426
+ }
427
+ if (error?.aiAppBridgeForwardMismatch) {
428
+ return {
429
+ code: 'bridge_forward_mismatch',
430
+ message,
431
+ suggestion: 'The ADB mapping is missing or belongs to another target. Inspect the forwards and observe the target again before a new action.',
432
+ };
433
+ }
434
+ if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
435
+ return {
436
+ code: 'bridge_package_mismatch',
437
+ message,
438
+ suggestion: 'The SDK endpoint or response belongs to another package. Inspect the requested package before a new operation.',
439
+ };
440
+ }
441
+ if (lower.includes('adb forward')) {
442
+ return {
443
+ code: 'bridge_forward_failed',
444
+ message,
445
+ suggestion: 'Remove stale adb forwards or pass a different --port for this target package.',
446
+ };
447
+ }
448
+ return {
449
+ code: 'bridge_request_failed',
450
+ message,
451
+ suggestion: 'Check that the device is connected, the target package is installed, and the app is foreground or recently launched.',
452
+ };
453
+ }
454
+
455
+ function firstErrorLine(error) {
456
+ return String(error?.message || error || 'unknown_error').split(/\r?\n/).find(Boolean) || 'unknown_error';
457
+ }
458
+
459
+ async function bridgeGet(ctx, requestPath) {
460
+ return bridgeRequest(ctx, async () => {
461
+ const body = await httpGet(bridgeUrl(ctx, requestPath), ctx.httpTimeoutMs);
462
+ const payload = JSON.parse(body);
463
+ verifyBridgeTargetPackage(ctx, payload, requestPath);
464
+ return payload;
465
+ });
466
+ }
467
+
468
+ async function bridgePost(ctx, requestPath, payload) {
469
+ return bridgeRequest(ctx, () => runDeviceEffect({ kind: 'sdk-http', path: requestPath,
470
+ target: { packageName: ctx.packageName, adb: ctx.adb, ...(ctx.explicitPort ? { port: ctx.port } : {}) } }, async () => {
471
+ const body = await httpPost(bridgeUrl(ctx, requestPath), payload, ctx.httpTimeoutMs);
472
+ const responsePayload = JSON.parse(body);
473
+ verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
474
+ return responsePayload;
475
+ }));
476
+ }
477
+
478
+ async function bridgeRequest(ctx, request, { ensureForward: prepare = ensureForward } = {}) {
479
+ checkExecution();
480
+ try { await prepare(ctx); }
481
+ catch (error) { error.aiAppBridgeRequestNotStarted = true; throw error; }
482
+ checkExecution();
483
+ return request();
484
+ }
485
+
486
+ function bridgeUrl(ctx, requestPath) {
487
+ if (!ctx.hostPort) throw new CommandError('bridge_not_ready', 'SDK endpoint has not been connected.');
488
+ return `http://127.0.0.1:${ctx.hostPort}${requestPath}`;
489
+ }
490
+
491
+ function verifyBridgeTargetPackage(ctx, payload, requestPath) {
492
+ if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
493
+ const responsePackageName = payload.app && typeof payload.app === 'object'
494
+ ? payload.app.packageName
495
+ : undefined;
496
+ if (!responsePackageName || responsePackageName === ctx.packageName) return;
497
+ const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
498
+ error.aiAppBridgePackageMismatch = true;
499
+ error.expectedPackageName = ctx.packageName;
500
+ error.actualPackageName = responsePackageName;
501
+ throw error;
502
+ }
503
+
504
+ function httpGet(url, timeoutMs = 10000) {
505
+ return httpRequestBounded(url, { timeoutMs });
506
+ }
507
+
508
+ function httpPost(url, payload, timeoutMs = 20000) {
509
+ return httpRequestBounded(url, { method: 'POST', payload: payload || {}, timeoutMs });
510
+ }
511
+
512
+ function captureQuery(options) {
513
+ return {
514
+ sinceId: options.sinceId,
515
+ sinceMs: options.sinceMs,
516
+ limit: options.limit,
517
+ view: options.view ?? (booleanOption(options.history) ? 'connected-history' : undefined),
518
+ runtimeEpoch: options.runtimeEpoch,
519
+ afterActionId: options.afterActionId,
520
+ factCursor: options.factCursor,
521
+ mobileFactId: options.mobileFactId,
522
+ targetKey: options.targetKey,
523
+ };
524
+ }
525
+
526
+ async function networkRecords(ctx, options = {}) {
527
+ const capture = await bridgeGet(ctx, withQuery('/v1/network', captureQuery(options)));
528
+ return shapeNetworkCapture(capture, options);
529
+ }
530
+
531
+ function shapeNetworkCapture(capture, options = {}) {
532
+ const sourceItems = Array.isArray(capture?.items) ? capture.items : [];
533
+ const filteredItems = sourceItems.filter((item) => networkRecordMatches(item, options));
534
+ const compact = booleanOption(options.compact);
535
+ const shapedItems = filteredItems.map((item) => (
536
+ compact ? compactNetworkRecord(item) : shapeNetworkRecord(item, options)
537
+ ));
538
+ const result = {
539
+ ...capture,
540
+ count: shapedItems.length,
541
+ items: shapedItems,
542
+ };
543
+ if (filteredItems.length !== sourceItems.length) {
544
+ result.sourceCount = sourceItems.length;
545
+ if (Array.isArray(capture.refs) && capture.refs.length === sourceItems.length) {
546
+ const retained = new Set(filteredItems);
547
+ result.refs = capture.refs.filter((_ref, index) => retained.has(sourceItems[index]));
548
+ }
549
+ }
550
+ const filters = networkResultOptions(options);
551
+ if (Object.keys(filters).length > 0) {
552
+ result.options = filters;
553
+ }
554
+ return result;
555
+ }
556
+
557
+ function networkRecordMatches(record, options = {}) {
558
+ const urlFilter = options.urlFilter ? String(options.urlFilter) : '';
559
+ if (urlFilter && !networkRecordUrl(record).includes(urlFilter)) {
560
+ return false;
561
+ }
562
+
563
+ const method = options.method ? String(options.method).toUpperCase() : '';
564
+ if (method && networkRecordMethod(record).toUpperCase() !== method) {
565
+ return false;
566
+ }
567
+
568
+ if (options.statusCode !== undefined && options.statusCode !== null && options.statusCode !== '') {
569
+ const expectedStatus = Number(options.statusCode);
570
+ if (!Number.isFinite(expectedStatus) || Number(networkRecordStatus(record)) !== expectedStatus) {
571
+ return false;
572
+ }
573
+ }
574
+
575
+ return true;
576
+ }
577
+
578
+ function shapeNetworkRecord(record, options = {}) {
579
+ const shaped = { ...record };
580
+ const noBodies = booleanOption(options.noBodies);
581
+ for (const key of ['requestBody', 'responseBody']) {
582
+ if (!Object.prototype.hasOwnProperty.call(shaped, key)) {
583
+ continue;
584
+ }
585
+ if (noBodies) {
586
+ shaped[`${key}Omitted`] = true;
587
+ delete shaped[key];
588
+ continue;
589
+ }
590
+ const bodyMaxBytes = positiveNumber(options.bodyMaxBytes);
591
+ if (bodyMaxBytes !== null) {
592
+ shaped[key] = truncateString(shaped[key], bodyMaxBytes);
593
+ }
594
+ }
595
+ return shaped;
596
+ }
597
+
598
+ function compactNetworkRecord(record) {
599
+ const result = {
600
+ id: record?.id,
601
+ timestampMs: record?.timestampMs,
602
+ source: record?.source,
603
+ method: networkRecordMethod(record),
604
+ url: networkRecordUrl(record),
605
+ statusCode: networkRecordStatus(record),
606
+ durationMs: record?.durationMs,
607
+ error: record?.error || undefined,
608
+ redacted: record?.redacted,
609
+ };
610
+ const contentType = networkRecordContentType(record);
611
+ if (contentType) {
612
+ result.contentType = contentType;
613
+ }
614
+ if (Object.prototype.hasOwnProperty.call(record || {}, 'requestBody')) {
615
+ result.requestBodyBytes = bodyByteLength(record.requestBody);
616
+ }
617
+ if (Object.prototype.hasOwnProperty.call(record || {}, 'responseBody')) {
618
+ result.responseBodyBytes = bodyByteLength(record.responseBody);
619
+ }
620
+ return pruneUndefined(result);
621
+ }
622
+
623
+ function networkResultOptions(options = {}) {
624
+ const result = {};
625
+ if (booleanOption(options.compact)) result.compact = true;
626
+ if (options.urlFilter) result.urlFilter = String(options.urlFilter);
627
+ if (options.method) result.method = String(options.method).toUpperCase();
628
+ if (options.statusCode !== undefined && options.statusCode !== null && options.statusCode !== '') {
629
+ result.statusCode = Number(options.statusCode);
630
+ }
631
+ if (booleanOption(options.noBodies)) result.noBodies = true;
632
+ if (positiveNumber(options.bodyMaxBytes) !== null) result.bodyMaxBytes = positiveNumber(options.bodyMaxBytes);
633
+ return result;
634
+ }
635
+
636
+ function networkRecordUrl(record) {
637
+ return String(record?.url || record?.requestUrl || record?.responseUrl || record?.response?.url || '');
638
+ }
639
+
640
+ function networkRecordMethod(record) {
641
+ return String(record?.method || record?.requestMethod || record?.request?.method || '');
642
+ }
643
+
644
+ function networkRecordStatus(record) {
645
+ return record?.statusCode ?? record?.responseStatusCode ?? record?.status ?? record?.response?.statusCode;
646
+ }
647
+
648
+ function networkRecordContentType(record) {
649
+ return headerValue(record?.responseHeaders, 'content-type') ||
650
+ headerValue(record?.requestHeaders, 'content-type') ||
651
+ '';
652
+ }
653
+
654
+ function headerValue(headers, key) {
655
+ if (!headers || typeof headers !== 'object') {
656
+ return '';
657
+ }
658
+ const expected = key.toLowerCase();
659
+ for (const [name, value] of Object.entries(headers)) {
660
+ if (String(name).toLowerCase() === expected) {
661
+ return String(value);
662
+ }
663
+ }
664
+ return '';
665
+ }
666
+
667
+ function bodyByteLength(value) {
668
+ if (value === undefined || value === null) {
669
+ return 0;
670
+ }
671
+ return Buffer.byteLength(String(value), 'utf8');
672
+ }
673
+
674
+ function positiveNumber(value) {
675
+ const number = Number(value);
676
+ return Number.isFinite(number) && number >= 0 ? number : null;
677
+ }
678
+
679
+ function pruneUndefined(value) {
680
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
681
+ }
682
+
683
+ function withQuery(requestPath, query) {
684
+ const params = new URLSearchParams();
685
+ for (const [key, value] of Object.entries(query || {})) {
686
+ if (value === undefined || value === null || value === '' || value === false) continue;
687
+ params.set(key, String(value));
688
+ }
689
+ const queryString = params.toString();
690
+ return queryString ? `${requestPath}?${queryString}` : requestPath;
691
+ }
692
+
693
+ async function webviewPages(ctx, options) {
694
+ const setup = await setupWebViewDevTools(ctx, options, { selectPage: false });
695
+ const keepForward = booleanOption(options.keepForward);
696
+ if (!keepForward) {
697
+ await setup.cleanup();
698
+ }
699
+ return {
700
+ ok: true,
701
+ transport: 'webview-devtools-cdp',
702
+ packageName: ctx.packageName,
703
+ sockets: setup.sockets,
704
+ selectedSocket: setup.socket,
705
+ packagePids: setup.packagePids,
706
+ forward: {
707
+ localPort: setup.localPort,
708
+ socketName: setup.socket.name,
709
+ active: keepForward,
710
+ },
711
+ pages: setup.pages,
712
+ pageCount: setup.pages.length,
713
+ selectionWarning: setup.selectionWarning || null,
714
+ };
715
+ }
716
+
717
+ async function webviewCdpCapture(ctx, options) {
718
+ const setup = await setupWebViewDevTools(ctx, options, { selectPage: true });
719
+ const durationMs = Math.max(0, Number(options.durationMs || 3000));
720
+ const captureNetwork = options.captureNetwork !== false;
721
+ const captureConsole = options.captureConsole !== false;
722
+ const includeResponseBody = booleanOption(options.includeResponseBody);
723
+ const maxEvents = Number(options.maxEvents || 200);
724
+ const bodyMaxBytes = Number(options.bodyMaxBytes || 64 * 1024);
725
+ const urlFilter = options.urlFilter || '';
726
+ const events = [];
727
+ const requests = new Map();
728
+ const consoleEvents = [];
729
+ let scriptResult = null;
730
+ let cdp = null;
731
+
732
+ const handleEvent = (event) => {
733
+ if (events.length < maxEvents) {
734
+ events.push({
735
+ method: event.method,
736
+ params: summarizeCdpParams(event.method, event.params),
737
+ });
738
+ }
739
+ if (captureNetwork && event.method === 'Network.requestWillBeSent') {
740
+ const request = event.params?.request || {};
741
+ if (urlFilter && !String(request.url || '').includes(urlFilter)) return;
742
+ const entry = requests.get(event.params.requestId) || {};
743
+ requests.set(event.params.requestId, {
744
+ ...entry,
745
+ requestId: event.params.requestId,
746
+ loaderId: event.params.loaderId,
747
+ documentURL: event.params.documentURL,
748
+ type: event.params.type,
749
+ timestamp: event.params.timestamp,
750
+ wallTime: event.params.wallTime,
751
+ method: request.method,
752
+ url: request.url,
753
+ requestHeaders: request.headers || {},
754
+ requestPostData: truncateString(request.postData || '', bodyMaxBytes),
755
+ });
756
+ return;
757
+ }
758
+ if (captureNetwork && event.method === 'Network.responseReceived') {
759
+ const response = event.params?.response || {};
760
+ if (urlFilter && !String(response.url || '').includes(urlFilter)) return;
761
+ const entry = requests.get(event.params.requestId) || {};
762
+ requests.set(event.params.requestId, {
763
+ ...entry,
764
+ requestId: event.params.requestId,
765
+ type: event.params.type || entry.type,
766
+ status: response.status,
767
+ statusText: response.statusText,
768
+ responseUrl: response.url,
769
+ mimeType: response.mimeType,
770
+ protocol: response.protocol,
771
+ remoteIPAddress: response.remoteIPAddress,
772
+ remotePort: response.remotePort,
773
+ responseHeaders: response.headers || {},
774
+ });
775
+ return;
776
+ }
777
+ if (captureNetwork && event.method === 'Network.responseReceivedExtraInfo') {
778
+ const entry = requests.get(event.params.requestId) || {};
779
+ if (urlFilter && !String(entry.url || entry.responseUrl || '').includes(urlFilter)) return;
780
+ requests.set(event.params.requestId, {
781
+ ...entry,
782
+ requestId: event.params.requestId,
783
+ status: entry.status ?? event.params.statusCode,
784
+ responseHeaders: {
785
+ ...(entry.responseHeaders || {}),
786
+ ...(event.params.headers || {}),
787
+ },
788
+ responseHeadersText: event.params.headersText,
789
+ resourceIPAddressSpace: event.params.resourceIPAddressSpace,
790
+ });
791
+ return;
792
+ }
793
+ if (captureNetwork && event.method === 'Network.loadingFinished') {
794
+ const entry = requests.get(event.params.requestId) || {};
795
+ requests.set(event.params.requestId, {
796
+ ...entry,
797
+ requestId: event.params.requestId,
798
+ encodedDataLength: event.params.encodedDataLength,
799
+ finished: true,
800
+ });
801
+ return;
802
+ }
803
+ if (captureNetwork && event.method === 'Network.loadingFailed') {
804
+ const entry = requests.get(event.params.requestId) || {};
805
+ requests.set(event.params.requestId, {
806
+ ...entry,
807
+ requestId: event.params.requestId,
808
+ failed: true,
809
+ errorText: event.params.errorText,
810
+ canceled: event.params.canceled,
811
+ blockedReason: event.params.blockedReason,
812
+ corsErrorStatus: event.params.corsErrorStatus,
813
+ });
814
+ return;
815
+ }
816
+ if (captureConsole && event.method === 'Runtime.consoleAPICalled') {
817
+ consoleEvents.push({
818
+ type: event.params?.type,
819
+ timestamp: event.params?.timestamp,
820
+ args: (event.params?.args || []).map(cdpRemoteValue),
821
+ stackTrace: event.params?.stackTrace || null,
822
+ });
823
+ return;
824
+ }
825
+ if (captureConsole && event.method === 'Log.entryAdded') {
826
+ const entry = event.params?.entry || {};
827
+ consoleEvents.push({
828
+ source: entry.source,
829
+ level: entry.level,
830
+ text: entry.text,
831
+ url: entry.url,
832
+ lineNumber: entry.lineNumber,
833
+ timestamp: entry.timestamp,
834
+ });
835
+ }
836
+ };
837
+
838
+ try {
839
+ cdp = await CdpSession.open(setup.page.webSocketDebuggerUrl);
840
+ cdp.onEvent(handleEvent);
841
+ if (captureNetwork) {
842
+ await cdp.send('Network.enable');
843
+ }
844
+ if (captureConsole) {
845
+ await cdp.send('Runtime.enable');
846
+ await cdp.send('Log.enable').catch(() => null);
847
+ }
848
+ if (options.script) {
849
+ scriptResult = await runDeviceEffect({ kind: 'android-cdp', actionId: ctx.actionId,
850
+ target: { serial: ctx.serial, packageName: ctx.packageName, pageId: setup.page.id } }, () => cdp.send('Runtime.evaluate', {
851
+ expression: String(options.script),
852
+ awaitPromise: true,
853
+ returnByValue: true,
854
+ }));
855
+ }
856
+ await sleep(durationMs);
857
+ if (includeResponseBody && captureNetwork) {
858
+ for (const entry of requests.values()) {
859
+ if (entry.status === undefined || entry.failed) continue;
860
+ try {
861
+ const body = await cdp.send('Network.getResponseBody', { requestId: entry.requestId }, 2500);
862
+ entry.responseBody = truncateString(body.body || '', bodyMaxBytes);
863
+ entry.base64Encoded = Boolean(body.base64Encoded);
864
+ } catch (error) {
865
+ entry.responseBodyError = firstErrorLine(error);
866
+ }
867
+ }
868
+ }
869
+ } finally {
870
+ if (cdp) cdp.close();
871
+ await setup.cleanup();
872
+ }
873
+
874
+ const requestItems = Array.from(requests.values());
875
+ return {
876
+ ok: true,
877
+ transport: 'webview-devtools-cdp',
878
+ packageName: ctx.packageName,
879
+ socket: setup.socket,
880
+ packagePids: setup.packagePids,
881
+ page: setup.page,
882
+ durationMs,
883
+ captureNetwork,
884
+ captureConsole,
885
+ scriptResult: normalizeRuntimeEvaluateResult(scriptResult),
886
+ counts: {
887
+ events: events.length,
888
+ requests: requestItems.length,
889
+ console: consoleEvents.length,
890
+ },
891
+ requests: requestItems,
892
+ console: consoleEvents,
893
+ events,
894
+ selectionWarning: setup.selectionWarning || null,
895
+ };
896
+ }
897
+
898
+ async function setupWebViewDevTools(ctx, options, behavior = {}) {
899
+ const packagePids = await packagePidsFor(ctx);
900
+ const procNetUnix = (await adb(ctx, ['shell', 'cat', '/proc/net/unix'])).stdout;
901
+ const sockets = parseWebViewDevToolsSockets(procNetUnix, packagePids);
902
+ if (sockets.length === 0) {
903
+ throw new Error('no WebView DevTools socket found; make sure the app is running and WebView debugging is enabled');
904
+ }
905
+ const choice = chooseWebViewDevToolsSocket(sockets, options, packagePids);
906
+ if (!choice.socket) {
907
+ throw new Error(choice.error || 'no matching WebView DevTools socket found');
908
+ }
909
+ const localPort = await resolveWebViewDevToolsPort(ctx, options);
910
+ await removeAdbForwardIfPresent(ctx, localPort);
911
+ await adb(ctx, ['forward', `tcp:${localPort}`, `localabstract:${choice.socket.name}`]);
912
+ let pages = [];
913
+ try {
914
+ pages = JSON.parse(await httpGet(`http://127.0.0.1:${localPort}/json`));
915
+ if (!Array.isArray(pages)) pages = [];
916
+ pages = pages.map((page) => normalizeCdpPage(page, localPort));
917
+ } catch (error) {
918
+ await removeAdbForwardIfPresent(ctx, localPort);
919
+ throw error;
920
+ }
921
+ const page = behavior.selectPage === false ? null : chooseWebViewPage(pages, options);
922
+ if (behavior.selectPage !== false && !page) {
923
+ await removeAdbForwardIfPresent(ctx, localPort);
924
+ throw new Error('no attachable WebView CDP page found');
925
+ }
926
+ return {
927
+ sockets,
928
+ socket: choice.socket,
929
+ packagePids,
930
+ selectionWarning: choice.warning,
931
+ localPort,
932
+ pages,
933
+ page,
934
+ cleanup: () => removeAdbForwardIfPresent(ctx, localPort),
935
+ };
936
+ }
937
+
938
+ async function packagePidsFor(ctx) {
939
+ if (!ctx.packageName) return [];
940
+ try {
941
+ const result = await adb(ctx, ['shell', 'pidof', ctx.packageName]);
942
+ return result.stdout.split(/\s+/).map((value) => value.trim()).filter((value) => /^\d+$/.test(value));
943
+ } catch (_) {
944
+ return [];
945
+ }
946
+ }
947
+
948
+ async function freezeApp(ctx, options = {}) {
949
+ return signalAppProcesses(ctx, options, 'SIGSTOP', 'freeze-app');
950
+ }
951
+
952
+ async function thawApp(ctx, options = {}) {
953
+ return signalAppProcesses(ctx, options, 'SIGCONT', 'thaw-app');
954
+ }
955
+
956
+ async function signalAppProcesses(ctx, options, signal, action) {
957
+ const pids = await targetAppPids(ctx, options);
958
+ if (pids.length === 0) {
959
+ return {
960
+ ok: false,
961
+ packageName: ctx.packageName,
962
+ action,
963
+ signal,
964
+ error: 'app_process_not_found',
965
+ };
966
+ }
967
+
968
+ const results = [];
969
+ for (const pid of pids) {
970
+ try {
971
+ const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'kill', `-${signal}`, pid], { mutation: true });
972
+ results.push({
973
+ pid,
974
+ ok: true,
975
+ stdout: result.stdout.trim(),
976
+ stderr: result.stderr.trim(),
977
+ ...executionFields(result),
978
+ });
979
+ } catch (error) {
980
+ results.push({
981
+ pid,
982
+ ok: false,
983
+ error: firstErrorLine(error),
984
+ ...executionFields(error),
985
+ });
986
+ if (error.settled === false) break;
987
+ }
988
+ }
989
+
990
+ const failed = results.filter((item) => !item.ok);
991
+ return {
992
+ ok: failed.length === 0,
993
+ packageName: ctx.packageName,
994
+ action,
995
+ signal,
996
+ pids,
997
+ results,
998
+ executionReceipts: results.flatMap(result => result.executionReceipt ? [result.executionReceipt] : []),
999
+ ...(failed.length > 0 ? { error: 'signal_failed' } : {}),
1000
+ };
1001
+ }
1002
+
1003
+ async function targetAppPids(ctx, options = {}) {
1004
+ if (options.pid && options.pid !== true && options.pid !== 'current') {
1005
+ const explicitPid = String(options.pid).trim();
1006
+ return /^\d+$/.test(explicitPid) ? [explicitPid] : [];
1007
+ }
1008
+ const psPids = await packagePidsFromPs(ctx);
1009
+ if (psPids.length > 0) return psPids;
1010
+ return packagePidsFor(ctx);
1011
+ }
1012
+
1013
+ async function packagePidsFromPs(ctx) {
1014
+ if (!ctx.packageName) return [];
1015
+ try {
1016
+ const result = await adb(ctx, ['shell', 'ps', '-A', '-o', 'PID,NAME']);
1017
+ return parsePackagePidsFromPs(result.stdout, ctx.packageName);
1018
+ } catch (_) {
1019
+ return [];
1020
+ }
1021
+ }
1022
+
1023
+ function parsePackagePidsFromPs(stdout, packageName) {
1024
+ const pids = [];
1025
+ const seen = new Set();
1026
+ for (const line of String(stdout || '').split(/\r?\n/)) {
1027
+ const trimmed = line.trim();
1028
+ if (!trimmed || /^PID\s+NAME$/i.test(trimmed)) continue;
1029
+ const match = /^(\d+)\s+(\S+)$/.exec(trimmed);
1030
+ if (!match) continue;
1031
+ const pid = match[1];
1032
+ const processName = match[2];
1033
+ if (processName !== packageName && !processName.startsWith(`${packageName}:`)) continue;
1034
+ if (seen.has(pid)) continue;
1035
+ seen.add(pid);
1036
+ pids.push(pid);
1037
+ }
1038
+ return pids;
1039
+ }
1040
+
1041
+ function parseWebViewDevToolsSockets(procNetUnix, packagePids = []) {
1042
+ const packagePidSet = new Set(packagePids.map(String));
1043
+ const sockets = [];
1044
+ const seen = new Set();
1045
+ const lines = String(procNetUnix || '').split(/\r?\n/);
1046
+ for (const line of lines) {
1047
+ const matches = line.matchAll(/(?:^|\s|@)(webview_devtools_remote(?:_[^\s@]+)?)/g);
1048
+ for (const match of matches) {
1049
+ const name = match[1];
1050
+ if (!name || seen.has(name)) continue;
1051
+ seen.add(name);
1052
+ const pidMatch = /_(\d+)$/.exec(name);
1053
+ const pid = pidMatch ? pidMatch[1] : null;
1054
+ sockets.push({
1055
+ name,
1056
+ rawName: match[0].trim(),
1057
+ pid,
1058
+ packageMatch: Boolean(pid && packagePidSet.has(pid)),
1059
+ line: line.trim(),
1060
+ });
1061
+ }
1062
+ }
1063
+ return sockets;
1064
+ }
1065
+
1066
+ function chooseWebViewDevToolsSocket(sockets, options = {}, packagePids = []) {
1067
+ if (options.socketName) {
1068
+ const requested = String(options.socketName).replace(/^@/, '');
1069
+ const socket = sockets.find((item) => item.name === requested);
1070
+ return socket ? { socket } : { error: `requested WebView DevTools socket not found: ${requested}` };
1071
+ }
1072
+ for (const pid of packagePids.map(String)) {
1073
+ const socket = sockets.find((item) => item.pid === pid);
1074
+ if (socket) return { socket };
1075
+ }
1076
+ const matched = sockets.filter((item) => item.packageMatch);
1077
+ if (matched.length > 0) return { socket: matched[0] };
1078
+ if (sockets.length === 1) return { socket: sockets[0] };
1079
+ return {
1080
+ socket: sockets[0],
1081
+ warning: `multiple WebView DevTools sockets found and none matched ${packagePids.join(',') || 'the target package pid'}; selected ${sockets[0].name}`,
1082
+ };
1083
+ }
1084
+
1085
+ function chooseWebViewPage(pages, options = {}) {
1086
+ if (!Array.isArray(pages) || pages.length === 0) return null;
1087
+ if (options.targetId) {
1088
+ const targetId = String(options.targetId);
1089
+ const byId = pages.find((page) => page.id === targetId);
1090
+ if (byId) return byId;
1091
+ }
1092
+ if (options.pageUrlFilter) {
1093
+ const filter = String(options.pageUrlFilter);
1094
+ const byUrl = pages.find((page) => String(page.url || '').includes(filter));
1095
+ if (byUrl) return byUrl;
1096
+ }
1097
+ return pages.find((page) => page.webSocketDebuggerUrl && page.type === 'page') ||
1098
+ pages.find((page) => page.webSocketDebuggerUrl) ||
1099
+ null;
1100
+ }
1101
+
1102
+ function normalizeCdpPage(page, localPort) {
1103
+ const normalized = {
1104
+ id: page.id,
1105
+ type: page.type,
1106
+ title: page.title,
1107
+ url: page.url,
1108
+ description: page.description,
1109
+ webSocketDebuggerUrl: page.webSocketDebuggerUrl,
1110
+ };
1111
+ if (normalized.webSocketDebuggerUrl) {
1112
+ normalized.webSocketDebuggerUrl = normalized.webSocketDebuggerUrl.replace(
1113
+ /^ws:\/\/(?:\[::\]|localhost|127\.0\.0\.1):\d+/,
1114
+ `ws://127.0.0.1:${localPort}`,
1115
+ );
1116
+ }
1117
+ return normalized;
1118
+ }
1119
+
1120
+ async function resolveWebViewDevToolsPort(ctx, options) {
1121
+ const explicit = Number(options.webviewPort || options.devtoolsPort || options.cdpPort || 0);
1122
+ if (Number.isInteger(explicit) && explicit > 0) return explicit;
1123
+ const start = 9222;
1124
+ for (let port = start; port < start + 100; port += 1) {
1125
+ await removeAdbForwardIfPresent(ctx, port);
1126
+ if (await isLocalPortAvailable(port)) return port;
1127
+ }
1128
+ throw new Error('no available local port for WebView DevTools forwarding');
1129
+ }
1130
+
1131
+ function isLocalPortAvailable(port) {
1132
+ return new Promise((resolve) => {
1133
+ const server = net.createServer();
1134
+ server.once('error', () => resolve(false));
1135
+ server.once('listening', () => {
1136
+ server.close(() => resolve(true));
1137
+ });
1138
+ server.listen(port, '127.0.0.1');
1139
+ });
1140
+ }
1141
+
1142
+ async function removeAdbForwardIfPresent(ctx, port) {
1143
+ try {
1144
+ await adb(ctx, ['forward', '--remove', `tcp:${port}`], { mutation: true });
1145
+ } catch (_) {
1146
+ // A missing forward is the common case.
1147
+ }
1148
+ }
1149
+
1150
+ class CdpSession {
1151
+ constructor(socket) {
1152
+ this.socket = socket;
1153
+ this.nextId = 1;
1154
+ this.pending = new Map();
1155
+ this.eventHandlers = [];
1156
+ }
1157
+
1158
+ static async open(url) {
1159
+ const WebSocketCtor = webSocketConstructor();
1160
+ const socket = new WebSocketCtor(url);
1161
+ await waitForWebSocketOpen(socket, url);
1162
+ const session = new CdpSession(socket);
1163
+ addWebSocketMessageHandler(socket, (data) => {
1164
+ webSocketDataToText(data)
1165
+ .then((text) => session.handleMessage(text))
1166
+ .catch(() => null);
1167
+ });
1168
+ addWebSocketCloseHandler(socket, () => session.rejectAll(new Error('CDP WebSocket closed')));
1169
+ return session;
1170
+ }
1171
+
1172
+ onEvent(handler) {
1173
+ this.eventHandlers.push(handler);
1174
+ }
1175
+
1176
+ send(method, params = {}, timeoutMs = 5000) {
1177
+ const id = this.nextId;
1178
+ this.nextId += 1;
1179
+ const payload = JSON.stringify({ id, method, params });
1180
+ return new Promise((resolve, reject) => {
1181
+ const timer = setTimeout(() => {
1182
+ this.pending.delete(id);
1183
+ reject(new Error(`CDP timeout: ${method}`));
1184
+ }, timeoutMs);
1185
+ this.pending.set(id, { resolve, reject, timer, method });
1186
+ this.socket.send(payload);
1187
+ });
1188
+ }
1189
+
1190
+ handleMessage(text) {
1191
+ const message = JSON.parse(text);
1192
+ if (message.id !== undefined) {
1193
+ const pending = this.pending.get(message.id);
1194
+ if (!pending) return;
1195
+ clearTimeout(pending.timer);
1196
+ this.pending.delete(message.id);
1197
+ if (message.error) {
1198
+ pending.reject(new Error(`CDP ${pending.method} failed: ${message.error.message || JSON.stringify(message.error)}`));
1199
+ } else {
1200
+ pending.resolve(message.result || {});
1201
+ }
1202
+ return;
1203
+ }
1204
+ if (message.method) {
1205
+ for (const handler of this.eventHandlers) {
1206
+ handler(message);
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ rejectAll(error) {
1212
+ for (const [id, pending] of this.pending.entries()) {
1213
+ clearTimeout(pending.timer);
1214
+ pending.reject(error);
1215
+ this.pending.delete(id);
1216
+ }
1217
+ }
1218
+
1219
+ close() {
1220
+ try {
1221
+ this.socket.close();
1222
+ } catch (_) {
1223
+ // Ignore close races.
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ function webSocketConstructor() {
1229
+ if (typeof globalThis.WebSocket === 'function') return globalThis.WebSocket;
1230
+ try {
1231
+ return require('ws');
1232
+ } catch (_) {
1233
+ throw new Error('WebView CDP capture requires Node.js with WebSocket support or the ws package');
1234
+ }
1235
+ }
1236
+
1237
+ function waitForWebSocketOpen(socket, url) {
1238
+ return new Promise((resolve, reject) => {
1239
+ const timer = setTimeout(() => reject(new Error(`CDP WebSocket open timeout: ${url}`)), 10000);
1240
+ const done = (error) => {
1241
+ clearTimeout(timer);
1242
+ if (error) reject(error);
1243
+ else resolve();
1244
+ };
1245
+ if (typeof socket.addEventListener === 'function') {
1246
+ socket.addEventListener('open', () => done(), { once: true });
1247
+ socket.addEventListener('error', () => done(new Error(`CDP WebSocket error: ${url}`)), { once: true });
1248
+ return;
1249
+ }
1250
+ socket.once('open', () => done());
1251
+ socket.once('error', (error) => done(error));
1252
+ });
1253
+ }
1254
+
1255
+ function addWebSocketMessageHandler(socket, handler) {
1256
+ if (typeof socket.addEventListener === 'function') {
1257
+ socket.addEventListener('message', (event) => handler(event.data));
1258
+ return;
1259
+ }
1260
+ socket.on('message', handler);
1261
+ }
1262
+
1263
+ function addWebSocketCloseHandler(socket, handler) {
1264
+ if (typeof socket.addEventListener === 'function') {
1265
+ socket.addEventListener('close', handler);
1266
+ return;
1267
+ }
1268
+ socket.on('close', handler);
1269
+ }
1270
+
1271
+ async function webSocketDataToText(data) {
1272
+ if (typeof data === 'string') return data;
1273
+ if (Buffer.isBuffer(data)) return data.toString('utf8');
1274
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8');
1275
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
1276
+ if (data && typeof data.text === 'function') return data.text();
1277
+ return String(data);
1278
+ }
1279
+
1280
+ function cdpRemoteValue(value) {
1281
+ if (!value || typeof value !== 'object') return value;
1282
+ if (Object.prototype.hasOwnProperty.call(value, 'value')) return value.value;
1283
+ if (Object.prototype.hasOwnProperty.call(value, 'unserializableValue')) return value.unserializableValue;
1284
+ return value.description || value.type || null;
1285
+ }
1286
+
1287
+ function normalizeRuntimeEvaluateResult(result) {
1288
+ if (!result) return null;
1289
+ return {
1290
+ result: cdpRemoteValue(result.result),
1291
+ exceptionDetails: result.exceptionDetails || null,
1292
+ };
1293
+ }
1294
+
1295
+ function summarizeCdpParams(method, params) {
1296
+ if (!params || typeof params !== 'object') return params;
1297
+ if (method === 'Network.requestWillBeSent') {
1298
+ return {
1299
+ requestId: params.requestId,
1300
+ type: params.type,
1301
+ documentURL: params.documentURL,
1302
+ request: {
1303
+ method: params.request?.method,
1304
+ url: params.request?.url,
1305
+ },
1306
+ };
1307
+ }
1308
+ if (method === 'Network.responseReceived') {
1309
+ return {
1310
+ requestId: params.requestId,
1311
+ type: params.type,
1312
+ response: {
1313
+ url: params.response?.url,
1314
+ status: params.response?.status,
1315
+ mimeType: params.response?.mimeType,
1316
+ },
1317
+ };
1318
+ }
1319
+ if (method === 'Network.responseReceivedExtraInfo') {
1320
+ return {
1321
+ requestId: params.requestId,
1322
+ statusCode: params.statusCode,
1323
+ resourceIPAddressSpace: params.resourceIPAddressSpace,
1324
+ };
1325
+ }
1326
+ if (method === 'Network.loadingFinished' || method === 'Network.loadingFailed') {
1327
+ return {
1328
+ requestId: params.requestId,
1329
+ encodedDataLength: params.encodedDataLength,
1330
+ errorText: params.errorText,
1331
+ blockedReason: params.blockedReason,
1332
+ };
1333
+ }
1334
+ if (method === 'Runtime.consoleAPICalled') {
1335
+ return {
1336
+ type: params.type,
1337
+ args: (params.args || []).map(cdpRemoteValue),
1338
+ };
1339
+ }
1340
+ if (method === 'Log.entryAdded') {
1341
+ return params.entry || {};
1342
+ }
1343
+ return params;
1344
+ }
1345
+
1346
+ function truncateString(value, maxBytes) {
1347
+ const text = String(value || '');
1348
+ if (Buffer.byteLength(text, 'utf8') <= maxBytes) return text;
1349
+ return `${Buffer.from(text, 'utf8').subarray(0, maxBytes).toString('utf8')}...[truncated]`;
1350
+ }
1351
+
1352
+ async function uiaTree(ctx) {
1353
+ return uiaTreeOnce(ctx);
1354
+ }
1355
+
1356
+ async function uiaTreeOnce(ctx) {
1357
+ const snapshot = await createUiaRuntimePort({ adb: ctx.adb, serial: ctx.serial, timeoutMs: ctx.httpTimeoutMs }).observe();
1358
+ return snapshot.xml;
1359
+ }
1360
+
1361
+ async function uiaTap(ctx, binding, options = {}) {
1362
+ return executeUiaAction({ adb: ctx.adb, serial: ctx.serial, binding,
1363
+ actionId: options.runtimeActionId ?? ctx.actionId, timeoutMs: ctx.httpTimeoutMs });
1364
+ }
1365
+
1366
+ async function bridgeTree(ctx, options = {}) {
1367
+ const tree = await bridgeGet(ctx, '/v1/view/tree');
1368
+ return wantsCompactTree(options) ? compactBridgeTree(tree, options) : tree;
1369
+ }
1370
+
1371
+ async function uiaTreeCommand(ctx, options = {}) {
1372
+ const xml = await uiaTree(ctx);
1373
+ return wantsCompactTree(options) ? compactUiaTree(xml, options) : xml;
1374
+ }
1375
+
1376
+ function wantsCompactTree(options = {}) {
1377
+ return Boolean(
1378
+ booleanOption(options.compact) ||
1379
+ options.textFilter ||
1380
+ options.resourceIdFilter ||
1381
+ options.classFilter ||
1382
+ options.visibleOnly ||
1383
+ options.maxNodes ||
1384
+ options.maxDepth,
1385
+ );
1386
+ }
1387
+
1388
+ function compactTreeOptions(options = {}) {
1389
+ return {
1390
+ textFilter: normalizeFilter(options.textFilter || options.targetText),
1391
+ resourceIdFilter: normalizeFilter(options.resourceIdFilter || options.resourceFilter),
1392
+ classFilter: normalizeFilter(options.classFilter),
1393
+ visibleOnly: booleanOption(options.visibleOnly),
1394
+ maxNodes: boundedInteger(options.maxNodes, 80, 1, 1000),
1395
+ maxDepth: boundedInteger(options.maxDepth, Number.POSITIVE_INFINITY, 0, 200),
1396
+ };
1397
+ }
1398
+
1399
+ function compactBridgeTree(tree, options = {}) {
1400
+ const compactOptions = compactTreeOptions(options);
1401
+ const result = {
1402
+ ok: true,
1403
+ source: 'bridge-tree',
1404
+ compact: true,
1405
+ activity: tree?.activity || null,
1406
+ windowCount: Array.isArray(tree?.windows) ? tree.windows.length : undefined,
1407
+ originalNodeCount: Number.isFinite(Number(tree?.nodeCount)) ? Number(tree.nodeCount) : undefined,
1408
+ options: compactTreeResultOptions(compactOptions),
1409
+ nodes: [],
1410
+ scannedNodes: 0,
1411
+ matchedNodes: 0,
1412
+ truncated: false,
1413
+ updatedAtMs: tree?.updatedAtMs || Date.now(),
1414
+ };
1415
+
1416
+ const roots = [];
1417
+ if (Array.isArray(tree?.windows)) {
1418
+ tree.windows.forEach((windowInfo, index) => {
1419
+ if (windowInfo?.root) {
1420
+ roots.push({
1421
+ root: windowInfo.root,
1422
+ windowType: windowInfo.type || 'window',
1423
+ windowIndex: index,
1424
+ viewport: windowInfo.bounds || windowInfo.root.bounds || null,
1425
+ });
1426
+ }
1427
+ });
1428
+ }
1429
+ if (tree?.root) {
1430
+ roots.push({
1431
+ root: tree.root,
1432
+ windowType: 'root',
1433
+ windowIndex: null,
1434
+ viewport: tree.root.bounds || null,
1435
+ });
1436
+ }
1437
+
1438
+ for (const rootInfo of roots) {
1439
+ visitBridgeTreeNode(rootInfo.root, {
1440
+ result,
1441
+ options: compactOptions,
1442
+ depth: 0,
1443
+ parentPath: '',
1444
+ rootInfo,
1445
+ });
1446
+ if (result.truncated) break;
1447
+ }
1448
+
1449
+ return result;
1450
+ }
1451
+
1452
+ function visitBridgeTreeNode(node, state) {
1453
+ if (!node || state.result.truncated) return;
1454
+ state.result.scannedNodes += 1;
1455
+
1456
+ const children = Array.isArray(node.children) ? node.children : [];
1457
+ const path = state.parentPath ? `${state.parentPath}.${state.result.scannedNodes}` : String(state.result.scannedNodes);
1458
+ if (state.depth <= state.options.maxDepth && bridgeNodeMatchesCompactFilters(node, state.options, state.rootInfo.viewport)) {
1459
+ state.result.matchedNodes += 1;
1460
+ if (state.result.nodes.length >= state.options.maxNodes) {
1461
+ state.result.truncated = true;
1462
+ return;
1463
+ }
1464
+ state.result.nodes.push(compactBridgeNode(node, {
1465
+ depth: state.depth,
1466
+ path,
1467
+ childCount: children.length,
1468
+ windowType: state.rootInfo.windowType,
1469
+ windowIndex: state.rootInfo.windowIndex,
1470
+ }));
1471
+ }
1472
+
1473
+ if (state.depth >= state.options.maxDepth) return;
1474
+ for (const child of children) {
1475
+ visitBridgeTreeNode(child, {
1476
+ ...state,
1477
+ depth: state.depth + 1,
1478
+ parentPath: path,
1479
+ });
1480
+ if (state.result.truncated) return;
1481
+ }
1482
+ }
1483
+
1484
+ function bridgeNodeMatchesCompactFilters(node, options, viewport) {
1485
+ if (options.visibleOnly && nodeTapState(node, viewport).ok !== true) return false;
1486
+ const text = compactString(node.text || node.contentDescription || '');
1487
+ const resource = compactString(node.resourceName || node.id || '');
1488
+ const className = compactString(node.className || node.simpleClassName || '');
1489
+ return compactFiltersMatch({ text, resource, className }, options);
1490
+ }
1491
+
1492
+ function compactBridgeNode(node, extra) {
1493
+ return {
1494
+ depth: extra.depth,
1495
+ path: extra.path,
1496
+ windowType: extra.windowType,
1497
+ windowIndex: extra.windowIndex,
1498
+ className: node.simpleClassName || node.className || '',
1499
+ resourceName: node.resourceName || null,
1500
+ text: node.text || null,
1501
+ contentDescription: node.contentDescription || null,
1502
+ visible: node.visible !== false && node.effectiveVisible !== false,
1503
+ enabled: node.enabled !== false,
1504
+ clickable: Boolean(node.clickable),
1505
+ focusable: Boolean(node.focusable),
1506
+ focused: Boolean(node.focused),
1507
+ selected: Boolean(node.selected),
1508
+ bounds: compactBounds(node.bounds),
1509
+ childCount: extra.childCount,
1510
+ };
1511
+ }
1512
+
1513
+ function compactUiaTree(xml, options = {}) {
1514
+ const compactOptions = compactTreeOptions(options);
1515
+ const text = String(xml || '');
1516
+ const snapshot = uiaSnapshotIdentity(text);
1517
+ const viewport = parseUiaViewport(text);
1518
+ const result = {
1519
+ ok: true,
1520
+ source: 'uiautomator',
1521
+ compact: true,
1522
+ rawBytes: Buffer.byteLength(text, 'utf8'),
1523
+ viewport,
1524
+ options: compactTreeResultOptions(compactOptions),
1525
+ nodes: [],
1526
+ scannedNodes: 0,
1527
+ matchedNodes: 0,
1528
+ truncated: false,
1529
+ updatedAtMs: Date.now(),
1530
+ };
1531
+
1532
+ const tokenRegex = /<\/node>|<node\b[^>]*\/?>/g;
1533
+ let depth = -1;
1534
+ let token;
1535
+ while ((token = tokenRegex.exec(text)) !== null) {
1536
+ const tag = token[0];
1537
+ if (tag.startsWith('</node')) {
1538
+ depth = Math.max(-1, depth - 1);
1539
+ continue;
1540
+ }
1541
+
1542
+ depth += 1;
1543
+ result.scannedNodes += 1;
1544
+ const attrs = parseXmlAttributes(tag);
1545
+ const node = compactUiaNode(attrs, depth, result.scannedNodes);
1546
+ if (snapshot) node.targetRef = uiaNodeTargetRef(snapshot, attrs);
1547
+ if (depth <= compactOptions.maxDepth && uiaNodeMatchesCompactFilters(node, compactOptions, viewport)) {
1548
+ result.matchedNodes += 1;
1549
+ if (result.nodes.length >= compactOptions.maxNodes) {
1550
+ result.truncated = true;
1551
+ break;
1552
+ }
1553
+ result.nodes.push(node);
1554
+ }
1555
+ if (tag.endsWith('/>')) {
1556
+ depth = Math.max(-1, depth - 1);
1557
+ }
1558
+ }
1559
+
1560
+ return result;
1561
+ }
1562
+
1563
+ function compactUiaNode(attrs, depth, sequence) {
1564
+ return {
1565
+ depth,
1566
+ sequence,
1567
+ className: attrs.class || '',
1568
+ resourceId: attrs['resource-id'] || null,
1569
+ text: attrs.text || null,
1570
+ contentDescription: attrs['content-desc'] || null,
1571
+ packageName: attrs.package || null,
1572
+ enabled: attrs.enabled !== 'false',
1573
+ clickable: attrs.clickable === 'true',
1574
+ focusable: attrs.focusable === 'true',
1575
+ focused: attrs.focused === 'true',
1576
+ selected: attrs.selected === 'true',
1577
+ scrollable: attrs.scrollable === 'true',
1578
+ checked: attrs.checked === 'true',
1579
+ bounds: compactBounds(parseUiaBounds(attrs.bounds)),
1580
+ };
1581
+ }
1582
+
1583
+ function uiaNodeMatchesCompactFilters(node, options, viewport) {
1584
+ if (options.visibleOnly && !boundsInViewport(node.bounds, viewport)) return false;
1585
+ return compactFiltersMatch({
1586
+ text: compactString(node.text || node.contentDescription || ''),
1587
+ resource: compactString(node.resourceId || ''),
1588
+ className: compactString(node.className || ''),
1589
+ }, options);
1590
+ }
1591
+
1592
+ function compactFiltersMatch(values, options) {
1593
+ if (options.textFilter && !values.text.includes(options.textFilter)) return false;
1594
+ if (options.resourceIdFilter && !values.resource.includes(options.resourceIdFilter)) return false;
1595
+ if (options.classFilter && !values.className.includes(options.classFilter)) return false;
1596
+ return true;
1597
+ }
1598
+
1599
+ function compactTreeResultOptions(options) {
1600
+ return {
1601
+ textFilter: options.textFilter || undefined,
1602
+ resourceIdFilter: options.resourceIdFilter || undefined,
1603
+ classFilter: options.classFilter || undefined,
1604
+ visibleOnly: options.visibleOnly || undefined,
1605
+ maxNodes: options.maxNodes,
1606
+ maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : undefined,
1607
+ };
1608
+ }
1609
+
1610
+ function parseUiaBounds(value) {
1611
+ const match = /\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(String(value || ''));
1612
+ if (!match) return null;
1613
+ const left = Number(match[1]);
1614
+ const top = Number(match[2]);
1615
+ const right = Number(match[3]);
1616
+ const bottom = Number(match[4]);
1617
+ return {
1618
+ left,
1619
+ top,
1620
+ right,
1621
+ bottom,
1622
+ width: right - left,
1623
+ height: bottom - top,
1624
+ };
1625
+ }
1626
+
1627
+ function boundsInViewport(bounds, viewport) {
1628
+ if (!bounds) return false;
1629
+ const width = Number(bounds.width ?? bounds.right - bounds.left);
1630
+ const height = Number(bounds.height ?? bounds.bottom - bounds.top);
1631
+ if (width <= 0 || height <= 0) return false;
1632
+ if (!viewport) return true;
1633
+ const centerX = (Number(bounds.left) + Number(bounds.right)) / 2;
1634
+ const centerY = (Number(bounds.top) + Number(bounds.bottom)) / 2;
1635
+ return centerX >= Number(viewport.left) &&
1636
+ centerX <= Number(viewport.right) &&
1637
+ centerY >= Number(viewport.top) &&
1638
+ centerY <= Number(viewport.bottom);
1639
+ }
1640
+
1641
+ function compactBounds(bounds) {
1642
+ if (!bounds) return null;
1643
+ return {
1644
+ left: Number(bounds.left),
1645
+ top: Number(bounds.top),
1646
+ right: Number(bounds.right),
1647
+ bottom: Number(bounds.bottom),
1648
+ width: Number(bounds.width ?? bounds.right - bounds.left),
1649
+ height: Number(bounds.height ?? bounds.bottom - bounds.top),
1650
+ };
1651
+ }
1652
+
1653
+ function normalizeFilter(value) {
1654
+ return String(value || '').trim().toLowerCase();
1655
+ }
1656
+
1657
+ function compactString(value) {
1658
+ return String(value || '').toLowerCase();
1659
+ }
1660
+
1661
+ function boundedInteger(value, fallback, min, max) {
1662
+ if (value === undefined || value === null || value === '') return fallback;
1663
+ const number = Number(value);
1664
+ if (!Number.isFinite(number)) return fallback;
1665
+ return Math.max(min, Math.min(Math.floor(number), max));
1666
+ }
1667
+
1668
+ async function screenshot(ctx, outFile, options = {}) {
1669
+ const foreground = await foregroundWindow(ctx);
1670
+ const resolvedPath = await adbBinaryToFile(ctx, ['exec-out', 'screencap', '-p'], outFile);
1671
+ const size = pngSize(resolvedPath);
1672
+ const result = {
1673
+ ok: true,
1674
+ transport: 'adb',
1675
+ mimeType: 'image/png',
1676
+ path: resolvedPath,
1677
+ width: size.width,
1678
+ height: size.height,
1679
+ artifact: {
1680
+ path: resolvedPath,
1681
+ sha256: createHash('sha256').update(fs.readFileSync(resolvedPath)).digest('hex'),
1682
+ generatedDefault: !options.outFile,
1683
+ directory: path.dirname(resolvedPath),
1684
+ },
1685
+ foreground,
1686
+ };
1687
+ if (result.artifact.generatedDefault) {
1688
+ result.artifact.retention = await pruneGeneratedArtifacts({
1689
+ directory: result.artifact.directory,
1690
+ prefix: options.artifactPrefix || 'ai_app_bridge_screenshot',
1691
+ extension: 'png',
1692
+ currentPath: resolvedPath,
1693
+ });
1694
+ }
1695
+ if (ctx.packageName) {
1696
+ result.targetPackageName = ctx.packageName;
1697
+ }
1698
+ if (ctx.explicitPackageName) {
1699
+ result.foregroundMatchesPackage = foreground.packageName === ctx.packageName;
1700
+ if (!foreground.ok || !result.foregroundMatchesPackage) {
1701
+ result.ok = false;
1702
+ result.error = foreground.ok ? 'foreground_package_mismatch' : (foreground.error || 'foreground_probe_failed');
1703
+ result.warning = foreground.ok
1704
+ ? `screenshot captured foreground package ${foreground.packageName}, not requested package ${ctx.packageName}`
1705
+ : 'screenshot could not verify the foreground package';
1706
+ }
1707
+ }
1708
+ return result;
1709
+ }
1710
+
1711
+ function screenshotOutputPath(options = {}, prefix = 'ai_app_bridge_screenshot') {
1712
+ if (options.outFile) return options.outFile;
1713
+ return defaultArtifactPath(prefix, 'png', { artifactDir: options.artifactDir });
1714
+ }
1715
+
1716
+ function pngSize(filePath) {
1717
+ const bytes = fs.readFileSync(filePath);
1718
+ if (bytes.length < 24) return { width: 0, height: 0 };
1719
+ return {
1720
+ width: bytes.readUInt32BE(16),
1721
+ height: bytes.readUInt32BE(20),
1722
+ };
1723
+ }
1724
+
1725
+ async function foregroundWindow(ctx) {
1726
+ try {
1727
+ const result = await adb(ctx, ['shell', 'dumpsys', 'window']);
1728
+ return parseForegroundWindow(result.stdout);
1729
+ } catch (error) {
1730
+ if (error instanceof CommandError) throw error;
1731
+ return {
1732
+ ok: false,
1733
+ error: 'foreground_probe_failed',
1734
+ message: firstErrorLine(error),
1735
+ };
1736
+ }
1737
+ }
1738
+
1739
+ function parseForegroundWindow(raw) {
1740
+ const lines = String(raw || '').split(/\r?\n/);
1741
+ const markers = [
1742
+ 'mCurrentFocus',
1743
+ 'mTopResumedActivity',
1744
+ 'mResumedActivity',
1745
+ 'mFocusedApp',
1746
+ ];
1747
+ for (const marker of markers) {
1748
+ const line = lines.find((item) => item.includes(marker));
1749
+ if (!line) continue;
1750
+ const component = parseComponentFromWindowLine(line);
1751
+ if (!component) continue;
1752
+ return {
1753
+ ok: true,
1754
+ source: marker,
1755
+ packageName: component.packageName,
1756
+ activity: component.activity,
1757
+ component: component.component,
1758
+ raw: line.trim(),
1759
+ };
1760
+ }
1761
+ return {
1762
+ ok: false,
1763
+ error: 'foreground_not_found',
1764
+ };
1765
+ }
1766
+
1767
+ function parseComponentFromWindowLine(line) {
1768
+ const componentRegex = /([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/(\.?[A-Za-z0-9_.$]+(?:\.[A-Za-z0-9_.$]+)*)/g;
1769
+ let match;
1770
+ let lastMatch = null;
1771
+ while ((match = componentRegex.exec(line)) !== null) {
1772
+ lastMatch = match;
1773
+ }
1774
+ if (!lastMatch) return null;
1775
+ const packageName = lastMatch[1];
1776
+ const rawActivity = lastMatch[2];
1777
+ const activity = rawActivity.startsWith('.') ? `${packageName}${rawActivity}` : rawActivity;
1778
+ return {
1779
+ packageName,
1780
+ activity,
1781
+ component: `${packageName}/${rawActivity}`,
1782
+ };
1783
+ }
1784
+
1785
+ async function tap(ctx, x, y, options, dependencies = {}) {
1786
+ checkExecution();
1787
+ const runAdb = dependencies.adb || adb;
1788
+ const readForeground = dependencies.foregroundWindow || foregroundWindow;
1789
+ if (options?.nativeTarget && (!ctx.explicitPackageName || options.scope === 'device')) {
1790
+ return { ok: false, error: 'native_target_requires_app_scope', dispatched: false, ambiguous: false };
1791
+ }
1792
+ if (!ctx.explicitPackageName) {
1793
+ const result = await runAdb(ctx, ['shell', 'input', 'tap', String(x), String(y)], { mutation: true });
1794
+ return { ok: true, transport: 'adb', x, y, ...executionFields(result) };
1795
+ }
1796
+
1797
+ const foreground = await readForeground(ctx);
1798
+ if (!foreground.ok || foreground.packageName !== ctx.packageName) {
1799
+ return {
1800
+ ok: false,
1801
+ error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed',
1802
+ message: foreground.ok ? 'The foreground app does not match the requested app. Observe the target again.' : 'The foreground app could not be verified.',
1803
+ dispatched: false,
1804
+ ambiguous: false,
1805
+ transport: null,
1806
+ x,
1807
+ y,
1808
+ target: { packageName: ctx.packageName },
1809
+ targetFeedback: {
1810
+ status: 'unavailable',
1811
+ reason: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed',
1812
+ foreground,
1813
+ },
1814
+ };
1815
+ }
1816
+
1817
+ if (options?.scope === 'device') {
1818
+ const result = await runAdb(ctx, ['shell', 'input', 'tap', String(x), String(y)], { mutation: true });
1819
+ return { ok: true, transport: 'adb', x, y, target: { packageName: ctx.packageName }, ...executionFields(result) };
1820
+ }
1821
+
1822
+ try {
1823
+ const runtimeActionId = options?.runtimeActionId ?? options?.requestId;
1824
+ const result = await nativeAction(ctx, options?.nativeTarget ? '/v1/action/tap-target' : '/v1/action/tap', {
1825
+ ...(options?.nativeTarget || { x, y }),
1826
+ ...(runtimeActionId === undefined || runtimeActionId === null
1827
+ ? {}
1828
+ : { actionId: String(runtimeActionId) }),
1829
+ }, dependencies);
1830
+ return { ...result, transport: 'bridge' };
1831
+ } catch (error) {
1832
+ checkExecution();
1833
+ if (!bridgeTapWasDefinitelyNotDispatched(error)) throw error;
1834
+ return {
1835
+ ok: false,
1836
+ error: options?.nativeTarget ? 'native_atomic_target_unavailable' : 'app_action_unavailable',
1837
+ message: options?.nativeTarget ? 'The app SDK cannot validate this target. Update the SDK and observe again.'
1838
+ : 'The app SDK cannot accept this action. Device input requires explicit scope: device.',
1839
+ dispatched: false,
1840
+ ambiguous: false,
1841
+ transport: null,
1842
+ x,
1843
+ y,
1844
+ target: null,
1845
+ targetFeedback: {
1846
+ status: 'unavailable',
1847
+ reason: 'bridge_action_unavailable',
1848
+ error: firstErrorLine(error),
1849
+ },
1850
+ };
1851
+ }
1852
+ }
1853
+
1854
+ function bridgeTapWasDefinitelyNotDispatched(error) {
1855
+ if (error?.aiAppBridgeRequestNotStarted === true) return true;
1856
+ if (error?.code === 'ECONNREFUSED') return true;
1857
+ return /^HTTP 404:/.test(String(error?.message || error || ''));
1858
+ }
1859
+
1860
+ async function tapText(ctx, targetText, options = {}, dependencies = {}) {
1861
+ checkExecution();
1862
+ const readForeground = dependencies.foregroundWindow || foregroundWindow;
1863
+ const foreground = await readForeground(ctx);
1864
+ if (!foreground.ok || (ctx.explicitPackageName && foreground.packageName !== ctx.packageName)) {
1865
+ return { ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed',
1866
+ dispatched: false, ambiguous: false, foreground };
1867
+ }
1868
+ const selectedProvider = options.provider || 'auto';
1869
+ const providers = selectedProvider === 'auto' ? ['native', 'flutter', 'uia'] : [selectedProvider];
1870
+ const observations = [];
1871
+ let nativeForeground;
1872
+ const reject = (error, message) => ({ ok: false, error, message, targetText, dispatched: false, ambiguous: false, observations });
1873
+ async function readMatch(provider) {
1874
+ checkExecution();
1875
+ if (provider === 'native') {
1876
+ const tree = await (dependencies.bridgeTree || bridgeTree)(ctx);
1877
+ if (tree?.ok === false) throw new CommandError('observation_unavailable', tree.error || 'native_tree_unavailable');
1878
+ nativeForeground = nativeWindow(tree);
1879
+ const found = findTappableNodeByText(tree, targetText);
1880
+ if (found.ambiguous) throw new CommandError('target_ambiguous', 'Several native Views match targetText. Use tap-native or Intent with a precise selector.');
1881
+ if (!found.node) return null;
1882
+ return { x: (found.node.bounds.left + found.node.bounds.right) / 2,
1883
+ y: (found.node.bounds.top + found.node.bounds.bottom) / 2, node: bridgeNodeTarget(found.node, found.windowType),
1884
+ guarded: nativeTargetRequest(found.node, found.node.text === targetText ? { text: targetText } : { contentDescription: targetText }),
1885
+ identity: { window: nativeWindowIdentity(nativeForeground), node: nativeNodeIdentity(found.node) }, coordinateSpace: 'physical-pixels' };
1886
+ }
1887
+ if (provider === 'flutter') {
1888
+ // Flutter coordinates belong to the activity. A native dialog owns input.
1889
+ if (nativeForeground === undefined || selectedProvider === 'flutter') {
1890
+ nativeForeground = nativeWindow(await (dependencies.bridgeTree || bridgeTree)(ctx));
1891
+ }
1892
+ if (nativeForeground?.type !== 'activity') throw new CommandError('native_foreground_blocks_flutter', 'Observe the foreground native window before targeting Flutter.');
1893
+ const selected = selectFlutterNode(await (dependencies.flutterNodes || flutterNodes)(ctx), { text: targetText });
1894
+ if (selected.error === 'flutter_selector_not_unique') throw new CommandError('target_ambiguous', 'Several Flutter nodes match targetText. Use Intent with a precise selector.');
1895
+ if (!selected.ok) {
1896
+ if (selected.error === 'flutter_selector_not_found') return null;
1897
+ throw new CommandError(selected.error, 'The Flutter target could not be observed.');
1898
+ }
1899
+ return { ...selected, guarded: flutterTargetRequest(selected.node, { text: targetText }), identity: flutterNodeIdentity(selected.node), coordinateSpace: 'flutter-logical-pixels' };
1900
+ }
1901
+ const xml = await (dependencies.uiaTree || uiaTree)(ctx);
1902
+ const node = findUiaNodeByAny(xml, { texts: [targetText], exact: true, requireUnique: true, packageName: foreground.packageName });
1903
+ if (!node) return null;
1904
+ const selector = node.rawNode.text === targetText ? { text: targetText } : { contentDescription: targetText };
1905
+ return { node: node.matched, binding: observedUiaTarget(xml, node.rawNode, selector, foreground.packageName) };
1906
+ }
1907
+ for (const provider of providers) {
1908
+ let match;
1909
+ try { match = await readMatch(provider); }
1910
+ catch (error) {
1911
+ checkExecution();
1912
+ observations.push({ provider, status: 'unavailable', error: error.code || error.message });
1913
+ if (error.code === 'target_ambiguous' || selectedProvider !== 'auto') return reject(error.code || 'observation_unavailable', error.message);
1914
+ continue;
1915
+ }
1916
+ observations.push({ provider, status: match ? 'matched' : 'not-found', observedAtMs: Date.now() });
1917
+ if (!match) continue;
1918
+ // A selected provider owns this attempt. Fresh selection may reject it but
1919
+ // cannot trigger an input through another provider.
1920
+ let currentMatch = match;
1921
+ try {
1922
+ if (provider === 'flutter') nativeForeground = undefined;
1923
+ if (provider !== 'uia') currentMatch = await readMatch(provider);
1924
+ } catch (error) {
1925
+ checkExecution();
1926
+ return reject(error.code || 'observation_unavailable', error.message);
1927
+ }
1928
+ if (!currentMatch || provider !== 'uia' && JSON.stringify(match.identity) !== JSON.stringify(currentMatch.identity)) return reject('reobserve_required', 'The selected target changed before dispatch.');
1929
+ match = currentMatch;
1930
+ if (['native', 'flutter'].includes(provider) && !match.guarded.ok) return { ...match.guarded, targetText, provider, observations };
1931
+ if (provider !== 'uia' && (!Number.isFinite(match.x) || !Number.isFinite(match.y) || match.x < 0 || match.y < 0)) return reject('invalid_observed_coordinates');
1932
+ const current = await readForeground(ctx);
1933
+ if (!current.ok || current.component !== foreground.component || current.packageName !== foreground.packageName) return reject('foreground_changed_during_observation');
1934
+ checkExecution();
1935
+ const revalidatedAtMs = Date.now();
1936
+ const result = provider === 'uia'
1937
+ ? await (dependencies.uiaTap || uiaTap)(ctx, match.binding, options)
1938
+ : provider === 'flutter'
1939
+ ? await (dependencies.flutterAction || flutterAction)(ctx, { action: 'tapTarget', ...match.guarded.request }, options)
1940
+ : await (dependencies.tap || tap)(ctx, Math.round(match.x), Math.round(match.y), { ...options,
1941
+ nativeTarget: match.guarded.request, scope: 'app' });
1942
+ return { ...result, targetText, provider, source: provider === 'native' ? 'bridge-tree' : provider === 'flutter' ? 'flutter-operable-tree' : 'uia-node-runtime',
1943
+ ...(provider === 'uia' ? {} : { coordinateSpace: match.coordinateSpace }), selected: match.node, revalidatedAtMs, observations };
1944
+ }
1945
+ return reject('target_not_found', 'No observed provider has a unique operable match.');
1946
+ }
1947
+
1948
+ function bridgeNodeTarget(node, windowType) {
1949
+ return {
1950
+ className: node?.className || node?.simpleClassName || '',
1951
+ resourceName: node?.resourceName || node?.resourceId || '',
1952
+ clickable: Boolean(node?.clickable),
1953
+ enabled: node?.enabled !== false,
1954
+ windowType: windowType || 'window',
1955
+ bounds: node?.bounds || null,
1956
+ };
1957
+ }
1958
+
1959
+ function findTappableNodeByText(tree, targetText) {
1960
+ const window = nativeWindow(tree);
1961
+ if (!window) return { node: null, rejected: { reason: 'visible_observed_window_required' } };
1962
+ const result = findNodeByText(window.root, targetText, window.bounds);
1963
+ return { ...result, windowType: window.type, viewport: window.bounds };
1964
+ }
1965
+
1966
+ function findNodeByText(node, targetText, viewport = null) {
1967
+ const matches = [];
1968
+ let rejected = null;
1969
+ function visit(current, ancestors = []) {
1970
+ if (!current || explicitlyHidden(current)) return;
1971
+ if (current.text === targetText || current.contentDescription === targetText) {
1972
+ const state = nodeTapState(current, viewport);
1973
+ if (state.ok && ancestors.some(ancestor => ancestor.enabled === false || !validBounds(ancestor.bounds)
1974
+ || !pointInsideBounds({ x: (current.bounds.left + current.bounds.right) / 2, y: (current.bounds.top + current.bounds.bottom) / 2 }, ancestor.bounds))) {
1975
+ state.ok = false; state.reason = 'center_outside_ancestor';
1976
+ }
1977
+ if (state.ok) matches.push(current);
1978
+ else rejected ||= { node: current, viewport, reason: state.reason };
1979
+ }
1980
+ for (const child of current.children || []) visit(child, [...ancestors, current]);
1981
+ }
1982
+ visit(node);
1983
+ return { node: matches.length === 1 ? matches[0] : null, ambiguous: matches.length > 1, rejected };
1984
+ }
1985
+
1986
+ function nodeTapState(node, viewport) {
1987
+ const bounds = node?.bounds;
1988
+ if (!validBounds(bounds)) return { ok: false, reason: 'invalid_bounds' };
1989
+ if (node.visible === false || node.effectiveVisible === false || (node.visible !== true && node.effectiveVisible !== true)) {
1990
+ return { ok: false, reason: 'not_effectively_visible' };
1991
+ }
1992
+ if (node.enabled === false) {
1993
+ return { ok: false, reason: 'not_enabled' };
1994
+ }
1995
+ const width = Number(bounds.width ?? bounds.right - bounds.left);
1996
+ const height = Number(bounds.height ?? bounds.bottom - bounds.top);
1997
+ if (width <= 0 || height <= 0) {
1998
+ return { ok: false, reason: 'empty_bounds' };
1999
+ }
2000
+ if (!viewport) return { ok: true };
2001
+ const centerX = (Number(bounds.left) + Number(bounds.right)) / 2;
2002
+ const centerY = (Number(bounds.top) + Number(bounds.bottom)) / 2;
2003
+ if (
2004
+ centerX < Number(viewport.left) ||
2005
+ centerX >= Number(viewport.right) ||
2006
+ centerY < Number(viewport.top) ||
2007
+ centerY >= Number(viewport.bottom)
2008
+ ) {
2009
+ return { ok: false, reason: 'center_outside_viewport' };
2010
+ }
2011
+ return { ok: true };
2012
+ }
2013
+
2014
+ function findUiaNodeByText(xml, targetText) {
2015
+ return findUiaNodeByAny(xml, { texts: [targetText], exact: true });
2016
+ }
2017
+
2018
+ function parseUiaViewport(xml) {
2019
+ const match = /<node\b[^>]*\bbounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(String(xml || ''));
2020
+ if (!match) return null;
2021
+ const left = Number(match[1]);
2022
+ const top = Number(match[2]);
2023
+ const right = Number(match[3]);
2024
+ const bottom = Number(match[4]);
2025
+ return {
2026
+ left,
2027
+ top,
2028
+ right,
2029
+ bottom,
2030
+ width: right - left,
2031
+ height: bottom - top,
2032
+ };
2033
+ }
2034
+
2035
+ async function tapUiaText(ctx, targetText, options = {}) {
2036
+ if (options.exact === false) throw new CommandError('invalid_argument', 'Text actions require an exact match. Use an observed Intent selector.');
2037
+ return tapText(ctx, targetText, { ...options, provider: 'uia' });
2038
+ }
2039
+
2040
+ async function bindNativeCommandTarget(ctx, selector, editable, dependencies) {
2041
+ checkExecution();
2042
+ if (!ctx.explicitPackageName) return { ok: false, error: 'native_target_requires_app_scope', dispatched: false, ambiguous: false };
2043
+ const readForeground = dependencies.foregroundWindow || foregroundWindow;
2044
+ const foreground = await readForeground(ctx);
2045
+ if (!foreground.ok || foreground.packageName !== ctx.packageName) return {
2046
+ ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed', dispatched: false, ambiguous: false,
2047
+ };
2048
+ const readTree = () => (dependencies.bridgeTree || bridgeTree)(ctx);
2049
+ const selected = await revalidateNativeNode(readTree, await readTree(), { selector }, editable);
2050
+ if (!selected.ok) return { ...selected, ambiguous: false };
2051
+ const binding = nativeTargetRequest(selected.node, selector);
2052
+ if (!binding.ok) return binding;
2053
+ const current = await readForeground(ctx);
2054
+ if (!current.ok || current.component !== foreground.component || current.packageName !== foreground.packageName) return {
2055
+ ok: false, error: 'foreground_changed_during_observation', dispatched: false, ambiguous: false,
2056
+ };
2057
+ checkExecution();
2058
+ return { ok: true, selected, nativeTarget: binding.request };
2059
+ }
2060
+
2061
+ async function tapNative(ctx, options = {}, dependencies = {}) {
2062
+ const binding = await bindNativeCommandTarget(ctx, options.selector, false, dependencies);
2063
+ if (!binding.ok) return binding;
2064
+ const { selected } = binding;
2065
+ const result = await tap(ctx, selected.x, selected.y, { ...options, nativeTarget: binding.nativeTarget, scope: 'app' }, dependencies);
2066
+ return { ...result, provider: 'native', source: 'bridge-tree', selector: options.selector };
2067
+ }
2068
+
2069
+ async function inputNativeText(ctx, text, options = {}, dependencies = {}) {
2070
+ if (Object.hasOwn(options, 'tapX') || Object.hasOwn(options, 'tapY'))
2071
+ throw new CommandError('invalid_argument', 'A Native selector and input coordinates are mutually exclusive.', { field: 'selector' });
2072
+ const binding = await bindNativeCommandTarget(ctx, options.selector, true, dependencies);
2073
+ if (!binding.ok) return binding;
2074
+ const result = await inputText(ctx, text, { ...options, nativeTarget: binding.nativeTarget }, dependencies);
2075
+ return { ...result, provider: 'native', source: 'bridge-tree', selector: options.selector };
2076
+ }
2077
+
2078
+ async function tapUia(ctx, options = {}) {
2079
+ checkExecution();
2080
+ const foreground = await foregroundWindow(ctx);
2081
+ if (!foreground.ok || foreground.packageName !== ctx.packageName)
2082
+ return { ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed',
2083
+ dispatched: false, ambiguous: false, foreground };
2084
+ let binding;
2085
+ if (options.targetRef) binding = uiaBindingFromTargetRef(options.targetRef, ctx.packageName);
2086
+ else {
2087
+ const xml = await uiaTree(ctx);
2088
+ const selected = selectUiaNode(xml, { selector: options.selector }, ctx.packageName);
2089
+ if (!selected.ok) return { ...selected, ambiguous: false,
2090
+ message: `UIAutomator selector did not identify one enabled visible node: ${selected.error}.`, selector: options.selector };
2091
+ binding = observedUiaTarget(xml, selected.node, options.selector, ctx.packageName);
2092
+ }
2093
+ const current = await foregroundWindow(ctx);
2094
+ if (!current.ok || current.component !== foreground.component || current.packageName !== foreground.packageName)
2095
+ return { ok: false, error: 'foreground_changed_during_observation', dispatched: false, ambiguous: false };
2096
+ checkExecution();
2097
+ const result = await uiaTap(ctx, binding, options);
2098
+ return { ...result, provider: 'uia', source: 'uia-node-runtime',
2099
+ ...(options.targetRef ? { targetRef: options.targetRef } : { selector: options.selector }) };
2100
+ }
2101
+
2102
+ function findUiaNodeByAny(xml, options) {
2103
+ const texts = (options.texts || []).map(String).filter(Boolean);
2104
+ const resourceIds = (options.resourceIds || []).map(String).filter(Boolean);
2105
+ const exact = Boolean(options.exact);
2106
+ const nodeRegex = /<node\b[^>]*>/g;
2107
+ const matches = [];
2108
+ let match;
2109
+ while ((match = nodeRegex.exec(xml)) !== null) {
2110
+ const nodeXml = match[0];
2111
+ const rawNode = parseXmlAttributes(nodeXml);
2112
+ if (options.packageName !== undefined && rawNode.package !== options.packageName) continue;
2113
+ if (rawNode['visible-to-user'] === 'false') continue;
2114
+ const attrs = {
2115
+ text: decodeXmlAttribute(readXmlAttribute(nodeXml, 'text')),
2116
+ contentDescription: decodeXmlAttribute(readXmlAttribute(nodeXml, 'content-desc')),
2117
+ resourceId: decodeXmlAttribute(readXmlAttribute(nodeXml, 'resource-id')),
2118
+ className: decodeXmlAttribute(readXmlAttribute(nodeXml, 'class')),
2119
+ clickable: readXmlAttribute(nodeXml, 'clickable') === 'true',
2120
+ enabled: readXmlAttribute(nodeXml, 'enabled') !== 'false',
2121
+ };
2122
+ if (!attrs.enabled || (options.requireClickable && !attrs.clickable)) continue;
2123
+ const textMatch = texts.find((target) => {
2124
+ return [attrs.text, attrs.contentDescription].some((value) => {
2125
+ return exact ? value === target : value.includes(target);
2126
+ });
2127
+ });
2128
+ const resourceIdMatch = resourceIds.find((target) => {
2129
+ return exact ? attrs.resourceId === target : attrs.resourceId.includes(target);
2130
+ });
2131
+ if (!textMatch && !resourceIdMatch) continue;
2132
+ const boundsMatch = /\bbounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(nodeXml);
2133
+ if (!boundsMatch) continue;
2134
+ if (Number(boundsMatch[3]) <= Number(boundsMatch[1]) || Number(boundsMatch[4]) <= Number(boundsMatch[2])) continue;
2135
+ const found = {
2136
+ rawNode,
2137
+ left: Number(boundsMatch[1]),
2138
+ top: Number(boundsMatch[2]),
2139
+ right: Number(boundsMatch[3]),
2140
+ bottom: Number(boundsMatch[4]),
2141
+ matched: {
2142
+ text: attrs.text,
2143
+ contentDescription: attrs.contentDescription,
2144
+ resourceId: attrs.resourceId,
2145
+ className: attrs.className,
2146
+ clickable: attrs.clickable,
2147
+ target: textMatch || resourceIdMatch,
2148
+ },
2149
+ };
2150
+ if (!options.requireUnique) return found;
2151
+ matches.push(found);
2152
+ }
2153
+ if (matches.length > 1) throw new CommandError('target_ambiguous', 'Several UIAutomator nodes match the label. Use tap-uia or Intent with an explicit text, contentDescription or resourceName selector.');
2154
+ return matches[0] || null;
2155
+ }
2156
+
2157
+ function readXmlAttribute(xml, name) {
2158
+ const match = new RegExp(`\\b${escapeRegExp(name)}="([^"]*)"`).exec(xml);
2159
+ return match ? match[1] : '';
2160
+ }
2161
+
2162
+ function escapeRegExp(value) {
2163
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2164
+ }
2165
+
2166
+ async function waitText(ctx, targetText, options = {}, dependencies = {}) {
2167
+ return require('./shared-kernel/text-wait').waitForText({
2168
+ options: { ...options, targetText, packageName: ctx.explicitPackageName ? ctx.packageName : undefined },
2169
+ readForeground: () => (dependencies.foregroundWindow || foregroundWindow)(ctx),
2170
+ readTree: provider => provider === 'native' ? (dependencies.bridgeTree || bridgeTree)(ctx)
2171
+ : provider === 'flutter' ? (dependencies.flutterNodes || flutterNodes)(ctx) : (dependencies.uiaTreeOnce || uiaTreeOnce)(ctx),
2172
+ });
2173
+ }
2174
+
2175
+ async function flutterNodes(ctx) {
2176
+ const status = await bridgeGet(ctx, '/v1/status');
2177
+ return status.flutter?.layout?.operable || { ok: false, error: 'no_flutter_operable_tree' };
2178
+ }
2179
+
2180
+ function flutterTapPayload(options) {
2181
+ const point = {};
2182
+ for (const [option, axis] of [['tapX', 'x'], ['tapY', 'y']]) {
2183
+ const value = options[option];
2184
+ if (!(typeof value === 'number' || (typeof value === 'string' && value.trim() !== ''))
2185
+ || !Number.isFinite(Number(value)) || Number(value) < 0) {
2186
+ throw new Error('invalid_flutter_coordinates');
2187
+ }
2188
+ point[axis] = Number(value);
2189
+ }
2190
+ return { action: 'tapAt', ...point };
2191
+ }
2192
+
2193
+ async function flutterAction(ctx, payload, options = {}) {
2194
+ if (!['h5Adapters', 'h5Dom'].includes(payload.action)) {
2195
+ const foreground = await foregroundWindow(ctx);
2196
+ if (!foreground.ok || foreground.packageName !== ctx.packageName) return {
2197
+ ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed', dispatched: false, ambiguous: false,
2198
+ };
2199
+ const window = nativeWindow(await bridgeTree(ctx));
2200
+ if (!window || window.type !== 'activity') return { ok: false, error: 'native_foreground_blocks_flutter', dispatched: false, ambiguous: false };
2201
+ }
2202
+ const tree = await flutterNodes(ctx);
2203
+ if (['tapText', 'tapTarget', 'inputText', 'scrollBy', 'scrollUntilText'].includes(payload.action) && !payload.targetRef) {
2204
+ const bound = bindFlutterAction(tree, payload);
2205
+ if (!bound.ok) return bound;
2206
+ payload = bound.payload;
2207
+ }
2208
+ const actionId = options.runtimeActionId ?? options.requestId ?? ctx.actionId ?? randomUUID();
2209
+ const requestCtx = { ...ctx };
2210
+ const identity = { actionId: requiredString(actionId, 'actionId'), runtimeEpoch: tree.runtimeEpoch };
2211
+ const target = { packageName: ctx.packageName, adb: ctx.adb, ...(ctx.explicitPort ? { port: ctx.port } : {}) };
2212
+ const result = await runDeviceEffect({ kind: 'flutter', ...identity, target }, () => executeFlutterAction({ tree, payload,
2213
+ actionId: identity.actionId, timeoutMs: ctx.httpTimeoutMs,
2214
+ send: request => bridgePost(requestCtx, '/v1/flutter/action', request),
2215
+ cancel: identity => httpPost(bridgeUrl(requestCtx, '/v1/flutter/cancel'), identity, 4000).then(JSON.parse),
2216
+ }), result => flutterSettlementProof(result, identity));
2217
+ return { ...result, transport: 'bridge', source: 'flutter-runtime-action' };
2218
+ }
2219
+
2220
+ async function flutterCompletion(ctx, identity) {
2221
+ await bridgeGet(ctx, '/v1/status');
2222
+ return bridgePost(ctx, '/v1/flutter/cancel', identity);
2223
+ }
2224
+
2225
+ async function h5Foreground(ctx) {
2226
+ const foreground = await foregroundWindow(ctx);
2227
+ return foreground.ok && foreground.packageName === ctx.packageName ? null : {
2228
+ ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed', dispatched: false, ambiguous: false,
2229
+ };
2230
+ }
2231
+
2232
+ async function h5Dom(ctx, options = {}) {
2233
+ const rejected = await h5Foreground(ctx);
2234
+ if (rejected) return rejected;
2235
+ const result = await bridgeGet(ctx, withQuery('/v1/h5/dom', options.webViewId === undefined ? {} : { webViewId: options.webViewId }));
2236
+ if (result.ok !== true) return result;
2237
+ if (result.h5TargetSchema !== h5Target.schema) return { ok: false, error: 'android_h5_target_schema_required', dispatched: false, ambiguous: false };
2238
+ const changed = await h5Foreground(ctx);
2239
+ return changed || result;
2240
+ }
2241
+
2242
+ async function h5Action(ctx, payload) {
2243
+ const rejected = await h5Foreground(ctx);
2244
+ if (rejected) return rejected;
2245
+ const status = await bridgeGet(ctx, '/v1/status');
2246
+ const runtime = { runtimeEpoch: status.debugBridge?.runtimeEpoch, executionSchema: status.debugBridge?.h5ExecutionSchema };
2247
+ if (runtime.executionSchema !== h5ExecutionSchema || status.debugBridge?.h5TargetSchema !== h5Target.schema) return {
2248
+ ok: false, error: 'h5_execution_unavailable', dispatched: false, ambiguous: false,
2249
+ message: 'The app SDK must advertise managed H5 execution and Android H5 target binding.',
2250
+ };
2251
+ if (runtime.runtimeEpoch !== payload.pageRef.runtimeEpoch) return {
2252
+ ok: false, error: 'reobserve_required', dispatched: false, ambiguous: false,
2253
+ };
2254
+ const identity = { actionId: ctx.actionId ?? randomUUID(), runtimeEpoch: runtime.runtimeEpoch };
2255
+ const target = { packageName: ctx.packageName, adb: ctx.adb, ...(ctx.explicitPort ? { port: ctx.port } : {}) };
2256
+ const requestCtx = { ...ctx };
2257
+ return runDeviceEffect({ kind: 'h5', ...identity, target }, () => executeH5Action({ runtime, payload: { payload },
2258
+ actionId: identity.actionId, timeoutMs: ctx.httpTimeoutMs,
2259
+ send: request => bridgePost(requestCtx, '/v1/h5/action', request),
2260
+ cancel: identity => httpPost(bridgeUrl(requestCtx, '/v1/h5/cancel'), identity, 4000).then(JSON.parse),
2261
+ }), result => h5SettlementProof(result, identity));
2262
+ }
2263
+
2264
+ async function h5Completion(ctx, identity) {
2265
+ await bridgeGet(ctx, '/v1/status');
2266
+ return bridgePost(ctx, '/v1/h5/cancel', identity);
2267
+ }
2268
+
2269
+ async function h5Eval(ctx, options) {
2270
+ return h5Action(ctx, { action: 'eval', pageRef: options.expectedPage, script: options.script });
2271
+ }
2272
+
2273
+ async function h5Control(ctx, action, options) {
2274
+ const tree = await h5Dom(ctx, { webViewId: options.webViewId });
2275
+ if (tree.ok !== true) return tree;
2276
+ if (action === 'scroll' && options.selector === undefined) {
2277
+ if (options.expectedPage !== undefined && !isDeepStrictEqual(tree.pageRef, options.expectedPage)) return {
2278
+ ok: false, error: 'reobserve_required', dispatched: false, ambiguous: false,
2279
+ };
2280
+ return h5Action(ctx, { action: 'scrollBy', pageRef: tree.pageRef, deltaX: options.deltaX, deltaY: options.deltaY });
2281
+ }
2282
+ const selected = h5Target.selectH5Node(tree, options.selector, options.expectedTarget);
2283
+ if (!selected.ok) return selected;
2284
+ return h5Action(ctx, { action, ...selected.targetRef, ...(action === 'input' ? { text: options.text } : {}) });
2285
+ }
2286
+
2287
+ const h5Click = (ctx, options) => h5Control(ctx, 'click', options);
2288
+ const h5Input = (ctx, options) => h5Control(ctx, 'input', options);
2289
+ const h5Scroll = (ctx, options) => h5Control(ctx, 'scroll', options);
2290
+
2291
+ async function h5Wait(ctx, options) {
2292
+ const timeoutMs = options.timeoutMs ?? 10000, intervalMs = options.intervalMs ?? 500;
2293
+ const deadline = Date.now() + timeoutMs;
2294
+ let webViewId = options.webViewId, lastResult;
2295
+ do {
2296
+ checkExecution();
2297
+ const tree = await h5Dom({ ...ctx, httpTimeoutMs: Math.max(1, Math.min(ctx.httpTimeoutMs, deadline - Date.now())) }, { webViewId });
2298
+ if (tree.ok !== true) return tree;
2299
+ webViewId = tree.pageRef.webViewId;
2300
+ lastResult = h5Target.selectH5Node(tree, options.selector);
2301
+ if (lastResult.ok || lastResult.error !== 'android_h5_selector_not_found') return lastResult;
2302
+ const remaining = deadline - Date.now();
2303
+ if (remaining <= 0) break;
2304
+ await sleep(Math.min(intervalMs, remaining));
2305
+ } while (Date.now() < deadline);
2306
+ return { ok: false, error: 'h5_wait_timeout', timeoutMs, lastResult, dispatched: false, ambiguous: false };
2307
+ }
2308
+
2309
+ function flutterH5Port(ctx) {
2310
+ return require('./shared-kernel/flutter-h5-port').createFlutterH5Port(
2311
+ (payload, identity) => flutterAction({ ...ctx, actionId: identity.actionId }, payload), { actionId: ctx.actionId });
2312
+ }
2313
+
2314
+ async function flutterH5Dom(ctx, options = {}) {
2315
+ return flutterH5Port(ctx).snapshot(options.adapterId);
2316
+ }
2317
+
2318
+ async function flutterH5Eval(ctx, options) {
2319
+ return flutterH5Port(ctx).evaluate(options);
2320
+ }
2321
+
2322
+ async function flutterH5Click(ctx, options) {
2323
+ return flutterH5Port(ctx).control('click', options);
2324
+ }
2325
+
2326
+ async function flutterH5Input(ctx, options) {
2327
+ return flutterH5Port(ctx).control('input', options);
2328
+ }
2329
+
2330
+ async function flutterH5Scroll(ctx, options) {
2331
+ return flutterH5Port(ctx).control(options.selector === undefined ? 'scrollBy' : 'scroll', options);
2332
+ }
2333
+
2334
+ async function flutterH5Wait(ctx, options) {
2335
+ return flutterH5Port(ctx).wait(options);
2336
+ }
2337
+
2338
+ function booleanOption(value) {
2339
+ if (typeof value === 'boolean') return value;
2340
+ return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
2341
+ }
2342
+
2343
+ async function keyboardState(ctx) {
2344
+ try {
2345
+ const result = await adb(ctx, ['shell', 'dumpsys', 'input_method']);
2346
+ return parseKeyboardState(result.stdout);
2347
+ } catch (error) {
2348
+ return {
2349
+ ok: false,
2350
+ error: 'keyboard_state_probe_failed',
2351
+ message: firstErrorLine(error),
2352
+ };
2353
+ }
2354
+ }
2355
+
2356
+ function parseKeyboardState(raw) {
2357
+ const text = String(raw || '');
2358
+ const inputShown = text.includes('mInputShown=true') || text.includes('inputShown=true');
2359
+ const windowVisible = text.includes('mWindowVisible=true');
2360
+ const inputViewShown = text.includes('mIsInputViewShown=true') || text.includes('mInputViewStarted=true');
2361
+ const imeWindowVisible = /\bmImeWindowVis=0x[13]\b/i.test(text) || /\bmImeWindowVisibility=0x[13]\b/i.test(text);
2362
+ const markers = [];
2363
+ if (inputShown) markers.push('mInputShown=true');
2364
+ if (windowVisible) markers.push('mWindowVisible=true');
2365
+ if (inputViewShown) markers.push('mIsInputViewShown=true');
2366
+ if (imeWindowVisible) markers.push('mImeWindowVis');
2367
+ const hiddenMarkers = [
2368
+ 'mInputShown=false',
2369
+ 'mWindowVisible=false',
2370
+ 'mImeWindowVis=0',
2371
+ ].filter((marker) => text.includes(marker));
2372
+ return {
2373
+ ok: true,
2374
+ source: 'dumpsys input_method',
2375
+ visible: inputShown || imeWindowVisible || (windowVisible && inputViewShown),
2376
+ markers,
2377
+ hiddenMarkers,
2378
+ };
2379
+ }
2380
+
2381
+ async function hideKeyboard(ctx, options = {}) {
2382
+ const before = await keyboardState(ctx);
2383
+ const force = booleanOption(options.force);
2384
+ if (!force && before.ok && !before.visible) {
2385
+ return {
2386
+ ok: true,
2387
+ action: 'hide-keyboard',
2388
+ dismissed: false,
2389
+ reason: 'keyboard_not_visible',
2390
+ before,
2391
+ after: before,
2392
+ attempts: [],
2393
+ };
2394
+ }
2395
+
2396
+ const attempts = [];
2397
+ for (const keyCode of [111, 4]) {
2398
+ await keyevent(ctx, keyCode);
2399
+ await sleep(Number(options.intervalMs || 500));
2400
+ const after = await keyboardState(ctx);
2401
+ attempts.push({ keyCode, visible: after.visible, ok: after.ok });
2402
+ if (after.ok && !after.visible) {
2403
+ return {
2404
+ ok: true,
2405
+ action: 'hide-keyboard',
2406
+ dismissed: true,
2407
+ before,
2408
+ after,
2409
+ attempts,
2410
+ };
2411
+ }
2412
+ }
2413
+
2414
+ const after = await keyboardState(ctx);
2415
+ return {
2416
+ ok: false,
2417
+ action: 'hide-keyboard',
2418
+ error: 'keyboard_still_visible',
2419
+ dismissed: false,
2420
+ before,
2421
+ after,
2422
+ attempts,
2423
+ };
2424
+ }
2425
+
2426
+ async function maybeHideKeyboardForPoint(ctx, point, viewport) {
2427
+ const state = await keyboardState(ctx);
2428
+ const decision = shouldDismissKeyboardForPoint({ point, viewport, keyboardVisible: state.visible });
2429
+ if (!decision.dismiss) {
2430
+ return {
2431
+ dismissed: false,
2432
+ state,
2433
+ decision,
2434
+ };
2435
+ }
2436
+ const hide = await hideKeyboard(ctx, { reason: decision.reason });
2437
+ return {
2438
+ dismissed: hide.dismissed,
2439
+ state,
2440
+ decision,
2441
+ hide,
2442
+ };
2443
+ }
2444
+
2445
+ function shouldDismissKeyboardForPoint({ point, viewport, keyboardVisible }) {
2446
+ if (!keyboardVisible) {
2447
+ return { dismiss: false, reason: 'keyboard_not_visible' };
2448
+ }
2449
+ if (!point || !viewport) {
2450
+ return { dismiss: false, reason: 'missing_geometry' };
2451
+ }
2452
+ const top = Number(viewport.top || 0);
2453
+ const bottom = Number(viewport.bottom);
2454
+ if (!Number.isFinite(bottom) || bottom <= top) {
2455
+ return { dismiss: false, reason: 'invalid_viewport' };
2456
+ }
2457
+ const threshold = top + (bottom - top) * 0.58;
2458
+ if (Number(point.y) >= threshold) {
2459
+ return {
2460
+ dismiss: true,
2461
+ reason: 'target_may_be_obscured_by_keyboard',
2462
+ threshold,
2463
+ };
2464
+ }
2465
+ return {
2466
+ dismiss: false,
2467
+ reason: 'target_above_keyboard_risk_area',
2468
+ threshold,
2469
+ };
2470
+ }
2471
+
2472
+ async function inputText(ctx, text, options = {}, dependencies = {}) {
2473
+ const request = inputTextBridgePayload(text, options);
2474
+ let result;
2475
+ try { result = await nativeAction(ctx, options.nativeTarget ? '/v1/action/input-target' : '/v1/action/input-text', request, dependencies); }
2476
+ catch (error) {
2477
+ checkExecution();
2478
+ if (!options.nativeTarget || !bridgeTapWasDefinitelyNotDispatched(error)) throw error;
2479
+ return { ok: false, error: 'native_atomic_target_unavailable', dispatched: false, ambiguous: false,
2480
+ message: 'The app SDK cannot validate this input target. Update the SDK and observe again.' };
2481
+ }
2482
+ if (result?.ok !== true) return { ...result, ok: false, error: result?.error || 'bridge_input_failed',
2483
+ message: result?.message || 'The app SDK did not accept native text input.', transport: 'bridge', request };
2484
+ const response = { ...result, transport: 'bridge', source: result.source || 'native-view', request };
2485
+ if (options.hideKeyboard === true) response.keyboard = await hideKeyboard(ctx, options);
2486
+ return response;
2487
+ }
2488
+
2489
+ function inputTextBridgePayload(text, options = {}) {
2490
+ const payload = { text };
2491
+ const actionId = options.runtimeActionId ?? options.requestId;
2492
+ if (actionId != null) payload.actionId = String(actionId);
2493
+ const hasX = Object.prototype.hasOwnProperty.call(options, 'tapX');
2494
+ const hasY = Object.prototype.hasOwnProperty.call(options, 'tapY');
2495
+ if (options.nativeTarget) {
2496
+ if (hasX || hasY) throw Object.assign(new Error('Native target and coordinates are mutually exclusive.'), { code: 'conflicting_input_target' });
2497
+ return { ...payload, ...options.nativeTarget };
2498
+ }
2499
+ if (!hasX && !hasY) return payload;
2500
+ if (hasX !== hasY) throw Object.assign(new Error('x_y_must_be_provided_together'), { code: 'x_y_must_be_provided_together' });
2501
+ const coordinate = (value) => {
2502
+ if ((typeof value !== 'number' && typeof value !== 'string')
2503
+ || (typeof value === 'string' && !value.trim())) return NaN;
2504
+ return Number(value);
2505
+ };
2506
+ const x = coordinate(options.tapX);
2507
+ const y = coordinate(options.tapY);
2508
+ // Android validates the coordinates after conversion to Float.
2509
+ if (!Number.isFinite(Math.fround(x)) || !Number.isFinite(Math.fround(y))) {
2510
+ throw Object.assign(new Error('invalid_input_coordinates'), { code: 'invalid_input_coordinates' });
2511
+ }
2512
+ payload.x = x;
2513
+ payload.y = y;
2514
+ return payload;
2515
+ }
2516
+
2517
+ async function swipe(ctx, startX, startY, endX, endY, durationMs) {
2518
+ const result = await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)], { mutation: true });
2519
+ return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs, ...executionFields(result) };
2520
+ }
2521
+
2522
+ async function gestureNative(ctx, payload, options = {}, dependencies = {}) {
2523
+ const tree = await (dependencies.bridgeTree || bridgeTree)(ctx);
2524
+ const selected = selectNativeNode(tree, payload);
2525
+ if (!selected.ok) return { ...selected, ambiguous: false };
2526
+ const target = nativeTargetRequest(selected.node, payload.selector);
2527
+ if (!target.ok) return target;
2528
+ return nativeGesture(ctx, { ...payload, ...target.request, durationMs: payload.durationMs ?? 400,
2529
+ actionId: options.runtimeActionId ?? options.requestId ?? randomUUID() }, dependencies);
2530
+ }
2531
+
2532
+ async function nativeGesture(ctx, payload, dependencies = {}) {
2533
+ checkExecution();
2534
+ if (!ctx.explicitPackageName) return { ok: false, error: 'native_target_requires_app_scope', dispatched: false, ambiguous: false };
2535
+ const foreground = await (dependencies.foregroundWindow || foregroundWindow)(ctx);
2536
+ if (!foreground.ok || foreground.packageName !== ctx.packageName) return {
2537
+ ok: false, error: foreground.ok ? 'foreground_package_mismatch' : 'foreground_probe_failed', dispatched: false, ambiguous: false,
2538
+ };
2539
+ const result = await nativeAction({ ...ctx, httpTimeoutMs: payload.durationMs + 3500 }, '/v1/action/gesture-target', payload, dependencies);
2540
+ return { ...result, transport: 'bridge' };
2541
+ }
2542
+
2543
+ async function nativeAction(ctx, path, payload, dependencies = {}) {
2544
+ checkExecution();
2545
+ const status = await (dependencies.bridgeStatus || bridgeStatus)(ctx, { full: true });
2546
+ const runtime = { executionSchema: status?.debugBridge?.nativeExecutionSchema, runtimeEpoch: status?.debugBridge?.runtimeEpoch };
2547
+ const identity = { actionId: payload.actionId ?? randomUUID(), runtimeEpoch: runtime.runtimeEpoch };
2548
+ const target = { packageName: ctx.packageName, adb: ctx.adb, ...(ctx.explicitPort ? { port: ctx.port } : {}) };
2549
+ const requestCtx = { ...ctx };
2550
+ return runDeviceEffect({ kind: 'native', ...identity, target }, () => executeNativeAction({ runtime, payload,
2551
+ actionId: identity.actionId, timeoutMs: ctx.httpTimeoutMs,
2552
+ send: request => (dependencies.bridgePost || bridgePost)(requestCtx, path, request),
2553
+ cancel: identity => dependencies.cancelBridgePost ? dependencies.cancelBridgePost(requestCtx, identity)
2554
+ : httpPost(bridgeUrl(requestCtx, '/v1/action/cancel'), identity, 4000).then(JSON.parse),
2555
+ }), result => nativeSettlementProof(result, identity));
2556
+ }
2557
+
2558
+ async function nativeCompletion(ctx, identity) {
2559
+ await bridgeGet(ctx, '/v1/status');
2560
+ return bridgePost(ctx, '/v1/action/cancel', identity);
2561
+ }
2562
+
2563
+ async function keyevent(ctx, keyCode) {
2564
+ const result = await adb(ctx, ['shell', 'input', 'keyevent', String(keyCode)], { mutation: true });
2565
+ return { ok: true, transport: 'adb', keyCode, ...executionFields(result) };
2566
+ }
2567
+
2568
+ async function logcat(ctx, options) {
2569
+ const follow = Boolean(options.follow || options.live);
2570
+ const args = ['logcat', '-v', String(options.logcatFormat || options.format || 'threadtime')];
2571
+ const cleared = options.clear || options.clearFirst
2572
+ ? await adb(ctx, ['shell', 'logcat', '-c'], { mutation: true }) : null;
2573
+ if (!follow) {
2574
+ args.push('-d');
2575
+ }
2576
+ const since = options.logcatSince || options.since;
2577
+ if (since) {
2578
+ args.push('-T', String(since));
2579
+ } else if (!follow) {
2580
+ args.push('-t', String(options.logcatLines || options.lines || 200));
2581
+ }
2582
+ if (options.logcatFilter) {
2583
+ args.push(...String(options.logcatFilter).split(',').map((value) => value.trim()).filter(Boolean));
2584
+ }
2585
+ const text = follow
2586
+ ? await adbFollow(ctx, args, Number(options.durationMs || Number(options.durationSec || 5) * 1000))
2587
+ : (await adb(ctx, args)).stdout;
2588
+ const pid = await resolveLogcatPid(ctx, options);
2589
+ const output = filterLogcat(text, { ...options, pid });
2590
+ return cleared ? { ok: true, text: output, ...executionFields(cleared) } : output;
2591
+ }
2592
+
2593
+ async function appopsSet(ctx, op, mode) {
2594
+ const result = await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode], { mutation: true });
2595
+ return {
2596
+ ok: true,
2597
+ packageName: ctx.packageName,
2598
+ op,
2599
+ mode,
2600
+ ...executionFields(result),
2601
+ };
2602
+ }
2603
+
2604
+ async function clearAppData(ctx, options = {}) {
2605
+ if (!ctx.explicitPackageName) throw new CommandError('target_required', 'clear-app-data requires packageName.', { field: 'packageName' });
2606
+ if (options.method === 'runtime') {
2607
+ const result = await bridgePost(ctx, '/v1/app/clear-data', {});
2608
+ return { ...result, packageName: ctx.packageName, method: 'runtime' };
2609
+ }
2610
+ const result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName), { mutation: true });
2611
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
2612
+ return { ok: /^Success\b/im.test(output), packageName: ctx.packageName, action: 'clear-app-data',
2613
+ method: 'pm-clear', output, ...executionFields(result), ...(/^Success\b/im.test(output) ? {} : { error: 'clear_app_data_failed' }) };
2614
+ }
2615
+
2616
+ function clearAppDataAdbArgs(packageName) {
2617
+ return ['shell', 'pm', 'clear', packageName];
2618
+ }
2619
+
2620
+ async function resolveLogcatPid(ctx, options) {
2621
+ if (options.pid && options.pid !== true && options.pid !== 'current') {
2622
+ return String(options.pid);
2623
+ }
2624
+ if (options.pid === 'current' || options.appPid || options.packagePid) {
2625
+ try {
2626
+ const result = await adb(ctx, ['shell', 'pidof', '-s', ctx.packageName]);
2627
+ return result.stdout.trim().split(/\s+/).filter(Boolean)[0] || '';
2628
+ } catch (_) {
2629
+ return '';
2630
+ }
2631
+ }
2632
+ return '';
2633
+ }
2634
+
2635
+ function filterLogcat(text, options) {
2636
+ const tags = splitCsv(options.tag || options.tags);
2637
+ const grep = options.grep ? String(options.grep) : '';
2638
+ const grepCaseSensitive = Boolean(options.grepCaseSensitive);
2639
+ const minLevel = priorityValue(options.level || options.minLevel || '');
2640
+ const pid = options.pid ? String(options.pid) : '';
2641
+ const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
2642
+ if (requiresAppPid && !pid) {
2643
+ return '';
2644
+ }
2645
+ const lines = String(text || '').split(/\r?\n/);
2646
+ const filtered = [];
2647
+ let previousIncluded = false;
2648
+ for (const line of lines) {
2649
+ if (!line) continue;
2650
+ const parsed = parseLogcatLine(line);
2651
+ if (!parsed) {
2652
+ if (previousIncluded) filtered.push(line);
2653
+ continue;
2654
+ }
2655
+ let include = true;
2656
+ if (pid && parsed.pid !== pid) include = false;
2657
+ if (tags.length && !tags.includes(parsed.tag)) include = false;
2658
+ if (minLevel >= 0 && priorityValue(parsed.priority) < minLevel) include = false;
2659
+ if (grep) {
2660
+ include = include && (
2661
+ grepCaseSensitive
2662
+ ? line.includes(grep)
2663
+ : line.toLowerCase().includes(grep.toLowerCase())
2664
+ );
2665
+ }
2666
+ previousIncluded = include;
2667
+ if (include) filtered.push(line);
2668
+ }
2669
+ const limit = Number(options.limitLines || options.outputLines || 0);
2670
+ const result = limit > 0 && filtered.length > limit ? filtered.slice(-limit) : filtered;
2671
+ return result.join('\n');
2672
+ }
2673
+
2674
+ function parseLogcatLine(line) {
2675
+ const match = /^\d\d-\d\d\s+\d\d:\d\d:\d\d\.\d+\s+(\d+)\s+(\d+)\s+([VDIWEAF])\s+([^:]+):\s?(.*)$/.exec(line);
2676
+ if (!match) return null;
2677
+ return {
2678
+ pid: match[1],
2679
+ tid: match[2],
2680
+ priority: match[3],
2681
+ tag: match[4].trim(),
2682
+ message: match[5],
2683
+ };
2684
+ }
2685
+
2686
+ function priorityValue(value) {
2687
+ const normalized = String(value || '').trim().toUpperCase();
2688
+ return { V: 0, D: 1, I: 2, W: 3, E: 4, F: 5, A: 5 }[normalized] ?? -1;
2689
+ }
2690
+
2691
+ function splitCsv(value) {
2692
+ return String(value || '').split(',').map((item) => item.trim()).filter(Boolean);
2693
+ }
2694
+
2695
+ async function adbFollow(ctx, args, durationMs) {
2696
+ const boundedDurationMs = Math.max(500, Math.min(durationMs || 5000, 60000));
2697
+ try {
2698
+ return (await execFileBounded(ctx.adb, adbArgs(ctx, args), {
2699
+ timeoutMs: boundedDurationMs, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, windowsHide: true,
2700
+ })).stdout;
2701
+ } catch (error) {
2702
+ // The collection duration is a successful end; execution cancellation and
2703
+ // provider failures are not successful partial log collections.
2704
+ if (error.code === 'provider_timeout') return error.stdout;
2705
+ throw error;
2706
+ }
2707
+ }
2708
+
2709
+ async function launchApp(ctx, options = {}) {
2710
+ if (options.component || options.activity) {
2711
+ return launchActivity(ctx, options);
2712
+ }
2713
+
2714
+ const candidates = await launcherActivityCandidates(ctx);
2715
+ if (candidates.length === 0) {
2716
+ return {
2717
+ ok: false,
2718
+ error: 'launcher_not_found',
2719
+ packageName: ctx.packageName,
2720
+ launcherCandidates: candidates,
2721
+ };
2722
+ }
2723
+ if (candidates.length > 1) {
2724
+ return {
2725
+ ok: false,
2726
+ error: 'launcher_ambiguous',
2727
+ packageName: ctx.packageName,
2728
+ launcherCandidates: candidates,
2729
+ suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
2730
+ };
2731
+ }
2732
+
2733
+ return startActivity(ctx, candidates[0], options, {
2734
+ packageName: ctx.packageName,
2735
+ launcherCandidates: candidates,
2736
+ });
2737
+ }
2738
+
2739
+ async function launchActivity(ctx, options = {}) {
2740
+ const component = normalizeActivityComponent(
2741
+ ctx.packageName,
2742
+ options.component || requiredString(options.activity, 'activity'),
2743
+ );
2744
+ return startActivity(ctx, component, options, { packageName: ctx.packageName });
2745
+ }
2746
+
2747
+ async function launcherActivityCandidates(ctx) {
2748
+ const result = await adb(ctx, [
2749
+ 'shell',
2750
+ 'cmd',
2751
+ 'package',
2752
+ 'query-activities',
2753
+ '--brief',
2754
+ '-a',
2755
+ 'android.intent.action.MAIN',
2756
+ '-c',
2757
+ 'android.intent.category.LAUNCHER',
2758
+ ctx.packageName,
2759
+ ]);
2760
+ return parseLauncherActivityCandidates(result.stdout);
2761
+ }
2762
+
2763
+ function parseLauncherActivityCandidates(stdout) {
2764
+ const candidates = [];
2765
+ for (const line of String(stdout || '').split(/\r?\n/)) {
2766
+ const candidate = line.trim();
2767
+ if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
2768
+ candidates.push(candidate);
2769
+ }
2770
+ }
2771
+ return candidates;
2772
+ }
2773
+
2774
+ function normalizeActivityComponent(packageName, activityOrComponent) {
2775
+ const value = requiredString(activityOrComponent, 'activity');
2776
+ if (value.includes('/')) return value;
2777
+ return `${packageName}/${value}`;
2778
+ }
2779
+
2780
+ async function startActivity(ctx, component, options = {}, extraResult = {}, dependencies = {}) {
2781
+ const runAdb = dependencies.adb || adb;
2782
+ const readForeground = dependencies.foregroundWindow || foregroundWindow;
2783
+ const wait = dependencies.sleep || sleep;
2784
+ const args = buildAmStartArgs(component, options);
2785
+ const result = await runAdb(ctx, args, { mutation: true });
2786
+ const launched = {
2787
+ ok: true,
2788
+ transport: 'adb',
2789
+ component,
2790
+ ...extraResult,
2791
+ stdout: result.stdout.trim(),
2792
+ stderr: result.stderr.trim(),
2793
+ ...executionFields(result),
2794
+ };
2795
+ if (!ctx.packageName) return launched;
2796
+ const deadline = Date.now() + Number(options.foregroundTimeoutMs || 8000);
2797
+ let foreground = null;
2798
+ while (Date.now() < deadline) {
2799
+ foreground = await readForeground(ctx);
2800
+ if (foreground && foreground.ok && foreground.packageName === ctx.packageName) {
2801
+ return { ...launched, foreground };
2802
+ }
2803
+ await wait(200);
2804
+ }
2805
+ return {
2806
+ ...launched,
2807
+ ok: false,
2808
+ error: 'foreground_package_mismatch',
2809
+ foreground,
2810
+ };
2811
+ }
2812
+
2813
+ function buildAmStartArgs(component, options = {}) {
2814
+ const args = ['shell', 'am', 'start', '-W'];
2815
+ if (booleanOption(options.clearTask)) args.push('--activity-clear-task');
2816
+ if (options.action) args.push('-a', options.action);
2817
+ for (const category of optionList(options.category)) {
2818
+ args.push('-c', category);
2819
+ }
2820
+ if (options.data) args.push('-d', options.data);
2821
+ for (const extra of parseStartExtras(options.extra)) {
2822
+ args.push('-e', extra.key, extra.value);
2823
+ }
2824
+ args.push('-n', component);
2825
+ return args;
2826
+ }
2827
+
2828
+ function parseStartExtras(rawExtras) {
2829
+ return optionList(rawExtras).map((rawExtra) => {
2830
+ const value = String(rawExtra);
2831
+ const separator = value.indexOf('=');
2832
+ if (separator <= 0) {
2833
+ throw new Error('extra must use key=value');
2834
+ }
2835
+ return {
2836
+ key: value.slice(0, separator),
2837
+ value: value.slice(separator + 1),
2838
+ };
2839
+ });
2840
+ }
2841
+
2842
+ function optionList(value) {
2843
+ if (value === undefined || value === null || value === false || value === '') return [];
2844
+ return Array.isArray(value) ? value : [value];
2845
+ }
2846
+
2847
+ function sleep(ms) {
2848
+ return executionSleep(ms);
2849
+ }
2850
+
2851
+ function requiredString(value, name) {
2852
+ if (typeof value !== 'string' || value.length === 0) throw new Error(`${name} is required`);
2853
+ return value;
2854
+ }
2855
+
2856
+ function requiredInputString(value, name) {
2857
+ if (typeof value !== 'string') throw new Error(`${name} is required`);
2858
+ return value;
2859
+ }
2860
+
2861
+ function requiredNumber(value, name) {
2862
+ const number = Number(value);
2863
+ if (!Number.isFinite(number)) throw new Error(`${name} is required`);
2864
+ return number;
2865
+ }
2866
+
2867
+ module.exports = {
2868
+ executeProviderCommand,
2869
+ buildBridgeFailureResult,
2870
+ bridgeTree,
2871
+ createBridgeContext,
2872
+ flutterTapPayload,
2873
+ flutterAction,
2874
+ flutterCompletion,
2875
+ nativeCompletion,
2876
+ h5Completion,
2877
+ h5Eval,
2878
+ h5Dom,
2879
+ h5Control,
2880
+ nativeAction,
2881
+ flutterNodes,
2882
+ foregroundWindow,
2883
+ tapText,
2884
+ tapNative,
2885
+ waitText,
2886
+ tapUiaText,
2887
+ launchApp,
2888
+ startActivity,
2889
+ inputText,
2890
+ inputNativeText,
2891
+ inputTextBridgePayload,
2892
+ keyevent,
2893
+ nativeGesture,
2894
+ gestureNative,
2895
+ uiaTree,
2896
+ uiaTreeOnce,
2897
+ uiaTap,
2898
+ findUiaNodeByAny,
2899
+ bridgeRequest,
2900
+ bridgeNodeTarget,
2901
+ clearAppDataAdbArgs,
2902
+ artifactTimestamp,
2903
+ compactBridgeTree,
2904
+ compactStatus,
2905
+ compactUiaTree,
2906
+ defaultArtifactDirectory,
2907
+ defaultArtifactPath,
2908
+ findTappableNodeByText,
2909
+ filterLogcat,
2910
+ firstErrorLine,
2911
+ nodeTapState,
2912
+ normalizeBridgeError,
2913
+ normalizeActivityComponent,
2914
+ parseWebViewDevToolsSockets,
2915
+ parseKeyboardState,
2916
+ parsePackagePidsFromPs,
2917
+ parseLauncherActivityCandidates,
2918
+ parseStartExtras,
2919
+ parseUiaBounds,
2920
+ parseUiaViewport,
2921
+ parseComponentFromWindowLine,
2922
+ parseForegroundWindow,
2923
+ chooseWebViewDevToolsSocket,
2924
+ chooseWebViewPage,
2925
+ shapeNetworkCapture,
2926
+ compactNetworkRecord,
2927
+ pruneGeneratedArtifacts,
2928
+ requiredInputString,
2929
+ shouldDismissKeyboardForPoint,
2930
+ screenshotOutputPath,
2931
+ swipe,
2932
+ tap,
2933
+ verifyBridgeTargetPackage,
2934
+ };