mobile-debug-mcp 0.34.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 (432) 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 +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  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 +16 -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 +27 -2
  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 +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -1,1008 +0,0 @@
1
- import { handleDebugger } from './debugger-handlers.js'
2
- import { debuggerContracts } from './debugger-schema.js'
3
- import { debuggerSessions } from '../debugger/sessions.js'
4
- import { withDebuggerGuard, guardDispatched } from '../debugger/guard.js'
5
- import { systemTraces, TraceError } from '../observe/system-trace.js'
6
- import { systemTraceTools, traceTargetSchema, traceStartSchema, traceControlSchema } from './system-trace-schema.js'
7
- import { currentEvidenceCorrelationId, bindSystemTrace, withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js'
8
- import { measure, withTiming } from '../utils/timing.js'
9
- import { randomUUID } from 'node:crypto'
10
- import { runWithBudget } from '../utils/operation-budget.js'
11
- import { journeySchema, tapElementControlsSchema } from './journey-schema.js'
12
- import { readinessSchema, readinessWaitSchema } from './readiness-schema.js'
13
- import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js'
14
- import { validateExpectedState } from './condition-schema.js'
15
- import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js'
16
- import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js'
17
- import type {
18
- InstallAppResponse,
19
- ResetAppDataResponse,
20
- TerminateAppResponse
21
- } from '../types.js'
22
- import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js'
23
- import { ToolsInteract } from '../interact/index.js'
24
- import { ToolsObserve } from '../observe/index.js'
25
- import { classifyActionOutcome } from '../interact/classify.js'
26
- import { ToolsNetwork } from '../network/index.js'
27
- import { getSystemStatus } from '../system/index.js'
28
- import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js'
29
- import {
30
- buildActionExecutionResult,
31
- captureActionFingerprint,
32
- getArrayArg,
33
- getBooleanArg,
34
- getNumberArg,
35
- getObjectArg,
36
- getStringArg,
37
- inferGenericFailure,
38
- inferScrollFailure,
39
- requireBooleanArg,
40
- requireNumberArg,
41
- requireObjectArg,
42
- requireStringArg,
43
- ToolCallArgs,
44
- ToolCallResult,
45
- ToolHandler,
46
- wrapResponse,
47
- wrapToolError
48
- } from './common.js'
49
-
50
- const systemTraceToolNames = new Set(systemTraceTools.map(tool => tool.name))
51
-
52
- type PlatformArg = 'android' | 'ios'
53
- type ProjectTypeArg = 'native' | 'kmp' | 'react-native' | 'flutter'
54
- type ExpectElementSelectorArg = { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
55
- type WaitForUiMatchArg = { index?: number }
56
- type WaitForUiRetryArg = { max_attempts?: number, backoff_ms?: number }
57
- type ScrollSelectorArg = { text?: string, resourceId?: string, contentDesc?: string, className?: string }
58
- type ClassifyNetworkRequestArg = { endpoint: string, status: 'success' | 'failure' | 'retryable' }
59
- type VerificationMode = 'none' | 'light' | 'full'
60
- type ResponseMode = 'compact' | 'debug'
61
-
62
- function interactionControls(args: ToolCallArgs) {
63
- const verificationMode = (getStringArg(args, 'verificationMode') as VerificationMode | undefined) ?? 'full'
64
- const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
65
- if (!['none', 'light', 'full'].includes(verificationMode) || !['compact', 'debug'].includes(responseMode)) throw new Error('INVALID_ARGUMENT')
66
- for (const key of ['verificationMode', 'responseMode']) if (args[key] !== undefined && typeof args[key] !== 'string') throw new Error('INVALID_ARGUMENT')
67
- for (const key of ['actionTimeoutMs', 'verificationTimeoutMs']) if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key]))) throw new Error('INVALID_ARGUMENT')
68
- const actionTimeoutMs = getNumberArg(args, 'actionTimeoutMs')
69
- const verificationTimeoutMs = getNumberArg(args, 'verificationTimeoutMs')
70
- return { verificationMode, responseMode,
71
- actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
72
- verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) }
73
-
74
- }
75
-
76
- function compactAction(result: any, verificationMode: VerificationMode, started = true, stateDelta?: unknown, totalMs = 0) {
77
- return {
78
- action_id: result.action_id,
79
- action_type: result.action_type,
80
- success: !!result.success,
81
- lifecycle_state: result.success ? (verificationMode === 'full' ? result.lifecycle_state : 'pending_verification') : 'failed',
82
- ...(result.failure_code ? { failure_code: result.failure_code } : {}),
83
- ...(typeof result.retryable === 'boolean' ? { retryable: result.retryable } : {}),
84
- delivery_status: result.success ? 'delivered' : (started ? 'unknown' : 'not_delivered'),
85
- dispatch_started: started,
86
- verification: { mode: verificationMode, status: result.success ? (verificationMode === 'none' ? 'not_requested' : verificationMode === 'light' ? 'observed' : (result.lifecycle_state === 'verified' ? 'verified' : 'observed')) : 'failed' },
87
- ...(stateDelta ? { state_delta: stateDelta } : {}),
88
- timing: { total_ms: totalMs }
89
- }
90
- }
91
-
92
- function correlatedHandler(name: string, handler: ToolHandler, action = false, journey = false): ToolHandler {
93
- return async args => {
94
- const ref = validateActionRef(args.actionRef)
95
- const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
96
- try { return await handler(args) }
97
- catch (error) { return error instanceof TraceError ? { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true } : wrapToolError(name, error) }
98
- })
99
- const item = result.content.find(item => item.type === 'text')
100
- if (item?.text) {
101
- const payload = JSON.parse(item.text)
102
- payload.evidence = evidence
103
- item.text = JSON.stringify(payload)
104
- }
105
- return result
106
- }
107
- }
108
-
109
- function actionHandler(type: string, execute: ToolHandler): ToolHandler {
110
- return correlatedHandler(type, async (args) => {
111
- const timing: Record<string, number> = {}
112
- return withTiming(timing, async () => {
113
- const started = performance.now()
114
- const controls = interactionControls(args)
115
- const expected = validateExpectedState(type, args)
116
- const condition = expected ? createConditionalAction(args.platform as PlatformArg, args.deviceId as string, expected) : undefined
117
- const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000)
118
- let dispatchStarted = false
119
- let result: any
120
- const dispatchWithBudget = (timeoutMs: number) => runWithBudget(timeoutMs, async (budget) => {
121
- try {
122
- if (!condition) return payloadOf(await execute(args))
123
- return await withConditionalAction(condition, async () => {
124
- const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId })
125
- return withResolvedDevice(device, async () => payloadOf(await execute(args)))
126
- })
127
- }
128
- finally { dispatchStarted = budget.dispatchStarted }
129
- })
130
- try {
131
- // Omitted controls retain the existing adapter budgets.
132
- result = args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined
133
- ? await dispatchWithBudget(Infinity) : await dispatchWithBudget(budgetMs)
134
- if (result.success) dispatchStarted = true
135
- } catch (error) {
136
- if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error))) return wrapToolError(type, error)
137
- result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) }
138
- }
139
- const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status)
140
- if (blocked) {
141
- dispatchStarted = false
142
- result = buildActionExecutionResult({
143
- actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
144
- selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
145
- success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
146
- failure: { failureCode: conditionFailureCode(condition), retryable: false },
147
- traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
148
- })
149
- }
150
- if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? '')) result.failure_code = 'ACTION_TIMEOUT'
151
- if (result.failure_code === 'ACTION_TIMEOUT') result.retryable = !dispatchStarted
152
- if (!result.success && typeof result.retryable !== 'boolean') result.retryable = !dispatchStarted
153
- result.action_id ??= currentEvidenceActionId() ?? randomUUID()
154
- result.action_type ??= type
155
- recordObservationEvent({
156
- step_id: args._stepId as string | undefined,
157
- action_type: type,
158
- dispatch_ms: timing.dispatch_ms,
159
- verification_mode: controls.verificationMode,
160
- dispatch_started: dispatchStarted
161
- })
162
- let delta: unknown
163
- let observationAvailable = false
164
- if (result.success && controls.verificationMode === 'light') {
165
- const verificationStarted = performance.now()
166
- const tree = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'verification' }, () =>
167
- measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () =>
168
- ToolsObserve.getUITreeHandler({ platform: (args.platform as PlatformArg | undefined) ?? result.device?.platform, deviceId: (args.deviceId as string | undefined) ?? result.device?.id })))).catch(() => null) as any
169
- markEvidenceVerification()
170
- observationAvailable = !!tree && !tree.error
171
- recordObservationEvent({
172
- verification_budget_ms: controls.verificationTimeoutMs,
173
- verification_elapsed_ms: performance.now() - verificationStarted,
174
- verification_outcome: observationAvailable ? 'observed' : 'unavailable',
175
- verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
176
- })
177
- if (observationAvailable && tree.device) delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined
178
- }
179
- if (!blocked && controls.verificationMode === 'full' && dispatchStarted) markEvidenceVerification()
180
- const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started)
181
- if (blocked) compact.verification.status = 'not_requested'
182
- if (result.success && controls.verificationMode === 'light' && !observationAvailable) compact.verification.status = 'unavailable'
183
- const evidence = {
184
- ...compact,
185
- ...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
186
- ? { ...condition.result, observed: condition.result.observed ? {
187
- snapshot_session_id: condition.result.observed.snapshot_session_id,
188
- snapshot_revision: condition.result.observed.snapshot_revision,
189
- captured_at_ms: condition.result.observed.captured_at_ms
190
- } : null } : condition.result } : {}),
191
- ...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
192
- ...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
193
- }
194
- return wrapResponse(controls.responseMode === 'compact' ? evidence : {
195
- ...result, ...evidence, timing: { ...timing, total_ms: performance.now() - started },
196
- effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
197
- }, controls.responseMode === 'debug')
198
- })
199
- }, true)
200
- }
201
- const handleStartApp = actionHandler('start_app', executeStartApp)
202
- const handleRestartApp = actionHandler('restart_app', executeRestartApp)
203
- const handleTap = actionHandler('tap', executeTap)
204
- const handleTapElement = actionHandler('tap_element', executeTapElement)
205
- const handleSwipe = actionHandler('swipe', executeSwipe)
206
- const handleScrollToElement = actionHandler('scroll_to_element', executeScrollToElement)
207
- const handleTypeText = actionHandler('type_text', executeTypeText)
208
- const handlePressBack = actionHandler('press_back', executePressBack)
209
-
210
- async function executeStartApp(args: ToolCallArgs) {
211
- const platform = requireStringArg(args, 'platform') as PlatformArg
212
- const appId = requireStringArg(args, 'appId')
213
- const deviceId = getStringArg(args, 'deviceId')
214
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
215
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
216
- notifyActionStart()
217
- const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')))
218
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
219
- const result = buildActionExecutionResult({
220
- actionType: 'start_app',
221
- sourceModule: 'server',
222
- device: res.device,
223
- selector: { appId },
224
- success: !!res.appStarted,
225
- uiFingerprintBefore,
226
- uiFingerprintAfter,
227
- failure: res.appStarted ? undefined : inferGenericFailure(res.error),
228
- details: {
229
- launch_time_ms: res.launchTimeMs,
230
- ...(typeof res.output === 'string' ? { output: res.output } : {}),
231
- ...(res.device ? { device_id: res.device.id } : {}),
232
- ...(typeof res.error === 'string' ? { error: res.error } : {}),
233
- ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
234
- ...(res.observedApp ? { observed_app: res.observedApp } : {})
235
- }
236
- })
237
- return wrapResponse(result)
238
- }
239
-
240
- async function handleTerminateApp(args: ToolCallArgs) {
241
- const platform = requireStringArg(args, 'platform') as PlatformArg
242
- const appId = requireStringArg(args, 'appId')
243
- const deviceId = getStringArg(args, 'deviceId')
244
- const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
245
- const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
246
- return wrapResponse(response)
247
- }
248
-
249
- async function executeRestartApp(args: ToolCallArgs) {
250
- const platform = requireStringArg(args, 'platform') as PlatformArg
251
- const appId = requireStringArg(args, 'appId')
252
- const deviceId = getStringArg(args, 'deviceId')
253
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
254
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
255
- notifyActionStart()
256
- const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined
257
- const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)))
258
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
259
- const result = buildActionExecutionResult({
260
- actionType: 'restart_app',
261
- sourceModule: 'server',
262
- device: res.device,
263
- selector: { appId },
264
- success: !!res.appRestarted,
265
- uiFingerprintBefore,
266
- uiFingerprintAfter,
267
- failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
268
- details: {
269
- launch_time_ms: res.launchTimeMs,
270
- ...(typeof res.output === 'string' ? { output: res.output } : {}),
271
- ...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
272
- ...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
273
- ...(typeof res.error === 'string' ? { error: res.error } : {}),
274
- ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
275
- ...(res.observedApp ? { observed_app: res.observedApp } : {})
276
- }
277
- })
278
- return wrapResponse(result)
279
- }
280
-
281
- async function handleResetAppData(args: ToolCallArgs) {
282
- const platform = requireStringArg(args, 'platform') as PlatformArg
283
- const appId = requireStringArg(args, 'appId')
284
- const deviceId = getStringArg(args, 'deviceId')
285
- const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
286
- const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
287
- return wrapResponse(response)
288
- }
289
-
290
- async function handleInstallApp(args: ToolCallArgs) {
291
- const platform = requireStringArg(args, 'platform') as PlatformArg
292
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
293
- const appPath = requireStringArg(args, 'appPath')
294
- const deviceId = getStringArg(args, 'deviceId')
295
- const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
296
- const response: InstallAppResponse = {
297
- device: res.device,
298
- installed: res.installed,
299
- output: (res as any).output,
300
- error: (res as any).error
301
- }
302
- return wrapResponse(response)
303
- }
304
-
305
- async function handleBuildApp(args: ToolCallArgs) {
306
- const platform = requireStringArg(args, 'platform') as PlatformArg
307
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
308
- const projectPath = requireStringArg(args, 'projectPath')
309
- const variant = getStringArg(args, 'variant')
310
- const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
311
- return wrapResponse(res)
312
- }
313
-
314
- async function handleBuildAndInstall(args: ToolCallArgs) {
315
- const platform = requireStringArg(args, 'platform') as PlatformArg
316
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
317
- const projectPath = requireStringArg(args, 'projectPath')
318
- const deviceId = getStringArg(args, 'deviceId')
319
- const timeout = getNumberArg(args, 'timeout')
320
- const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
321
- return {
322
- content: [
323
- { type: 'text' as const, text: res.ndjson },
324
- { type: 'text' as const, text: JSON.stringify(res.result, null, 2) }
325
- ]
326
- }
327
- }
328
-
329
- async function handleGetLogs(args: ToolCallArgs) {
330
- const platform = requireStringArg(args, 'platform') as PlatformArg
331
- const appId = getStringArg(args, 'appId')
332
- const deviceId = getStringArg(args, 'deviceId')
333
- const pid = getNumberArg(args, 'pid')
334
- const tag = getStringArg(args, 'tag')
335
- const level = getStringArg(args, 'level')
336
- const contains = getStringArg(args, 'contains')
337
- const since_seconds = getNumberArg(args, 'since_seconds')
338
- const limit = getNumberArg(args, 'limit')
339
- const lines = getNumberArg(args, 'lines')
340
- const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines })
341
- const filtered = !!(pid || tag || level || contains || since_seconds || appId)
342
- return {
343
- content: [
344
- { type: 'text' as const, text: JSON.stringify({ device: res.device, result: { count: res.logCount, filtered, crashLines: (res.crashLines || []), source: res.source, meta: res.meta || {} } }, null, 2) },
345
- { type: 'text' as const, text: JSON.stringify({ logs: res.logs }, null, 2) }
346
- ]
347
- }
348
- }
349
-
350
- async function handleListDevices(args: ToolCallArgs) {
351
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
352
- const appId = getStringArg(args, 'appId')
353
- const res = await ToolsManage.listDevicesHandler({ platform, appId })
354
- return wrapResponse(res)
355
- }
356
-
357
- async function handleGetSystemStatus() {
358
- const result = await getSystemStatus()
359
- return wrapResponse(result)
360
- }
361
-
362
- async function handleCaptureScreenshot(args: ToolCallArgs) {
363
- const platform = requireStringArg(args, 'platform') as PlatformArg
364
- const deviceId = getStringArg(args, 'deviceId')
365
- const started = performance.now()
366
- const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
367
- const screenshotMs = performance.now() - started
368
- const mime = (res as any).screenshot_mime || 'image/png'
369
- const content: Array<{ type: 'text' | 'image'; text?: string; data?: string; mimeType?: string }> = [
370
- { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution, mimeType: mime }, timing: { total_ms: screenshotMs, screenshot_ms: screenshotMs } }, null, 2) },
371
- { type: 'image', data: (res as any).screenshot, mimeType: mime }
372
- ]
373
- if ((res as any).screenshot_fallback) {
374
- content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' }) })
375
- content.push({ type: 'image', data: (res as any).screenshot_fallback, mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' })
376
- }
377
- return { content }
378
- }
379
-
380
- async function handleCaptureDebugSnapshot(args: ToolCallArgs) {
381
- const reason = getStringArg(args, 'reason')
382
- const includeLogs = getBooleanArg(args, 'includeLogs')
383
- const logLines = getNumberArg(args, 'logLines')
384
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
385
- const appId = getStringArg(args, 'appId')
386
- const deviceId = getStringArg(args, 'deviceId')
387
- const sessionId = getStringArg(args, 'sessionId')
388
- const includeScreenshot = getBooleanArg(args, 'includeScreenshot')
389
- const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, includeScreenshot, logLines, platform, appId, deviceId, sessionId })
390
- return wrapResponse(res)
391
- }
392
-
393
- async function handleGetUITree(args: ToolCallArgs) {
394
- const platform = requireStringArg(args, 'platform') as PlatformArg
395
- const deviceId = getStringArg(args, 'deviceId')
396
- const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
397
- const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision')
398
- const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined
399
- const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () =>
400
- ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }))
401
- return wrapResponse(res)
402
- }
403
-
404
- async function handleGetCurrentScreen(args: ToolCallArgs) {
405
- const deviceId = getStringArg(args, 'deviceId')
406
- const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
407
- return wrapResponse(res)
408
- }
409
-
410
- async function handleGetScreenFingerprint(args: ToolCallArgs) {
411
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
412
- const deviceId = getStringArg(args, 'deviceId')
413
- const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
414
- return wrapResponse(res)
415
- }
416
-
417
- async function handleWaitForScreenChange(args: ToolCallArgs) {
418
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
419
- const previousFingerprint = requireStringArg(args, 'previousFingerprint')
420
- const timeoutMs = getNumberArg(args, 'timeoutMs')
421
- const pollIntervalMs = getNumberArg(args, 'pollIntervalMs')
422
- const deviceId = getStringArg(args, 'deviceId')
423
- const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
424
- return wrapResponse(res)
425
- }
426
-
427
- async function handleExpectScreen(args: ToolCallArgs) {
428
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
429
- const fingerprint = getStringArg(args, 'fingerprint')
430
- const screen = getStringArg(args, 'screen')
431
- const deviceId = getStringArg(args, 'deviceId')
432
- const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId })
433
- return wrapResponse(res)
434
- }
435
-
436
- async function handleExpectElementVisible(args: ToolCallArgs) {
437
- const selector = requireObjectArg<ExpectElementSelectorArg>(args, 'selector')
438
- const element_id = getStringArg(args, 'element_id')
439
- const timeout_ms = getNumberArg(args, 'timeout_ms')
440
- const poll_interval_ms = getNumberArg(args, 'poll_interval_ms')
441
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
442
- const deviceId = getStringArg(args, 'deviceId')
443
- const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId })
444
- return wrapResponse(res)
445
- }
446
-
447
- async function handleExpectState(args: ToolCallArgs) {
448
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
449
- const element_id = getStringArg(args, 'element_id')
450
- const property = requireStringArg(args, 'property')
451
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
452
- const deviceId = getStringArg(args, 'deviceId')
453
- if (!selector && !element_id) {
454
- throw new Error('Missing selector or element_id argument')
455
- }
456
- if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
457
- throw new Error('Missing expected argument')
458
- }
459
- const expected = args.expected as boolean | number | string | Record<string, unknown>
460
- const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId })
461
- return wrapResponse(res)
462
- }
463
-
464
- async function handleAdjustControl(args: ToolCallArgs) {
465
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
466
- const element_id = getStringArg(args, 'element_id')
467
- const property = getStringArg(args, 'property') ?? 'value'
468
- const targetValue = requireNumberArg(args, 'targetValue')
469
- const tolerance = getNumberArg(args, 'tolerance')
470
- const maxAttempts = getNumberArg(args, 'maxAttempts')
471
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
472
- const deviceId = getStringArg(args, 'deviceId')
473
- if ((!selector && !element_id) || (selector && element_id)) {
474
- throw new Error('Exactly one of selector or element_id argument is required')
475
- }
476
- const res = await ToolsInteract.adjustControlHandler({
477
- selector: selector ?? undefined,
478
- element_id: element_id ?? undefined,
479
- property,
480
- targetValue,
481
- tolerance,
482
- maxAttempts,
483
- platform,
484
- deviceId
485
- })
486
- markEvidenceVerification()
487
- return wrapResponse(res)
488
- }
489
-
490
- async function handleWaitForUI(args: ToolCallArgs) {
491
- if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
492
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
493
- const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
494
- const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
495
- const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300
496
- const match = getObjectArg<WaitForUiMatchArg>(args, 'match')
497
- const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
498
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
499
- const deviceId = getStringArg(args, 'deviceId')
500
- const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId })
501
- const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
502
- device => execute(device.platform as PlatformArg, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute()
503
- return wrapResponse(res)
504
- }
505
-
506
- async function handleWaitForUIChange(args: ToolCallArgs) {
507
- if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
508
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
509
- const deviceId = getStringArg(args, 'deviceId')
510
- const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
511
- const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300
512
- const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
513
- const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
514
- const target = getStringArg(args, 'target')
515
- const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target })
516
- const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
517
- device => execute(device.platform as PlatformArg, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute()
518
- return wrapResponse(res)
519
- }
520
-
521
- async function handleFindElement(args: ToolCallArgs) {
522
- const query = requireStringArg(args, 'query')
523
- const exact = getBooleanArg(args, 'exact') ?? false
524
- const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000
525
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
526
- const deviceId = getStringArg(args, 'deviceId')
527
- const targetStarted = performance.now()
528
- const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }))
529
- recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' })
530
- return wrapResponse(res)
531
- }
532
-
533
- async function executeTap(args: ToolCallArgs) {
534
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
535
- const x = requireNumberArg(args, 'x')
536
- const y = requireNumberArg(args, 'y')
537
- const deviceId = getStringArg(args, 'deviceId')
538
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
539
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
540
- notifyActionStart()
541
- const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }))
542
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
543
- const result = buildActionExecutionResult({
544
- actionType: 'tap',
545
- sourceModule: 'server',
546
- selector: { x, y },
547
- success: !!res.success,
548
- uiFingerprintBefore,
549
- uiFingerprintAfter,
550
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
551
- })
552
- return wrapResponse(result)
553
- }
554
-
555
- async function executeTapElement(args: ToolCallArgs) {
556
- const controls = tapElementControlsSchema.safeParse({ elementId: args.elementId, selector: args.selector, waitFor: args.waitFor })
557
- if (!controls.success) throw new Error(`INVALID_ARGUMENT: ${controls.error.message}`)
558
- const elementId = controls.data.elementId
559
- const selector = controls.data.selector as ExpectElementSelectorArg | undefined
560
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
561
- let resolvedId = elementId
562
- let freshTree: any
563
- if (selector) {
564
- const waitFor = getObjectArg<WaitForUiMatchArg>(args, 'waitFor') as any
565
- const targetStarted = performance.now()
566
- const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000
567
- const resolution = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'target' }, () =>
568
- measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })))) as any
569
- recordObservationEvent({ step_id: args._stepId as string | undefined, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status })
570
- if (resolution.status !== 'success' || !resolution.element?.elementId) return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true })
571
- resolvedId = resolution.element.elementId
572
- freshTree = resolution._tree
573
- }
574
- notifyActionStart()
575
- if (freshTree) recordObservationConsumer(freshTree, 'target', args._stepId as string | undefined)
576
- const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId!, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })) as any
577
- return wrapResponse(res)
578
- }
579
-
580
- function payloadOf(response: ToolCallResult) {
581
- const text = response.content.find((item: any) => item.type === 'text')?.text
582
- return text ? JSON.parse(text) : null
583
- }
584
-
585
- async function expectElementAbsent({ selector, timeoutMs, pollIntervalMs, platform, deviceId }: { selector: ExpectElementSelectorArg, timeoutMs: number, pollIntervalMs: number, platform: PlatformArg, deviceId?: string }) {
586
- const deadline = performance.now() + timeoutMs
587
- let consecutive = 0
588
- let observed: unknown
589
- while (performance.now() < deadline) {
590
- const result = await ToolsInteract.waitForUIHandler({ selector, condition: 'not_exists', timeout_ms: 0, singleObservation: true, singleAttempt: true, platform, deviceId }) as any
591
- observed = result.observed
592
- consecutive = result.status === 'success' ? consecutive + 1 : 0
593
- if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed }
594
- const delay = Math.max(100, pollIntervalMs)
595
- if (performance.now() + delay >= deadline) break
596
- await new Promise(resolve => setTimeout(resolve, delay))
597
- }
598
- return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed }
599
- }
600
-
601
- async function expectFingerprint({ fingerprint, timeoutMs, platform, deviceId }: { fingerprint: string, timeoutMs: number, platform: PlatformArg, deviceId?: string }) {
602
- const deadline = performance.now() + timeoutMs
603
- let consecutive = 0
604
- let observed: string | null = null
605
- while (performance.now() < deadline) {
606
- const result = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as any
607
- observed = result?.fingerprint ?? null
608
- consecutive = observed === fingerprint ? consecutive + 1 : 0
609
- if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed_fingerprint: observed }
610
- if (performance.now() + 100 >= deadline) break
611
- await new Promise(resolve => setTimeout(resolve, 100))
612
- }
613
- return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed_fingerprint: observed }
614
- }
615
-
616
- async function handleRunJourney(args: ToolCallArgs) {
617
- const parsed = journeySchema.safeParse(args)
618
- if (!parsed.success) throw new Error(`INVALID_ARGUMENT: ${parsed.error.message}`)
619
- const { platform, responseMode, defaults, captureOnFailure } = parsed.data
620
- const steps = parsed.data.steps as Array<Record<string, any>>
621
- const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId })
622
- const deviceId = device.id
623
- const finishTrace = parsed.data.systemTraceId
624
- ? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()!) : undefined
625
- if (parsed.data.systemTraceId) bindSystemTrace(parsed.data.systemTraceId)
626
- try {
627
- return await withResolvedDevice(device, async () => {
628
- const started = performance.now()
629
- const results: any[] = []
630
- let stoppedAt: string | null = null
631
- let pendingLightStep: number | null = null
632
- for (let index = 0; index < steps.length; index++) {
633
- const step = steps[index]
634
- if (platform === 'android' && debuggerSessions.paused(deviceId)) {
635
- results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } })
636
- stoppedAt = step.id as string
637
- break
638
- }
639
- const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full'
640
- const next = steps[index + 1]
641
- const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
642
- next.assertion?.kind === 'element_visible'
643
- const stepStart = performance.now()
644
- let result: any
645
- if (stoppedAt) {
646
- results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } })
647
- continue
648
- }
649
- try {
650
- if (step.type === 'tap') {
651
- result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }))
652
- } else if (step.type === 'wait') {
653
- result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector as ExpectElementSelectorArg, condition: (step.condition as any) ?? 'exists', timeout_ms: (step.timeoutMs as number | undefined) ?? 5000, poll_interval_ms: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId }))
654
- result = { success: result.status === 'success', ...result }
655
- } else if (step.type === 'assert') {
656
- result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget((step.timeoutMs as number | undefined) ?? 5000, async () => {
657
- const assertion = step.assertion as any
658
- if (assertion?.kind === 'element_visible') result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: (step.timeoutMs as number | undefined) ?? 5000, poll_interval_ms: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId })
659
- else if (assertion?.kind === 'element_absent') result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: (step.timeoutMs as number | undefined) ?? 5000, pollIntervalMs: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId })
660
- else if (assertion?.kind === 'screen_fingerprint') result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: (step.timeoutMs as number | undefined) ?? 5000, platform, deviceId })
661
- else if (assertion?.kind === 'state_equals') result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true })
662
- else result = { success: false, failure_code: 'INVALID_ASSERTION' }
663
- return result
664
- }))
665
- } else if (step.type === 'start_app') {
666
- result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }))
667
- } else {
668
- result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' }
669
- }
670
- } catch (error) {
671
- result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) }
672
- }
673
- if (platform === 'android' && debuggerSessions.paused(deviceId)) { result.success = false; result.failure_code = 'DEBUGGER_PAUSED'; result.retryable = false }
674
- if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? '')) result.failure_code = 'ASSERTION_TIMEOUT'
675
- const success = !!result?.success
676
- if (lightCoveredByAssertion) {
677
- result.verification = { mode: 'light', status: success ? 'unavailable' :
678
- (['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') }
679
- if (success) {
680
- result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true }
681
- pendingLightStep = results.length
682
- }
683
- }
684
- if (step.type === 'assert' && pendingLightStep !== null) {
685
- const pending = results[pendingLightStep].result
686
- if (typeof result?.observed?.matched_count === 'number') {
687
- pending.verification.status = 'observed'
688
- if (pending.evidence) completeJourneyVerification(pending.evidence.action_id)
689
- delete pending.verification_diagnostic
690
- const delta = ToolsObserve.getLatestStateDelta(platform, deviceId)
691
- if (delta) { pending.state_delta = delta; pending.state_delta_available = true }
692
- }
693
- pendingLightStep = null
694
- }
695
- if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type)) result = {
696
- success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
697
- ...(result.error?.code ? { failure_code: result.error.code } : {}),
698
- ...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
699
- ...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
700
- ...(result.actual_state ? { observed: result.actual_state } : {})
701
- }
702
- results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } })
703
- if (!success) stoppedAt = step.id as string
704
- }
705
- const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
706
- ? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined
707
- const componentTiming: Record<string, number> = {}
708
- for (const step of results) for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
709
- if (key !== 'total_ms' && typeof value === 'number') componentTiming[key] = (componentTiming[key] ?? 0) + value
710
- }
711
- refreshJourneyEvidence(results)
712
- return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug')
713
- })
714
- } finally { await finishTrace?.() }
715
- }
716
-
717
- async function executeSwipe(args: ToolCallArgs) {
718
- const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
719
- const x1 = requireNumberArg(args, 'x1')
720
- const y1 = requireNumberArg(args, 'y1')
721
- const x2 = requireNumberArg(args, 'x2')
722
- const y2 = requireNumberArg(args, 'y2')
723
- const duration = requireNumberArg(args, 'duration')
724
- const deviceId = getStringArg(args, 'deviceId')
725
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
726
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
727
- notifyActionStart()
728
- const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }))
729
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
730
- const result = buildActionExecutionResult({
731
- actionType: 'swipe',
732
- sourceModule: 'server',
733
- selector: { x1, y1, x2, y2, duration },
734
- success: !!res.success,
735
- uiFingerprintBefore,
736
- uiFingerprintAfter,
737
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
738
- })
739
- return wrapResponse(result)
740
- }
741
-
742
- async function executeScrollToElement(args: ToolCallArgs) {
743
- const platform = requireStringArg(args, 'platform') as PlatformArg
744
- const selector = requireObjectArg<ScrollSelectorArg>(args, 'selector')
745
- const direction = getStringArg(args, 'direction') as 'down' | 'up' | undefined
746
- const maxScrolls = getNumberArg(args, 'maxScrolls')
747
- const scrollAmount = getNumberArg(args, 'scrollAmount')
748
- const deviceId = getStringArg(args, 'deviceId')
749
- const { verificationMode } = interactionControls(args)
750
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
751
- notifyActionStart()
752
- const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }))
753
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
754
- const result = buildActionExecutionResult({
755
- actionType: 'scroll_to_element',
756
- sourceModule: 'server',
757
- selector: selector ?? null,
758
- resolved: res?.success && res?.element ? {
759
- elementId: null,
760
- text: (res.element as any).text ?? null,
761
- resource_id: (res.element as any).resourceId ?? null,
762
- accessibility_id: (res.element as any).contentDesc ?? null,
763
- class: (res.element as any).className ?? null,
764
- bounds: (res.element as any).bounds ?? null,
765
- index: null
766
- } : null,
767
- success: !!res.success,
768
- uiFingerprintBefore,
769
- uiFingerprintAfter,
770
- failure: res.success ? undefined : inferScrollFailure((res as any).reason)
771
- })
772
- return wrapResponse(result)
773
- }
774
-
775
- async function executeTypeText(args: ToolCallArgs) {
776
- const text = requireStringArg(args, 'text')
777
- const deviceId = getStringArg(args, 'deviceId')
778
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
779
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
780
- notifyActionStart()
781
- const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }))
782
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
783
- const result = buildActionExecutionResult({
784
- actionType: 'type_text',
785
- sourceModule: 'server',
786
- selector: { text },
787
- success: !!res.success,
788
- uiFingerprintBefore,
789
- uiFingerprintAfter,
790
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
791
- })
792
- return wrapResponse(result)
793
- }
794
-
795
- async function executePressBack(args: ToolCallArgs) {
796
- const deviceId = getStringArg(args, 'deviceId')
797
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
798
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
799
- notifyActionStart()
800
- const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }))
801
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
802
- const result = buildActionExecutionResult({
803
- actionType: 'press_back',
804
- sourceModule: 'server',
805
- selector: { key: 'back' },
806
- success: !!res.success,
807
- uiFingerprintBefore,
808
- uiFingerprintAfter,
809
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
810
- })
811
- return wrapResponse(result)
812
- }
813
-
814
- async function handleStartLogStream(args: ToolCallArgs) {
815
- const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
816
- const packageName = requireStringArg(args, 'packageName')
817
- const level = (getStringArg(args, 'level') as 'error' | 'warn' | 'info' | 'debug' | undefined) ?? 'error'
818
- const sessionId = getStringArg(args, 'sessionId')
819
- const deviceId = getStringArg(args, 'deviceId')
820
- const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
821
- return wrapResponse(res)
822
- }
823
-
824
- async function handleReadLogStream(args: ToolCallArgs) {
825
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
826
- const sessionId = getStringArg(args, 'sessionId')
827
- const limit = getNumberArg(args, 'limit')
828
- const since = getStringArg(args, 'since')
829
- const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
830
- return wrapResponse(res)
831
- }
832
-
833
- async function handleStopLogStream(args: ToolCallArgs) {
834
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
835
- const sessionId = getStringArg(args, 'sessionId')
836
- const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
837
- return wrapResponse(res)
838
- }
839
-
840
- function handleClassifyActionOutcome(args: ToolCallArgs) {
841
- const uiChanged = requireBooleanArg(args, 'uiChanged')
842
- const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible')
843
- const actionType = getStringArg(args, 'actionType')
844
- const networkRequests = getArrayArg<ClassifyNetworkRequestArg>(args, 'networkRequests')
845
- const hasLogErrors = getBooleanArg(args, 'hasLogErrors')
846
- const result = classifyActionOutcome({
847
- uiChanged,
848
- expectedElementVisible: expectedElementVisible ?? null,
849
- actionType: actionType ?? null,
850
- networkRequests: networkRequests ?? null,
851
- hasLogErrors: hasLogErrors ?? null
852
- })
853
- return Promise.resolve(wrapResponse(result))
854
- }
855
-
856
- async function handleGetNetworkActivity(args: ToolCallArgs) {
857
- const platform = requireStringArg(args, 'platform') as PlatformArg
858
- const deviceId = getStringArg(args, 'deviceId')
859
- const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId })
860
- return wrapResponse(result)
861
- }
862
-
863
- async function handleSystemTrace(name: string, args: ToolCallArgs) {
864
- const schema = name === 'start_system_trace' ? traceStartSchema : name === 'get_system_trace_capabilities' ? traceTargetSchema : traceControlSchema
865
- const parsed = schema.safeParse(args)
866
- if (!parsed.success) throw new TraceError('INVALID_ARGUMENT')
867
- if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
868
- const target = parsed.data as { platform: PlatformArg; deviceId?: string; durationMs?: number }
869
- if (target.platform !== 'android') {
870
- if (name === 'start_system_trace') throw new TraceError('UNSUPPORTED_PLATFORM')
871
- return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: {}, reasons: ['UNSUPPORTED_PLATFORM'], enabled_sources: [], unavailable_categories: [], limitations: ['Android only'] })
872
- }
873
- let device
874
- try { device = await resolveTargetDevice(target) }
875
- catch {
876
- if (name === 'start_system_trace') throw new TraceError('TRACE_UNSUPPORTED')
877
- return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: { device: false }, reasons: ['DEVICE_UNAVAILABLE'], enabled_sources: [], unavailable_categories: [], limitations: ['An authorised Android device is required'] })
878
- }
879
- if (name === 'get_system_trace_capabilities') return wrapResponse(await systemTraces.capabilities(device.id))
880
- const result = await systemTraces.start(device.id, target.durationMs)
881
- return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) }
882
- }
883
- const operation = name === 'stop_system_trace' ? 'stop' : name === 'cancel_system_trace' ? 'cancel' : 'status'
884
- const result = await systemTraces.control((parsed.data as { traceId: string }).traceId, operation)
885
- return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) }
886
- }
887
-
888
- export const toolHandlers: Record<string, ToolHandler> = {
889
- ...Object.fromEntries(systemTraceTools.map(({ name }) => [name, (args: ToolCallArgs) => handleSystemTrace(name, args)])),
890
- get_app_readiness: async args => {
891
- const parsed = readinessSchema.safeParse(args)
892
- if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
893
- return wrapResponse(await getAppReadiness(parsed.data))
894
- },
895
- wait_for_app_ready: async args => {
896
- const parsed = readinessWaitSchema.safeParse(args)
897
- if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
898
- return wrapResponse(await getAppReadiness(parsed.data, true))
899
- },
900
- start_app: handleStartApp,
901
- terminate_app: handleTerminateApp,
902
- restart_app: handleRestartApp,
903
- reset_app_data: handleResetAppData,
904
- install_app: handleInstallApp,
905
- build_app: handleBuildApp,
906
- build_and_install: handleBuildAndInstall,
907
- get_logs: handleGetLogs,
908
- list_devices: handleListDevices,
909
- get_system_status: handleGetSystemStatus,
910
- capture_screenshot: handleCaptureScreenshot,
911
- capture_debug_snapshot: handleCaptureDebugSnapshot,
912
- get_ui_tree: handleGetUITree,
913
- get_current_screen: handleGetCurrentScreen,
914
- get_screen_fingerprint: handleGetScreenFingerprint,
915
- wait_for_screen_change: handleWaitForScreenChange,
916
- wait_for_ui_change: handleWaitForUIChange,
917
- expect_screen: handleExpectScreen,
918
- expect_element_visible: handleExpectElementVisible,
919
- expect_state: handleExpectState,
920
- adjust_control: handleAdjustControl,
921
- wait_for_ui: handleWaitForUI,
922
- find_element: handleFindElement,
923
- tap: handleTap,
924
- tap_element: handleTapElement,
925
- run_journey: handleRunJourney,
926
- swipe: handleSwipe,
927
- scroll_to_element: handleScrollToElement,
928
- type_text: handleTypeText,
929
- press_back: handlePressBack,
930
- start_log_stream: handleStartLogStream,
931
- read_log_stream: handleReadLogStream,
932
- stop_log_stream: handleStopLogStream,
933
- classify_action_outcome: handleClassifyActionOutcome,
934
- get_network_activity: handleGetNetworkActivity
935
- }
936
-
937
- for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
938
- toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey')
939
- }
940
-
941
- export async function handleToolCall(name: string, args: ToolCallArgs = {}, signal?: AbortSignal) {
942
- if (Object.hasOwn(debuggerContracts, name)) return handleDebugger(name, args, signal)
943
- const handler = toolHandlers[name]
944
- if (!handler) throw new Error(`Unknown tool: ${name}`)
945
-
946
- const started = performance.now()
947
- const measured: Record<string, number> = {}
948
- const addTiming = (response: ToolCallResult) => {
949
- if (!['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'wait_for_ui', 'get_ui_tree', 'capture_screenshot', 'capture_debug_snapshot'].includes(name)) return response
950
- const item = response.content.find(item => item.type === 'text')
951
- if (item?.text) {
952
- try {
953
- const value = JSON.parse(item.text)
954
- value.timing = { ...measured, ...value.timing, total_ms: performance.now() - started }
955
- item.text = JSON.stringify(value)
956
- } catch { /* Non-JSON text responses retain their original content. */ }
957
- }
958
- return response
959
- }
960
- return withTiming(measured, async () => {
961
- try {
962
- if (!systemTraceToolNames.has(name)) validateExpectedState(name, args)
963
- const guarded = ['start_app', 'terminate_app', 'restart_app', 'reset_app_data', 'install_app', 'build_and_install', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'run_journey', 'wait_for_ui', 'wait_for_ui_change', 'wait_for_screen_change', 'expect_screen', 'expect_element_visible', 'expect_state'].includes(name)
964
- if (guarded && args.platform !== 'ios' && debuggerSessions.hasActiveSessions()) {
965
- const device = await resolveTargetDevice({ platform: 'android', deviceId: args.deviceId as string | undefined })
966
- return withResolvedDevice(device, () => withDebuggerGuard('android', device.id, async () => {
967
- const before = debuggerSessions.paused(device.id)
968
- if (before) return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before })
969
- let response: ToolCallResult
970
- try { response = await handler(args) } catch (error) {
971
- if (!debuggerSessions.paused(device.id)) throw error
972
- response = wrapResponse({ success: false })
973
- }
974
- const pause = debuggerSessions.paused(device.id)
975
- if (pause) {
976
- const item = response.content.find(item => item.type === 'text')
977
- if (item?.text) {
978
- const payload = JSON.parse(item.text)
979
- Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
980
- dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
981
- verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause })
982
- item.text = JSON.stringify(payload)
983
- }
984
- }
985
- return addTiming(response)
986
- }))
987
- }
988
- const response = await handler(args)
989
- if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name) && args.platform !== 'ios') {
990
- const item = response.content.find(item => item.type === 'text')
991
- if (item?.text) {
992
- try {
993
- const payload = JSON.parse(item.text)
994
- const device = payload.device ?? payload.evidence?.device
995
- const id = device?.id ?? args.deviceId
996
- const pause = id && device?.platform !== 'ios' ? debuggerSessions.paused(String(id)) : null
997
- if (pause) item.text = JSON.stringify({ ...payload, debugger: pause })
998
- } catch { /* Non-JSON observation */ }
999
- }
1000
- }
1001
- return addTiming(response)
1002
- } catch (error) {
1003
- if (error instanceof TraceError) return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true }
1004
- console.error(`Error executing tool ${name}:`, error)
1005
- return addTiming(wrapToolError(name, error))
1006
- }
1007
- })
1008
- }