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
@@ -1,923 +0,0 @@
1
- import { withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js'
2
- import { measure, withTiming } from '../utils/timing.js'
3
- import { randomUUID } from 'node:crypto'
4
- import { runWithBudget } from '../utils/operation-budget.js'
5
- import { journeySchema, tapElementControlsSchema } from './journey-schema.js'
6
- import { readinessSchema, readinessWaitSchema } from './readiness-schema.js'
7
- import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js'
8
- import { validateExpectedState } from './condition-schema.js'
9
- import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js'
10
- import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js'
11
- import type {
12
- InstallAppResponse,
13
- ResetAppDataResponse,
14
- TerminateAppResponse
15
- } from '../types.js'
16
- import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js'
17
- import { ToolsInteract } from '../interact/index.js'
18
- import { ToolsObserve } from '../observe/index.js'
19
- import { classifyActionOutcome } from '../interact/classify.js'
20
- import { ToolsNetwork } from '../network/index.js'
21
- import { getSystemStatus } from '../system/index.js'
22
- import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js'
23
- import {
24
- buildActionExecutionResult,
25
- captureActionFingerprint,
26
- getArrayArg,
27
- getBooleanArg,
28
- getNumberArg,
29
- getObjectArg,
30
- getStringArg,
31
- inferGenericFailure,
32
- inferScrollFailure,
33
- requireBooleanArg,
34
- requireNumberArg,
35
- requireObjectArg,
36
- requireStringArg,
37
- ToolCallArgs,
38
- ToolCallResult,
39
- ToolHandler,
40
- wrapResponse,
41
- wrapToolError
42
- } from './common.js'
43
-
44
- type PlatformArg = 'android' | 'ios'
45
- type ProjectTypeArg = 'native' | 'kmp' | 'react-native' | 'flutter'
46
- type ExpectElementSelectorArg = { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
47
- type WaitForUiMatchArg = { index?: number }
48
- type WaitForUiRetryArg = { max_attempts?: number, backoff_ms?: number }
49
- type ScrollSelectorArg = { text?: string, resourceId?: string, contentDesc?: string, className?: string }
50
- type ClassifyNetworkRequestArg = { endpoint: string, status: 'success' | 'failure' | 'retryable' }
51
- type VerificationMode = 'none' | 'light' | 'full'
52
- type ResponseMode = 'compact' | 'debug'
53
-
54
- function interactionControls(args: ToolCallArgs) {
55
- const verificationMode = (getStringArg(args, 'verificationMode') as VerificationMode | undefined) ?? 'full'
56
- const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
57
- if (!['none', 'light', 'full'].includes(verificationMode) || !['compact', 'debug'].includes(responseMode)) throw new Error('INVALID_ARGUMENT')
58
- for (const key of ['verificationMode', 'responseMode']) if (args[key] !== undefined && typeof args[key] !== 'string') throw new Error('INVALID_ARGUMENT')
59
- for (const key of ['actionTimeoutMs', 'verificationTimeoutMs']) if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key]))) throw new Error('INVALID_ARGUMENT')
60
- const actionTimeoutMs = getNumberArg(args, 'actionTimeoutMs')
61
- const verificationTimeoutMs = getNumberArg(args, 'verificationTimeoutMs')
62
- return { verificationMode, responseMode,
63
- actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
64
- verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) }
65
-
66
- }
67
-
68
- function compactAction(result: any, verificationMode: VerificationMode, started = true, stateDelta?: unknown, totalMs = 0) {
69
- return {
70
- action_id: result.action_id,
71
- action_type: result.action_type,
72
- success: !!result.success,
73
- lifecycle_state: result.success ? (verificationMode === 'full' ? result.lifecycle_state : 'pending_verification') : 'failed',
74
- ...(result.failure_code ? { failure_code: result.failure_code } : {}),
75
- ...(typeof result.retryable === 'boolean' ? { retryable: result.retryable } : {}),
76
- delivery_status: result.success ? 'delivered' : (started ? 'unknown' : 'not_delivered'),
77
- dispatch_started: started,
78
- verification: { mode: verificationMode, status: result.success ? (verificationMode === 'none' ? 'not_requested' : verificationMode === 'light' ? 'observed' : (result.lifecycle_state === 'verified' ? 'verified' : 'observed')) : 'failed' },
79
- ...(stateDelta ? { state_delta: stateDelta } : {}),
80
- timing: { total_ms: totalMs }
81
- }
82
- }
83
-
84
- function correlatedHandler(name: string, handler: ToolHandler, action = false, journey = false): ToolHandler {
85
- return async args => {
86
- const ref = validateActionRef(args.actionRef)
87
- const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
88
- try { return await handler(args) }
89
- catch (error) { return wrapToolError(name, error) }
90
- })
91
- const item = result.content.find(item => item.type === 'text')
92
- if (item?.text) {
93
- const payload = JSON.parse(item.text)
94
- payload.evidence = evidence
95
- item.text = JSON.stringify(payload)
96
- }
97
- return result
98
- }
99
- }
100
-
101
- function actionHandler(type: string, execute: ToolHandler): ToolHandler {
102
- return correlatedHandler(type, async (args) => {
103
- const timing: Record<string, number> = {}
104
- return withTiming(timing, async () => {
105
- const started = performance.now()
106
- const controls = interactionControls(args)
107
- const expected = validateExpectedState(type, args)
108
- const condition = expected ? createConditionalAction(args.platform as PlatformArg, args.deviceId as string, expected) : undefined
109
- const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000)
110
- let dispatchStarted = false
111
- let result: any
112
- const dispatchWithBudget = (timeoutMs: number) => runWithBudget(timeoutMs, async (budget) => {
113
- try {
114
- if (!condition) return payloadOf(await execute(args))
115
- return await withConditionalAction(condition, async () => {
116
- const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId })
117
- return withResolvedDevice(device, async () => payloadOf(await execute(args)))
118
- })
119
- }
120
- finally { dispatchStarted = budget.dispatchStarted }
121
- })
122
- try {
123
- // Omitted controls retain the existing adapter budgets.
124
- result = args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined
125
- ? await dispatchWithBudget(Infinity) : await dispatchWithBudget(budgetMs)
126
- if (result.success) dispatchStarted = true
127
- } catch (error) {
128
- if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error))) return wrapToolError(type, error)
129
- result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) }
130
- }
131
- const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status)
132
- if (blocked) {
133
- dispatchStarted = false
134
- result = buildActionExecutionResult({
135
- actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
136
- selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
137
- success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
138
- failure: { failureCode: conditionFailureCode(condition), retryable: false },
139
- traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
140
- })
141
- }
142
- if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? '')) result.failure_code = 'ACTION_TIMEOUT'
143
- if (result.failure_code === 'ACTION_TIMEOUT') result.retryable = !dispatchStarted
144
- if (!result.success && typeof result.retryable !== 'boolean') result.retryable = !dispatchStarted
145
- result.action_id ??= currentEvidenceActionId() ?? randomUUID()
146
- result.action_type ??= type
147
- recordObservationEvent({
148
- step_id: args._stepId as string | undefined,
149
- action_type: type,
150
- dispatch_ms: timing.dispatch_ms,
151
- verification_mode: controls.verificationMode,
152
- dispatch_started: dispatchStarted
153
- })
154
- let delta: unknown
155
- let observationAvailable = false
156
- if (result.success && controls.verificationMode === 'light') {
157
- const verificationStarted = performance.now()
158
- const tree = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'verification' }, () =>
159
- measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () =>
160
- ToolsObserve.getUITreeHandler({ platform: (args.platform as PlatformArg | undefined) ?? result.device?.platform, deviceId: (args.deviceId as string | undefined) ?? result.device?.id })))).catch(() => null) as any
161
- markEvidenceVerification()
162
- observationAvailable = !!tree && !tree.error
163
- recordObservationEvent({
164
- verification_budget_ms: controls.verificationTimeoutMs,
165
- verification_elapsed_ms: performance.now() - verificationStarted,
166
- verification_outcome: observationAvailable ? 'observed' : 'unavailable',
167
- verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
168
- })
169
- if (observationAvailable && tree.device) delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined
170
- }
171
- if (!blocked && controls.verificationMode === 'full' && dispatchStarted) markEvidenceVerification()
172
- const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started)
173
- if (blocked) compact.verification.status = 'not_requested'
174
- if (result.success && controls.verificationMode === 'light' && !observationAvailable) compact.verification.status = 'unavailable'
175
- const evidence = {
176
- ...compact,
177
- ...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
178
- ? { ...condition.result, observed: condition.result.observed ? {
179
- snapshot_session_id: condition.result.observed.snapshot_session_id,
180
- snapshot_revision: condition.result.observed.snapshot_revision,
181
- captured_at_ms: condition.result.observed.captured_at_ms
182
- } : null } : condition.result } : {}),
183
- ...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
184
- ...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
185
- }
186
- return wrapResponse(controls.responseMode === 'compact' ? evidence : {
187
- ...result, ...evidence, timing: { ...timing, total_ms: performance.now() - started },
188
- 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) }
189
- }, controls.responseMode === 'debug')
190
- })
191
- }, true)
192
- }
193
- const handleStartApp = actionHandler('start_app', executeStartApp)
194
- const handleRestartApp = actionHandler('restart_app', executeRestartApp)
195
- const handleTap = actionHandler('tap', executeTap)
196
- const handleTapElement = actionHandler('tap_element', executeTapElement)
197
- const handleSwipe = actionHandler('swipe', executeSwipe)
198
- const handleScrollToElement = actionHandler('scroll_to_element', executeScrollToElement)
199
- const handleTypeText = actionHandler('type_text', executeTypeText)
200
- const handlePressBack = actionHandler('press_back', executePressBack)
201
-
202
- async function executeStartApp(args: ToolCallArgs) {
203
- const platform = requireStringArg(args, 'platform') as PlatformArg
204
- const appId = requireStringArg(args, 'appId')
205
- const deviceId = getStringArg(args, 'deviceId')
206
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
207
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
208
- notifyActionStart()
209
- 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')))
210
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
211
- const result = buildActionExecutionResult({
212
- actionType: 'start_app',
213
- sourceModule: 'server',
214
- device: res.device,
215
- selector: { appId },
216
- success: !!res.appStarted,
217
- uiFingerprintBefore,
218
- uiFingerprintAfter,
219
- failure: res.appStarted ? undefined : inferGenericFailure(res.error),
220
- details: {
221
- launch_time_ms: res.launchTimeMs,
222
- ...(typeof res.output === 'string' ? { output: res.output } : {}),
223
- ...(res.device ? { device_id: res.device.id } : {}),
224
- ...(typeof res.error === 'string' ? { error: res.error } : {}),
225
- ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
226
- ...(res.observedApp ? { observed_app: res.observedApp } : {})
227
- }
228
- })
229
- return wrapResponse(result)
230
- }
231
-
232
- async function handleTerminateApp(args: ToolCallArgs) {
233
- const platform = requireStringArg(args, 'platform') as PlatformArg
234
- const appId = requireStringArg(args, 'appId')
235
- const deviceId = getStringArg(args, 'deviceId')
236
- const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
237
- const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
238
- return wrapResponse(response)
239
- }
240
-
241
- async function executeRestartApp(args: ToolCallArgs) {
242
- const platform = requireStringArg(args, 'platform') as PlatformArg
243
- const appId = requireStringArg(args, 'appId')
244
- const deviceId = getStringArg(args, 'deviceId')
245
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
246
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
247
- notifyActionStart()
248
- const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined
249
- 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)))
250
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
251
- const result = buildActionExecutionResult({
252
- actionType: 'restart_app',
253
- sourceModule: 'server',
254
- device: res.device,
255
- selector: { appId },
256
- success: !!res.appRestarted,
257
- uiFingerprintBefore,
258
- uiFingerprintAfter,
259
- failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
260
- details: {
261
- launch_time_ms: res.launchTimeMs,
262
- ...(typeof res.output === 'string' ? { output: res.output } : {}),
263
- ...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
264
- ...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
265
- ...(typeof res.error === 'string' ? { error: res.error } : {}),
266
- ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
267
- ...(res.observedApp ? { observed_app: res.observedApp } : {})
268
- }
269
- })
270
- return wrapResponse(result)
271
- }
272
-
273
- async function handleResetAppData(args: ToolCallArgs) {
274
- const platform = requireStringArg(args, 'platform') as PlatformArg
275
- const appId = requireStringArg(args, 'appId')
276
- const deviceId = getStringArg(args, 'deviceId')
277
- const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
278
- const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
279
- return wrapResponse(response)
280
- }
281
-
282
- async function handleInstallApp(args: ToolCallArgs) {
283
- const platform = requireStringArg(args, 'platform') as PlatformArg
284
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
285
- const appPath = requireStringArg(args, 'appPath')
286
- const deviceId = getStringArg(args, 'deviceId')
287
- const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
288
- const response: InstallAppResponse = {
289
- device: res.device,
290
- installed: res.installed,
291
- output: (res as any).output,
292
- error: (res as any).error
293
- }
294
- return wrapResponse(response)
295
- }
296
-
297
- async function handleBuildApp(args: ToolCallArgs) {
298
- const platform = requireStringArg(args, 'platform') as PlatformArg
299
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
300
- const projectPath = requireStringArg(args, 'projectPath')
301
- const variant = getStringArg(args, 'variant')
302
- const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
303
- return wrapResponse(res)
304
- }
305
-
306
- async function handleBuildAndInstall(args: ToolCallArgs) {
307
- const platform = requireStringArg(args, 'platform') as PlatformArg
308
- const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
309
- const projectPath = requireStringArg(args, 'projectPath')
310
- const deviceId = getStringArg(args, 'deviceId')
311
- const timeout = getNumberArg(args, 'timeout')
312
- const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
313
- return {
314
- content: [
315
- { type: 'text' as const, text: res.ndjson },
316
- { type: 'text' as const, text: JSON.stringify(res.result, null, 2) }
317
- ]
318
- }
319
- }
320
-
321
- async function handleGetLogs(args: ToolCallArgs) {
322
- const platform = requireStringArg(args, 'platform') as PlatformArg
323
- const appId = getStringArg(args, 'appId')
324
- const deviceId = getStringArg(args, 'deviceId')
325
- const pid = getNumberArg(args, 'pid')
326
- const tag = getStringArg(args, 'tag')
327
- const level = getStringArg(args, 'level')
328
- const contains = getStringArg(args, 'contains')
329
- const since_seconds = getNumberArg(args, 'since_seconds')
330
- const limit = getNumberArg(args, 'limit')
331
- const lines = getNumberArg(args, 'lines')
332
- const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines })
333
- const filtered = !!(pid || tag || level || contains || since_seconds || appId)
334
- return {
335
- content: [
336
- { 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) },
337
- { type: 'text' as const, text: JSON.stringify({ logs: res.logs }, null, 2) }
338
- ]
339
- }
340
- }
341
-
342
- async function handleListDevices(args: ToolCallArgs) {
343
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
344
- const appId = getStringArg(args, 'appId')
345
- const res = await ToolsManage.listDevicesHandler({ platform, appId })
346
- return wrapResponse(res)
347
- }
348
-
349
- async function handleGetSystemStatus() {
350
- const result = await getSystemStatus()
351
- return wrapResponse(result)
352
- }
353
-
354
- async function handleCaptureScreenshot(args: ToolCallArgs) {
355
- const platform = requireStringArg(args, 'platform') as PlatformArg
356
- const deviceId = getStringArg(args, 'deviceId')
357
- const started = performance.now()
358
- const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
359
- const screenshotMs = performance.now() - started
360
- const mime = (res as any).screenshot_mime || 'image/png'
361
- const content: Array<{ type: 'text' | 'image'; text?: string; data?: string; mimeType?: string }> = [
362
- { 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) },
363
- { type: 'image', data: (res as any).screenshot, mimeType: mime }
364
- ]
365
- if ((res as any).screenshot_fallback) {
366
- content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' }) })
367
- content.push({ type: 'image', data: (res as any).screenshot_fallback, mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' })
368
- }
369
- return { content }
370
- }
371
-
372
- async function handleCaptureDebugSnapshot(args: ToolCallArgs) {
373
- const reason = getStringArg(args, 'reason')
374
- const includeLogs = getBooleanArg(args, 'includeLogs')
375
- const logLines = getNumberArg(args, 'logLines')
376
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
377
- const appId = getStringArg(args, 'appId')
378
- const deviceId = getStringArg(args, 'deviceId')
379
- const sessionId = getStringArg(args, 'sessionId')
380
- const includeScreenshot = getBooleanArg(args, 'includeScreenshot')
381
- const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, includeScreenshot, logLines, platform, appId, deviceId, sessionId })
382
- return wrapResponse(res)
383
- }
384
-
385
- async function handleGetUITree(args: ToolCallArgs) {
386
- const platform = requireStringArg(args, 'platform') as PlatformArg
387
- const deviceId = getStringArg(args, 'deviceId')
388
- const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
389
- const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision')
390
- const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined
391
- const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () =>
392
- ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }))
393
- return wrapResponse(res)
394
- }
395
-
396
- async function handleGetCurrentScreen(args: ToolCallArgs) {
397
- const deviceId = getStringArg(args, 'deviceId')
398
- const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
399
- return wrapResponse(res)
400
- }
401
-
402
- async function handleGetScreenFingerprint(args: ToolCallArgs) {
403
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
404
- const deviceId = getStringArg(args, 'deviceId')
405
- const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
406
- return wrapResponse(res)
407
- }
408
-
409
- async function handleWaitForScreenChange(args: ToolCallArgs) {
410
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
411
- const previousFingerprint = requireStringArg(args, 'previousFingerprint')
412
- const timeoutMs = getNumberArg(args, 'timeoutMs')
413
- const pollIntervalMs = getNumberArg(args, 'pollIntervalMs')
414
- const deviceId = getStringArg(args, 'deviceId')
415
- const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
416
- return wrapResponse(res)
417
- }
418
-
419
- async function handleExpectScreen(args: ToolCallArgs) {
420
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
421
- const fingerprint = getStringArg(args, 'fingerprint')
422
- const screen = getStringArg(args, 'screen')
423
- const deviceId = getStringArg(args, 'deviceId')
424
- const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId })
425
- return wrapResponse(res)
426
- }
427
-
428
- async function handleExpectElementVisible(args: ToolCallArgs) {
429
- const selector = requireObjectArg<ExpectElementSelectorArg>(args, 'selector')
430
- const element_id = getStringArg(args, 'element_id')
431
- const timeout_ms = getNumberArg(args, 'timeout_ms')
432
- const poll_interval_ms = getNumberArg(args, 'poll_interval_ms')
433
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
434
- const deviceId = getStringArg(args, 'deviceId')
435
- const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId })
436
- return wrapResponse(res)
437
- }
438
-
439
- async function handleExpectState(args: ToolCallArgs) {
440
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
441
- const element_id = getStringArg(args, 'element_id')
442
- const property = requireStringArg(args, 'property')
443
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
444
- const deviceId = getStringArg(args, 'deviceId')
445
- if (!selector && !element_id) {
446
- throw new Error('Missing selector or element_id argument')
447
- }
448
- if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
449
- throw new Error('Missing expected argument')
450
- }
451
- const expected = args.expected as boolean | number | string | Record<string, unknown>
452
- const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId })
453
- return wrapResponse(res)
454
- }
455
-
456
- async function handleAdjustControl(args: ToolCallArgs) {
457
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
458
- const element_id = getStringArg(args, 'element_id')
459
- const property = getStringArg(args, 'property') ?? 'value'
460
- const targetValue = requireNumberArg(args, 'targetValue')
461
- const tolerance = getNumberArg(args, 'tolerance')
462
- const maxAttempts = getNumberArg(args, 'maxAttempts')
463
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
464
- const deviceId = getStringArg(args, 'deviceId')
465
- if ((!selector && !element_id) || (selector && element_id)) {
466
- throw new Error('Exactly one of selector or element_id argument is required')
467
- }
468
- const res = await ToolsInteract.adjustControlHandler({
469
- selector: selector ?? undefined,
470
- element_id: element_id ?? undefined,
471
- property,
472
- targetValue,
473
- tolerance,
474
- maxAttempts,
475
- platform,
476
- deviceId
477
- })
478
- markEvidenceVerification()
479
- return wrapResponse(res)
480
- }
481
-
482
- async function handleWaitForUI(args: ToolCallArgs) {
483
- if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
484
- const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
485
- const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
486
- const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
487
- const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300
488
- const match = getObjectArg<WaitForUiMatchArg>(args, 'match')
489
- const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
490
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
491
- const deviceId = getStringArg(args, 'deviceId')
492
- const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId })
493
- const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
494
- 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()
495
- return wrapResponse(res)
496
- }
497
-
498
- async function handleWaitForUIChange(args: ToolCallArgs) {
499
- if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
500
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
501
- const deviceId = getStringArg(args, 'deviceId')
502
- const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
503
- const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300
504
- const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
505
- const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
506
- const target = getStringArg(args, 'target')
507
- const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target })
508
- const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
509
- 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()
510
- return wrapResponse(res)
511
- }
512
-
513
- async function handleFindElement(args: ToolCallArgs) {
514
- const query = requireStringArg(args, 'query')
515
- const exact = getBooleanArg(args, 'exact') ?? false
516
- const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000
517
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
518
- const deviceId = getStringArg(args, 'deviceId')
519
- const targetStarted = performance.now()
520
- const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }))
521
- recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' })
522
- return wrapResponse(res)
523
- }
524
-
525
- async function executeTap(args: ToolCallArgs) {
526
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
527
- const x = requireNumberArg(args, 'x')
528
- const y = requireNumberArg(args, 'y')
529
- const deviceId = getStringArg(args, 'deviceId')
530
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
531
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
532
- notifyActionStart()
533
- const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }))
534
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
535
- const result = buildActionExecutionResult({
536
- actionType: 'tap',
537
- sourceModule: 'server',
538
- selector: { x, y },
539
- success: !!res.success,
540
- uiFingerprintBefore,
541
- uiFingerprintAfter,
542
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
543
- })
544
- return wrapResponse(result)
545
- }
546
-
547
- async function executeTapElement(args: ToolCallArgs) {
548
- const controls = tapElementControlsSchema.safeParse({ elementId: args.elementId, selector: args.selector, waitFor: args.waitFor })
549
- if (!controls.success) throw new Error(`INVALID_ARGUMENT: ${controls.error.message}`)
550
- const elementId = controls.data.elementId
551
- const selector = controls.data.selector as ExpectElementSelectorArg | undefined
552
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
553
- let resolvedId = elementId
554
- let freshTree: any
555
- if (selector) {
556
- const waitFor = getObjectArg<WaitForUiMatchArg>(args, 'waitFor') as any
557
- const targetStarted = performance.now()
558
- const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000
559
- const resolution = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'target' }, () =>
560
- 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
561
- recordObservationEvent({ step_id: args._stepId as string | undefined, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status })
562
- 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 })
563
- resolvedId = resolution.element.elementId
564
- freshTree = resolution._tree
565
- }
566
- notifyActionStart()
567
- if (freshTree) recordObservationConsumer(freshTree, 'target', args._stepId as string | undefined)
568
- 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
569
- return wrapResponse(res)
570
- }
571
-
572
- function payloadOf(response: ToolCallResult) {
573
- const text = response.content.find((item: any) => item.type === 'text')?.text
574
- return text ? JSON.parse(text) : null
575
- }
576
-
577
- async function expectElementAbsent({ selector, timeoutMs, pollIntervalMs, platform, deviceId }: { selector: ExpectElementSelectorArg, timeoutMs: number, pollIntervalMs: number, platform: PlatformArg, deviceId?: string }) {
578
- const deadline = performance.now() + timeoutMs
579
- let consecutive = 0
580
- let observed: unknown
581
- while (performance.now() < deadline) {
582
- const result = await ToolsInteract.waitForUIHandler({ selector, condition: 'not_exists', timeout_ms: 0, singleObservation: true, singleAttempt: true, platform, deviceId }) as any
583
- observed = result.observed
584
- consecutive = result.status === 'success' ? consecutive + 1 : 0
585
- if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed }
586
- const delay = Math.max(100, pollIntervalMs)
587
- if (performance.now() + delay >= deadline) break
588
- await new Promise(resolve => setTimeout(resolve, delay))
589
- }
590
- return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed }
591
- }
592
-
593
- async function expectFingerprint({ fingerprint, timeoutMs, platform, deviceId }: { fingerprint: string, timeoutMs: number, platform: PlatformArg, deviceId?: string }) {
594
- const deadline = performance.now() + timeoutMs
595
- let consecutive = 0
596
- let observed: string | null = null
597
- while (performance.now() < deadline) {
598
- const result = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as any
599
- observed = result?.fingerprint ?? null
600
- consecutive = observed === fingerprint ? consecutive + 1 : 0
601
- if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed_fingerprint: observed }
602
- if (performance.now() + 100 >= deadline) break
603
- await new Promise(resolve => setTimeout(resolve, 100))
604
- }
605
- return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed_fingerprint: observed }
606
- }
607
-
608
- async function handleRunJourney(args: ToolCallArgs) {
609
- const parsed = journeySchema.safeParse(args)
610
- if (!parsed.success) throw new Error(`INVALID_ARGUMENT: ${parsed.error.message}`)
611
- const { platform, responseMode, defaults, captureOnFailure } = parsed.data
612
- const steps = parsed.data.steps as Array<Record<string, any>>
613
- const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId })
614
- const deviceId = device.id
615
- return withResolvedDevice(device, async () => {
616
- const started = performance.now()
617
- const results: any[] = []
618
- let stoppedAt: string | null = null
619
- let pendingLightStep: number | null = null
620
- for (let index = 0; index < steps.length; index++) {
621
- const step = steps[index]
622
- const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full'
623
- const next = steps[index + 1]
624
- const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
625
- next.assertion?.kind === 'element_visible'
626
- const stepStart = performance.now()
627
- let result: any
628
- if (stoppedAt) {
629
- results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } })
630
- continue
631
- }
632
- try {
633
- if (step.type === 'tap') {
634
- result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }))
635
- } else if (step.type === 'wait') {
636
- 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 }))
637
- result = { success: result.status === 'success', ...result }
638
- } else if (step.type === 'assert') {
639
- result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget((step.timeoutMs as number | undefined) ?? 5000, async () => {
640
- const assertion = step.assertion as any
641
- 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 })
642
- 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 })
643
- else if (assertion?.kind === 'screen_fingerprint') result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: (step.timeoutMs as number | undefined) ?? 5000, platform, deviceId })
644
- 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 })
645
- else result = { success: false, failure_code: 'INVALID_ASSERTION' }
646
- return result
647
- }))
648
- } else if (step.type === 'start_app') {
649
- result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }))
650
- } else {
651
- result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' }
652
- }
653
- } catch (error) {
654
- result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) }
655
- }
656
- if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? '')) result.failure_code = 'ASSERTION_TIMEOUT'
657
- const success = !!result?.success
658
- if (lightCoveredByAssertion) {
659
- result.verification = { mode: 'light', status: success ? 'unavailable' :
660
- (['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') }
661
- if (success) {
662
- result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true }
663
- pendingLightStep = results.length
664
- }
665
- }
666
- if (step.type === 'assert' && pendingLightStep !== null) {
667
- const pending = results[pendingLightStep].result
668
- if (typeof result?.observed?.matched_count === 'number') {
669
- pending.verification.status = 'observed'
670
- if (pending.evidence) completeJourneyVerification(pending.evidence.action_id)
671
- delete pending.verification_diagnostic
672
- const delta = ToolsObserve.getLatestStateDelta(platform, deviceId)
673
- if (delta) { pending.state_delta = delta; pending.state_delta_available = true }
674
- }
675
- pendingLightStep = null
676
- }
677
- if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type)) result = {
678
- success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
679
- ...(result.error?.code ? { failure_code: result.error.code } : {}),
680
- ...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
681
- ...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
682
- ...(result.actual_state ? { observed: result.actual_state } : {})
683
- }
684
- results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } })
685
- if (!success) stoppedAt = step.id as string
686
- }
687
- const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
688
- ? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined
689
- const componentTiming: Record<string, number> = {}
690
- for (const step of results) for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
691
- if (key !== 'total_ms' && typeof value === 'number') componentTiming[key] = (componentTiming[key] ?? 0) + value
692
- }
693
- refreshJourneyEvidence(results)
694
- return wrapResponse({ 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')
695
- })
696
- }
697
-
698
- async function executeSwipe(args: ToolCallArgs) {
699
- const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
700
- const x1 = requireNumberArg(args, 'x1')
701
- const y1 = requireNumberArg(args, 'y1')
702
- const x2 = requireNumberArg(args, 'x2')
703
- const y2 = requireNumberArg(args, 'y2')
704
- const duration = requireNumberArg(args, 'duration')
705
- const deviceId = getStringArg(args, 'deviceId')
706
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
707
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
708
- notifyActionStart()
709
- const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }))
710
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
711
- const result = buildActionExecutionResult({
712
- actionType: 'swipe',
713
- sourceModule: 'server',
714
- selector: { x1, y1, x2, y2, duration },
715
- success: !!res.success,
716
- uiFingerprintBefore,
717
- uiFingerprintAfter,
718
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
719
- })
720
- return wrapResponse(result)
721
- }
722
-
723
- async function executeScrollToElement(args: ToolCallArgs) {
724
- const platform = requireStringArg(args, 'platform') as PlatformArg
725
- const selector = requireObjectArg<ScrollSelectorArg>(args, 'selector')
726
- const direction = getStringArg(args, 'direction') as 'down' | 'up' | undefined
727
- const maxScrolls = getNumberArg(args, 'maxScrolls')
728
- const scrollAmount = getNumberArg(args, 'scrollAmount')
729
- const deviceId = getStringArg(args, 'deviceId')
730
- const { verificationMode } = interactionControls(args)
731
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
732
- notifyActionStart()
733
- const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }))
734
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
735
- const result = buildActionExecutionResult({
736
- actionType: 'scroll_to_element',
737
- sourceModule: 'server',
738
- selector: selector ?? null,
739
- resolved: res?.success && res?.element ? {
740
- elementId: null,
741
- text: (res.element as any).text ?? null,
742
- resource_id: (res.element as any).resourceId ?? null,
743
- accessibility_id: (res.element as any).contentDesc ?? null,
744
- class: (res.element as any).className ?? null,
745
- bounds: (res.element as any).bounds ?? null,
746
- index: null
747
- } : null,
748
- success: !!res.success,
749
- uiFingerprintBefore,
750
- uiFingerprintAfter,
751
- failure: res.success ? undefined : inferScrollFailure((res as any).reason)
752
- })
753
- return wrapResponse(result)
754
- }
755
-
756
- async function executeTypeText(args: ToolCallArgs) {
757
- const text = requireStringArg(args, 'text')
758
- const deviceId = getStringArg(args, 'deviceId')
759
- const { verificationMode, actionTimeoutMs } = interactionControls(args)
760
- const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
761
- notifyActionStart()
762
- const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }))
763
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
764
- const result = buildActionExecutionResult({
765
- actionType: 'type_text',
766
- sourceModule: 'server',
767
- selector: { text },
768
- success: !!res.success,
769
- uiFingerprintBefore,
770
- uiFingerprintAfter,
771
- failure: res.success ? undefined : inferGenericFailure((res as any).error)
772
- })
773
- return wrapResponse(result)
774
- }
775
-
776
- async function executePressBack(args: ToolCallArgs) {
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.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }))
782
- const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
783
- const result = buildActionExecutionResult({
784
- actionType: 'press_back',
785
- sourceModule: 'server',
786
- selector: { key: 'back' },
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 handleStartLogStream(args: ToolCallArgs) {
796
- const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
797
- const packageName = requireStringArg(args, 'packageName')
798
- const level = (getStringArg(args, 'level') as 'error' | 'warn' | 'info' | 'debug' | undefined) ?? 'error'
799
- const sessionId = getStringArg(args, 'sessionId')
800
- const deviceId = getStringArg(args, 'deviceId')
801
- const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
802
- return wrapResponse(res)
803
- }
804
-
805
- async function handleReadLogStream(args: ToolCallArgs) {
806
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
807
- const sessionId = getStringArg(args, 'sessionId')
808
- const limit = getNumberArg(args, 'limit')
809
- const since = getStringArg(args, 'since')
810
- const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
811
- return wrapResponse(res)
812
- }
813
-
814
- async function handleStopLogStream(args: ToolCallArgs) {
815
- const platform = getStringArg(args, 'platform') as PlatformArg | undefined
816
- const sessionId = getStringArg(args, 'sessionId')
817
- const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
818
- return wrapResponse(res)
819
- }
820
-
821
- function handleClassifyActionOutcome(args: ToolCallArgs) {
822
- const uiChanged = requireBooleanArg(args, 'uiChanged')
823
- const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible')
824
- const actionType = getStringArg(args, 'actionType')
825
- const networkRequests = getArrayArg<ClassifyNetworkRequestArg>(args, 'networkRequests')
826
- const hasLogErrors = getBooleanArg(args, 'hasLogErrors')
827
- const result = classifyActionOutcome({
828
- uiChanged,
829
- expectedElementVisible: expectedElementVisible ?? null,
830
- actionType: actionType ?? null,
831
- networkRequests: networkRequests ?? null,
832
- hasLogErrors: hasLogErrors ?? null
833
- })
834
- return Promise.resolve(wrapResponse(result))
835
- }
836
-
837
- async function handleGetNetworkActivity(args: ToolCallArgs) {
838
- const platform = requireStringArg(args, 'platform') as PlatformArg
839
- const deviceId = getStringArg(args, 'deviceId')
840
- const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId })
841
- return wrapResponse(result)
842
- }
843
-
844
- export const toolHandlers: Record<string, ToolHandler> = {
845
- get_app_readiness: async args => {
846
- const parsed = readinessSchema.safeParse(args)
847
- if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
848
- return wrapResponse(await getAppReadiness(parsed.data))
849
- },
850
- wait_for_app_ready: async args => {
851
- const parsed = readinessWaitSchema.safeParse(args)
852
- if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
853
- return wrapResponse(await getAppReadiness(parsed.data, true))
854
- },
855
- start_app: handleStartApp,
856
- terminate_app: handleTerminateApp,
857
- restart_app: handleRestartApp,
858
- reset_app_data: handleResetAppData,
859
- install_app: handleInstallApp,
860
- build_app: handleBuildApp,
861
- build_and_install: handleBuildAndInstall,
862
- get_logs: handleGetLogs,
863
- list_devices: handleListDevices,
864
- get_system_status: handleGetSystemStatus,
865
- capture_screenshot: handleCaptureScreenshot,
866
- capture_debug_snapshot: handleCaptureDebugSnapshot,
867
- get_ui_tree: handleGetUITree,
868
- get_current_screen: handleGetCurrentScreen,
869
- get_screen_fingerprint: handleGetScreenFingerprint,
870
- wait_for_screen_change: handleWaitForScreenChange,
871
- wait_for_ui_change: handleWaitForUIChange,
872
- expect_screen: handleExpectScreen,
873
- expect_element_visible: handleExpectElementVisible,
874
- expect_state: handleExpectState,
875
- adjust_control: handleAdjustControl,
876
- wait_for_ui: handleWaitForUI,
877
- find_element: handleFindElement,
878
- tap: handleTap,
879
- tap_element: handleTapElement,
880
- run_journey: handleRunJourney,
881
- swipe: handleSwipe,
882
- scroll_to_element: handleScrollToElement,
883
- type_text: handleTypeText,
884
- press_back: handlePressBack,
885
- start_log_stream: handleStartLogStream,
886
- read_log_stream: handleReadLogStream,
887
- stop_log_stream: handleStopLogStream,
888
- classify_action_outcome: handleClassifyActionOutcome,
889
- get_network_activity: handleGetNetworkActivity
890
- }
891
-
892
- for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
893
- toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey')
894
- }
895
-
896
- export async function handleToolCall(name: string, args: ToolCallArgs = {}) {
897
- const handler = toolHandlers[name]
898
- if (!handler) throw new Error(`Unknown tool: ${name}`)
899
-
900
- const started = performance.now()
901
- const measured: Record<string, number> = {}
902
- const addTiming = (response: ToolCallResult) => {
903
- 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
904
- const item = response.content.find(item => item.type === 'text')
905
- if (item?.text) {
906
- try {
907
- const value = JSON.parse(item.text)
908
- value.timing = { ...measured, ...value.timing, total_ms: performance.now() - started }
909
- item.text = JSON.stringify(value)
910
- } catch { /* Non-JSON text responses retain their original content. */ }
911
- }
912
- return response
913
- }
914
- return withTiming(measured, async () => {
915
- try {
916
- validateExpectedState(name, args)
917
- return addTiming(await handler(args))
918
- } catch (error) {
919
- console.error(`Error executing tool ${name}:`, error)
920
- return addTiming(wrapToolError(name, error))
921
- }
922
- })
923
- }