mobile-debug-mcp 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +20 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,81 @@
1
+ import { existsSync } from 'fs'
2
+ import path from 'path'
3
+ import { execCmd } from '../exec.js'
4
+ import { spawnSync } from 'child_process'
5
+ import type { SpawnOptions } from 'child_process'
6
+ export function findInPath(cmd: string): string | null {
7
+ try {
8
+ // prefer command -v for POSIX
9
+ const res = spawnSync('command', ['-v', cmd], { encoding: 'utf8' })
10
+ if (res.status === 0 && res.stdout) return res.stdout.trim()
11
+ } catch (e: unknown) { console.debug(`[findInPath] command -v ${cmd} failed: ${String(e)}`) }
12
+ try {
13
+ const res = spawnSync('which', [cmd], { encoding: 'utf8' })
14
+ if (res.status === 0 && res.stdout) return res.stdout.trim()
15
+ } catch (e: unknown) { console.debug(`[findInPath] which ${cmd} failed: ${String(e)}`) }
16
+ return null
17
+ }
18
+
19
+ export function resolveAdbCmd(): string {
20
+ // Priority: explicit env ADB_PATH -> PATH -> known SDK locations -> fallback to 'adb'
21
+ if (process.env.ADB_PATH && process.env.ADB_PATH.trim()) return process.env.ADB_PATH
22
+ const found = findInPath('adb')
23
+ if (found) return found
24
+ const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME
25
+ if (sdkRoot) {
26
+ const candidate = path.join(sdkRoot, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
27
+ if (existsSync(candidate)) return candidate
28
+ }
29
+ // common macOS user SDK path
30
+ const homeSdk = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
31
+ if (existsSync(homeSdk)) return homeSdk
32
+ return 'adb'
33
+ }
34
+
35
+ export function getAdbCmd() { return resolveAdbCmd() }
36
+
37
+ export function ensureAdbAvailable() {
38
+ const adb = resolveAdbCmd()
39
+ try {
40
+ const res = spawnSync(adb, ['--version'], { encoding: 'utf8' })
41
+ if (res.status === 0) {
42
+ return { adbCmd: adb, ok: true, version: (res.stdout || res.stderr || '').trim() }
43
+ }
44
+ return { adbCmd: adb, ok: false, error: (res.stderr || res.stdout || '').trim() }
45
+ } catch (err: unknown) {
46
+ return { adbCmd: adb, ok: false, error: String(err) }
47
+ }
48
+ }
49
+
50
+ export function getAdbArgs(args: string[], deviceId?: string): string[] {
51
+ if (deviceId) {
52
+ return ['-s', deviceId, ...args]
53
+ }
54
+ return args
55
+ }
56
+
57
+ export function getAdbTimeout(args: string[], customTimeout?: number): number {
58
+ if (typeof customTimeout === 'number' && !isNaN(customTimeout)) return customTimeout
59
+ const envTimeout = parseInt(process.env.MCP_ADB_TIMEOUT || process.env.ADB_TIMEOUT || '', 10)
60
+ if (!isNaN(envTimeout) && envTimeout > 0) return envTimeout
61
+ if (args.includes('logcat')) return 10000
62
+ if (args.includes('uiautomator') && args.includes('dump')) return 20000
63
+ return 120000
64
+ }
65
+
66
+ export type SpawnOptionsWithTimeout = SpawnOptions & { timeout?: number }
67
+
68
+ export async function execAdb(args: string[], deviceId?: string, options: SpawnOptionsWithTimeout = {}): Promise<string> {
69
+ const adbArgs = getAdbArgs(args, deviceId)
70
+ const timeoutMs = getAdbTimeout(args, options.timeout)
71
+ const res = await execCmd(getAdbCmd(), adbArgs, { timeout: timeoutMs, env: options.env, cwd: typeof options.cwd === 'string' ? options.cwd : undefined, shell: !!options.shell })
72
+ if (res.exitCode !== 0) throw new Error(res.stderr || `Command failed with code ${res.exitCode}`)
73
+ return res.stdout
74
+ }
75
+
76
+ export async function spawnAdb(args: string[], deviceId?: string, options: SpawnOptionsWithTimeout = {}): Promise<{ stdout: string, stderr: string, code: number | null }> {
77
+ const adbArgs = getAdbArgs(args, deviceId)
78
+ const timeoutMs = getAdbTimeout(args, options.timeout)
79
+ const res = await execCmd(getAdbCmd(), adbArgs, { timeout: timeoutMs, env: options.env, cwd: typeof options.cwd === 'string' ? options.cwd : undefined, shell: !!options.shell })
80
+ return { stdout: res.stdout, stderr: res.stderr, code: res.exitCode }
81
+ }
@@ -0,0 +1,132 @@
1
+ import { promises as fsPromises, existsSync } from 'fs'
2
+ import path from 'path'
3
+ import { detectJavaHome } from '../java.js'
4
+ import { checkGradle } from '../../system/gradle.js'
5
+ import { resolveAdbCmd } from './commands.js'
6
+ export type GradleEnvOverrides = Record<string, string | undefined>
7
+
8
+ export function mergeEnv(overrides?: GradleEnvOverrides) {
9
+ const env: Record<string, string> = {}
10
+ for (const [key, value] of Object.entries(process.env)) {
11
+ if (typeof value === 'string') env[key] = value
12
+ }
13
+ for (const [key, value] of Object.entries(overrides || {})) {
14
+ if (typeof value === 'string') env[key] = value
15
+ }
16
+ return env
17
+ }
18
+
19
+ export async function prepareGradle(projectPath: string, envOverrides: GradleEnvOverrides = {}): Promise<{ execCmd: string, gradleArgs: string[], spawnOpts: import('node:child_process').SpawnOptions }> {
20
+ const env = mergeEnv(envOverrides)
21
+ const gradlewPath = path.join(projectPath, 'gradlew')
22
+ const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle'
23
+ const execCmd = existsSync(gradlewPath) ? gradlewPath : gradleCmd
24
+
25
+ // Start with a default task; callers may append/override via env flags
26
+ const gradleArgs: string[] = [env.MCP_GRADLE_TASK || 'assembleDebug']
27
+
28
+ // Respect generic MCP_BUILD_JOBS and Android-specific MCP_GRADLE_WORKERS
29
+ const workers = env.MCP_GRADLE_WORKERS || env.MCP_BUILD_JOBS
30
+ if (workers) {
31
+ gradleArgs.push(`--max-workers=${workers}`)
32
+ }
33
+
34
+ // Respect gradle cache env: default enabled; set MCP_GRADLE_CACHE=0 to disable
35
+ if (env.MCP_GRADLE_CACHE === '0') {
36
+ gradleArgs.push('-Dorg.gradle.caching=false')
37
+ }
38
+
39
+ const detectedJavaHome = await detectJavaHome().catch(() => undefined)
40
+ // Check for problematic org.gradle.java.home entries (env or properties) and avoid passing invalid values to Gradle
41
+ let gradleCheck
42
+ try {
43
+ gradleCheck = await checkGradle()
44
+ } catch {
45
+ gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] }
46
+ }
47
+
48
+ // Ensure child processes can find Android platform-tools (adb, etc.) by
49
+ // prepending the platform-tools directory to PATH for spawned processes.
50
+ const adbPath = resolveAdbCmd()
51
+ let platformToolsDir: string | undefined = undefined
52
+ try {
53
+ if (adbPath && adbPath !== 'adb' && existsSync(adbPath)) {
54
+ platformToolsDir = path.dirname(adbPath)
55
+ }
56
+ } catch (e: unknown) { console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`) }
57
+
58
+ const pathParts: string[] = []
59
+ configureJava(env, gradleArgs, pathParts, detectedJavaHome, gradleCheck)
60
+
61
+ configurePlatformTools(env, pathParts, platformToolsDir)
62
+
63
+ if (pathParts.length > 0) {
64
+ env.PATH = `${pathParts.join(path.delimiter)}${path.delimiter}${env.PATH || ''}`
65
+ }
66
+
67
+ try { delete env.SHELL } catch (e: unknown) { console.debug('[prepareGradle] failed to delete SHELL from env:', String(e)) }
68
+
69
+ const useWrapper = existsSync(gradlewPath)
70
+ const spawnOpts: import('node:child_process').SpawnOptions = { cwd: projectPath, env }
71
+ if (useWrapper) {
72
+ try { await fsPromises.chmod(gradlewPath, 0o755) } catch (e: unknown) { console.debug('[prepareGradle] chmod failed for gradlew:', String(e)) }
73
+ // Execute the wrapper directly without a shell to avoid shell tokenization of args (spaces in paths)
74
+ spawnOpts.shell = false
75
+ } else {
76
+ // Prefer executing gradle directly without invoking a shell to preserve argument boundaries
77
+ spawnOpts.shell = false
78
+ }
79
+
80
+ return { execCmd, gradleArgs, spawnOpts }
81
+ }
82
+
83
+ function configureJava(env: Record<string, string>, gradleArgs: string[], pathParts: string[], detectedJavaHome: string | undefined, gradleCheck: { gradleJavaHome?: string; gradleValid: boolean }) {
84
+ // Prefer a detected (validated) Java home from the system/IDE
85
+ if (detectedJavaHome) {
86
+ if (env.JAVA_HOME !== detectedJavaHome) {
87
+ env.JAVA_HOME = detectedJavaHome
88
+ }
89
+ const javaBin = path.join(detectedJavaHome, 'bin')
90
+ pathParts.push(javaBin)
91
+ gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
92
+ gradleArgs.push('--no-daemon')
93
+ env.GRADLE_JAVA_HOME = detectedJavaHome
94
+ } else if (gradleCheck && gradleCheck.gradleJavaHome) {
95
+ // There's an org.gradle.java.home configured somewhere (env or gradle.properties)
96
+ if (gradleCheck.gradleValid) {
97
+ const p = gradleCheck.gradleJavaHome as string
98
+ const javaBin = path.join(p, 'bin')
99
+ if (!env.PATH || !env.PATH.includes(javaBin)) pathParts.push(javaBin)
100
+ gradleArgs.push(`-Dorg.gradle.java.home=${p}`)
101
+ gradleArgs.push('--no-daemon')
102
+ env.GRADLE_JAVA_HOME = p
103
+ } else {
104
+ // Invalid gradle java home detected: avoid passing it to Gradle and remove from spawn env
105
+ console.debug(`[prepareGradle] Invalid org.gradle.java.home detected (${gradleCheck.gradleJavaHome}); removing from spawn env to avoid Gradle error.`)
106
+ try { delete env.GRADLE_JAVA_HOME } catch { }
107
+ }
108
+ }
109
+
110
+ }
111
+
112
+ function configurePlatformTools(env: Record<string, string>, pathParts: string[], platformToolsDir: string | undefined) {
113
+ if (platformToolsDir) {
114
+ // Prepend platform-tools so gradle and child tools find adb without modifying global env
115
+ if (!env.PATH || !env.PATH.includes(platformToolsDir)) {
116
+ pathParts.push(platformToolsDir)
117
+ }
118
+ } else if (process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME) {
119
+ const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || ''
120
+ const candidate = path.join(sdkRoot, 'platform-tools')
121
+ if (existsSync(candidate) && (!env.PATH || !env.PATH.includes(candidate))) {
122
+ pathParts.push(candidate)
123
+ }
124
+ } else {
125
+ // also try common user sdk location
126
+ const homeSdkTools = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools')
127
+ if (existsSync(homeSdkTools) && (!env.PATH || !env.PATH.includes(homeSdkTools))) {
128
+ pathParts.push(homeSdkTools)
129
+ }
130
+ }
131
+
132
+ }
@@ -0,0 +1,103 @@
1
+ interface AndroidLogEntry {
2
+ timestamp: string; level: string; tag: string; message: string; _iso: string | null
3
+ crash: boolean; exception?: string; pid?: string; tid?: string
4
+ }
5
+ export function parseLogLine(line: string) { // Collapse internal newlines so multiline stack traces are parseable as a single entry
6
+ const rawLine = line
7
+ const normalizedLine = rawLine.replace(/\r?\n/g, ' ')
8
+ const entry: AndroidLogEntry = { timestamp: '', level: '', tag: '', message: rawLine, _iso: null, crash: false }
9
+
10
+ const nowYear = new Date().getFullYear()
11
+
12
+ const tryIso = (ts: string) => {
13
+ if (!ts) return null
14
+ // If it's already ISO
15
+ if (/^\d{4}-\d{2}-\d{2}T/.test(ts)) return ts
16
+ // If format MM-DD HH:MM:SS(.sss)
17
+ const m = ts.match(/^(\d{2})-(\d{2})\s+(\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)$/)
18
+ if (m) {
19
+ const month = m[1]
20
+ const day = m[2]
21
+ const time = m[3]
22
+ const candidate = `${nowYear}-${month}-${day}T${time}`
23
+ const d = new Date(candidate)
24
+ if (!isNaN(d.getTime())) return d.toISOString()
25
+ }
26
+ // If format YYYY-MM-DD HH:MM:SS(.sss)
27
+ const m2 = ts.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)$/)
28
+ if (m2) {
29
+ const candidate = `${m2[1]}T${m2[2]}`
30
+ const d = new Date(candidate)
31
+ if (!isNaN(d.getTime())) return d.toISOString()
32
+ }
33
+ return null
34
+ }
35
+
36
+ // Patterns to try (ordered)
37
+ const patterns: Array<{ re: RegExp, groups: string[] }> = [
38
+ // MM-DD HH:MM:SS.mmm PID TID LEVEL/Tag: msg
39
+ { re: /^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+(\d+)\s+([VDIWE])\/([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'tid', 'level', 'tag', 'msg'] },
40
+ // MM-DD HH:MM:SS.mmm PID TID LEVEL Tag: msg (space between level and tag)
41
+ { re: /^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+(\d+)\s+([VDIWE])\s+([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'tid', 'level', 'tag', 'msg'] },
42
+ // YYYY-MM-DD full date with PID TID LEVEL/Tag
43
+ { re: /^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+(\d+)\s+([VDIWE])\/([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'tid', 'level', 'tag', 'msg'] },
44
+ // YYYY-MM-DD with space separation
45
+ { re: /^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+(\d+)\s+([VDIWE])\s+([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'tid', 'level', 'tag', 'msg'] },
46
+ // MM-DD PID LEVEL/Tag: msg
47
+ { re: /^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+([VDIWE])\/([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'level', 'tag', 'msg'] },
48
+ // MM-DD PID LEVEL Tag: msg (space)
49
+ { re: /^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s+(\d+)\s+([VDIWE])\s+([^:]+):\s*(.*)$/, groups: ['ts', 'pid', 'level', 'tag', 'msg'] },
50
+ // Short form LEVEL/Tag: msg
51
+ { re: /^([VDIWE])\/([^\(\:]+)(?:\([0-9]+\))?:\s*(.*)$/, groups: ['level', 'tag', 'msg'] },
52
+ // Short form LEVEL Tag: msg
53
+ { re: /^([VDIWE])\s+([^\(\:]+)(?:\([0-9]+\))?:\s*(.*)$/, groups: ['level', 'tag', 'msg'] },
54
+ ]
55
+
56
+ for (const p of patterns) {
57
+ const m = normalizedLine.match(p.re)
58
+ if (m) {
59
+ const g = p.groups
60
+ const vals: Record<string, string> = {}
61
+ for (let i = 0; i < g.length; i++) vals[g[i]] = m[i + 1]
62
+ const ts = vals.ts
63
+ if (ts) {
64
+ const iso = tryIso(ts)
65
+ if (iso) {
66
+ entry.timestamp = ts
67
+ entry._iso = iso
68
+ } else {
69
+ entry.timestamp = ts
70
+ }
71
+ }
72
+ if (vals.level) entry.level = vals.level
73
+ if (vals.tag) entry.tag = vals.tag.trim()
74
+ entry.message = vals.msg || entry.message
75
+ // Crash heuristics
76
+ const msg = (entry.message || '').toString()
77
+ const crash = /FATAL EXCEPTION/i.test(msg) || /\b([A-Za-z0-9_$.]+Exception)\b/.test(msg)
78
+ if (crash) {
79
+ entry.crash = true
80
+ const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/)
81
+ if (exMatch) entry.exception = exMatch[1]
82
+ }
83
+ return entry
84
+ }
85
+ }
86
+
87
+ // No pattern matched: attempt to extract level/tag like '... E/Tag: msg'
88
+ const alt = normalizedLine.match(/([VDIWE])\/([^:]+):\s*(.*)$/)
89
+ if (alt) {
90
+ entry.level = alt[1]
91
+ entry.tag = alt[2].trim()
92
+ entry.message = alt[3]
93
+ const msg = entry.message
94
+ const crash = /FATAL EXCEPTION/i.test(msg) || /\b([A-Za-z0-9_$.]+Exception)\b/.test(msg)
95
+ if (crash) {
96
+ entry.crash = true
97
+ const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/)
98
+ if (exMatch) entry.exception = exMatch[1]
99
+ }
100
+ }
101
+
102
+ return entry
103
+ }
@@ -0,0 +1,239 @@
1
+ export interface AndroidXMLNode {
2
+ [key: string]: unknown
3
+ '@_class'?: string; '@_text'?: string; '@_content-desc'?: string; '@_resource-id'?: string; '@_bounds'?: string
4
+ '@_clickable'?: string | boolean; '@_enabled'?: string | boolean; '@_visible-to-user'?: string | boolean
5
+ node?: AndroidXMLNode | AndroidXMLNode[]
6
+ }
7
+ import { UIElement, UIElementSemanticMetadata, UIElementState, UIResolutionSelector, SelectorConfidence } from "../../types.js"
8
+ export function parseBounds(bounds: string): [number, number, number, number] {
9
+ const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
10
+ if (match) {
11
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), parseInt(match[4])];
12
+ }
13
+ return [0, 0, 0, 0];
14
+ }
15
+
16
+ export function getCenter(bounds: [number, number, number, number]): [number, number] {
17
+ const [x1, y1, x2, y2] = bounds;
18
+ return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
19
+ }
20
+
21
+ export function parseBooleanAttr(value: unknown): boolean | null {
22
+ if (value === true || value === 'true') return true
23
+ if (value === false || value === 'false') return false
24
+ return null
25
+ }
26
+
27
+ export function parseNumberAttr(value: unknown): number | null {
28
+ if (typeof value === 'number' && Number.isFinite(value)) return value
29
+ if (typeof value !== 'string') return null
30
+ const parsed = Number(value)
31
+ return Number.isFinite(parsed) ? parsed : null
32
+ }
33
+
34
+ export function normalizeClassName(value: unknown): string {
35
+ return typeof value === 'string' ? value.trim().toLowerCase() : ''
36
+ }
37
+
38
+ export function inferAndroidRole(className: string): string | null {
39
+ if (/seekbar|slider/.test(className)) return 'slider'
40
+ if (/stepper|numberpicker/.test(className)) return 'stepper'
41
+ if (/spinner|dropdown/.test(className)) return 'dropdown'
42
+ if (/segment|tablayout/.test(className)) return 'segmented_control'
43
+ if (/switch|toggle/.test(className)) return 'switch'
44
+ if (/checkbox/.test(className)) return 'checkbox'
45
+ if (/radiobutton|radio/.test(className)) return 'radio'
46
+ if (/edittext|textfield|search/.test(className)) return 'text_field'
47
+ if (/button|fab/.test(className)) return 'button'
48
+ if (/imageview|icon/.test(className)) return 'image'
49
+ if (/recyclerview|scroll|layout|viewgroup|frame/.test(className)) return 'container'
50
+ return null
51
+ }
52
+
53
+ export function buildAndroidSelectorConfidence(source: 'resource_id' | 'content_desc' | 'text' | 'class' | 'none'): SelectorConfidence | null {
54
+ switch (source) {
55
+ case 'resource_id':
56
+ return { score: 1, reason: 'resource_id' }
57
+ case 'content_desc':
58
+ return { score: 0.9, reason: 'content_description' }
59
+ case 'text':
60
+ return { score: 0.6, reason: 'text_match' }
61
+ case 'class':
62
+ return { score: 0.35, reason: 'class_match' }
63
+ default:
64
+ return null
65
+ }
66
+ }
67
+
68
+ export function buildAndroidSelector(text: string | null, contentDescription: string | null, resourceId: string | null, className: string): UIResolutionSelector | null {
69
+ if (resourceId) return { value: resourceId, confidence: buildAndroidSelectorConfidence('resource_id') }
70
+ if (contentDescription) return { value: contentDescription, confidence: buildAndroidSelectorConfidence('content_desc') }
71
+ if (text) return { value: text, confidence: buildAndroidSelectorConfidence('text') }
72
+ if (className) return { value: className, confidence: buildAndroidSelectorConfidence('class') }
73
+ return null
74
+ }
75
+
76
+ export function buildAndroidSemantic(clickable: boolean, className: string, role: string | null): UIElementSemanticMetadata {
77
+ const semantic: UIElementSemanticMetadata = {
78
+ is_clickable: clickable,
79
+ is_container: /recyclerview|scroll|layout|viewgroup|frame/.test(className)
80
+ }
81
+
82
+ if (role === 'slider') {
83
+ semantic.semantic_role = 'slider'
84
+ semantic.adjustable = true
85
+ semantic.supported_actions = ['adjust']
86
+ semantic.state_shape = 'continuous'
87
+ } else if (role === 'stepper') {
88
+ semantic.semantic_role = 'stepper'
89
+ semantic.adjustable = true
90
+ semantic.supported_actions = ['increment', 'decrement']
91
+ semantic.state_shape = 'discrete'
92
+ } else if (role === 'dropdown') {
93
+ semantic.semantic_role = 'dropdown'
94
+ semantic.supported_actions = ['tap', 'expand']
95
+ semantic.state_shape = 'semantic'
96
+ } else if (role === 'segmented_control') {
97
+ semantic.semantic_role = 'segmented_control'
98
+ semantic.supported_actions = ['tap']
99
+ semantic.state_shape = 'discrete'
100
+ } else if (clickable) {
101
+ semantic.supported_actions = ['tap']
102
+ }
103
+
104
+ return semantic
105
+ }
106
+
107
+ export function isSliderLikeAndroid(node: AndroidXMLNode): boolean {
108
+ const className = String(node['@_class'] || '').toLowerCase()
109
+ return /seekbar|slider|range/i.test(className)
110
+ }
111
+
112
+ export function extractAndroidState(node: AndroidXMLNode): UIElementState | null {
113
+ const checked = parseBooleanAttr(node['@_checked'])
114
+ const selectedFlag = parseBooleanAttr(node['@_selected'])
115
+ const focused = parseBooleanAttr(node['@_focused'])
116
+ const expanded = parseBooleanAttr(node['@_expanded'])
117
+ const enabled = parseBooleanAttr(node['@_enabled'])
118
+ const textValue = typeof node['@_text'] === 'string' && node['@_text'].trim().length > 0 ? node['@_text'] : null
119
+ const state: UIElementState = {}
120
+
121
+ if (checked !== null) state.checked = checked
122
+ if (selectedFlag !== null) {
123
+ state.selected = textValue || node['@_content-desc'] || true
124
+ }
125
+ if (focused !== null) state.focused = focused
126
+ if (expanded !== null) state.expanded = expanded
127
+ if (enabled !== null) state.enabled = enabled
128
+
129
+ if (textValue && /edittext|textfield|search/i.test(String(node['@_class'] || ''))) {
130
+ state.text_value = textValue
131
+ }
132
+
133
+ addNumericState(node, state, textValue)
134
+
135
+ return Object.keys(state).length > 0 ? state : null
136
+ }
137
+
138
+ export function traverseNode(node: AndroidXMLNode, elements: UIElement[], parentIndex: number = -1, depth: number = 0): number {
139
+ if (!node) return -1;
140
+
141
+ let currentIndex = -1;
142
+
143
+ if (node['@_class']) {
144
+ const { text, contentDescription, clickable, className, bounds, state, role, resourceId, stableId, testTag, selector, semantic, isUseful } = describeNode(node)
145
+
146
+ if (isUseful) {
147
+ const element: UIElement = {
148
+ text,
149
+ contentDescription,
150
+ type: className,
151
+ resourceId,
152
+ clickable,
153
+ enabled: node['@_enabled'] === 'true',
154
+ visible: true,
155
+ bounds,
156
+ center: getCenter(bounds),
157
+ depth,
158
+ state,
159
+ stable_id: stableId,
160
+ role,
161
+ test_tag: testTag,
162
+ selector,
163
+ semantic
164
+ };
165
+
166
+ if (parentIndex !== -1) {
167
+ element.parentId = parentIndex;
168
+ }
169
+
170
+ elements.push(element);
171
+ currentIndex = elements.length - 1;
172
+ }
173
+ }
174
+
175
+ const nextParentIndex = currentIndex !== -1 ? currentIndex : parentIndex;
176
+ const nextDepth = currentIndex !== -1 ? depth + 1 : depth;
177
+
178
+ const childrenIndices: number[] = [];
179
+
180
+ if (node.node) {
181
+ if (Array.isArray(node.node)) {
182
+ node.node.forEach((child: AndroidXMLNode) => {
183
+ const childIndex = traverseNode(child, elements, nextParentIndex, nextDepth);
184
+ if (childIndex !== -1) childrenIndices.push(childIndex);
185
+ });
186
+ } else {
187
+ const childIndex = traverseNode(node.node, elements, nextParentIndex, nextDepth);
188
+ if (childIndex !== -1) childrenIndices.push(childIndex);
189
+ }
190
+ }
191
+
192
+ if (currentIndex !== -1 && childrenIndices.length > 0) {
193
+ elements[currentIndex].children = childrenIndices;
194
+ }
195
+
196
+ return currentIndex;
197
+ }
198
+
199
+ function addNumericState(node: AndroidXMLNode, state: UIElementState, textValue: string | null) {
200
+ if (isSliderLikeAndroid(node)) {
201
+ const rawProgress = parseNumberAttr(node['@_progress'])
202
+ const max = parseNumberAttr(node['@_max'])
203
+ const fallbackValue = rawProgress ?? parseNumberAttr(node['@_value']) ?? parseNumberAttr(node['@_content-desc'])
204
+ const numericValue = rawProgress ?? fallbackValue
205
+ if (numericValue !== null) {
206
+ state.raw_value = numericValue
207
+ state.value_range = max !== null && max > 0 ? { min: 0, max } : null
208
+ state.value = max !== null && max > 0 ? Math.round((numericValue / max) * 100) : numericValue
209
+ }
210
+ } else {
211
+ const numericValue = parseNumberAttr(node['@_value'])
212
+ if (numericValue !== null) {
213
+ state.value = numericValue
214
+ state.raw_value = numericValue
215
+ } else if (textValue) {
216
+ state.value = textValue
217
+ }
218
+ }
219
+
220
+ }
221
+
222
+ function describeNode(node: AndroidXMLNode) {
223
+ const text = node['@_text'] || null;
224
+ const contentDescription = node['@_content-desc'] || null;
225
+ const clickable = node['@_clickable'] === 'true';
226
+ const className = String(node['@_class'] || 'unknown');
227
+ const bounds = parseBounds(node['@_bounds'] || '[0,0][0,0]');
228
+ const state = extractAndroidState(node);
229
+ const role = inferAndroidRole(normalizeClassName(className));
230
+ const resourceId = typeof node['@_resource-id'] === 'string' && node['@_resource-id'].trim().length > 0 ? node['@_resource-id'] : null
231
+ const stableId = resourceId ?? (typeof contentDescription === 'string' && contentDescription.trim().length > 0 ? contentDescription : null)
232
+ const testTag = stableId
233
+ const selector = buildAndroidSelector(text, contentDescription, resourceId, normalizeClassName(className))
234
+ const semantic = buildAndroidSemantic(clickable, normalizeClassName(className), role)
235
+
236
+ const isUseful = clickable || (text && text.length > 0) || (contentDescription && contentDescription.length > 0);
237
+
238
+ return { text, contentDescription, clickable, className, bounds, state, role, resourceId, stableId, testTag, selector, semantic, isUseful }
239
+ }