mobile-debug-mcp 0.34.0 → 0.36.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 (500) 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/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,55 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { ActionTargetResolved } from '../../types.js'
3
+ import type { UiSelector } from '../../contracts/selector.js'
4
+ import type { UiTree, ObserveUI } from '../observation.js'
5
+ import type { UiElement } from '../model.js'
6
+ import type { UICondition } from './ui-wait-condition.js'
7
+ import { currentReadinessWait } from '../../observe/readiness.js'
8
+
9
+ export interface UIWaitInput {
10
+ selector?: UiSelector; condition?: UICondition; timeout_ms?: number; poll_interval_ms?: number
11
+ match?: { index?: number }; retry?: { max_attempts?: number; backoff_ms?: number }
12
+ platform?: 'android' | 'ios'; deviceId?: string; singleObservation?: boolean; singleAttempt?: boolean
13
+ rejectAmbiguous?: boolean; resolveTapTarget?: boolean
14
+ }
15
+ export interface UIWaitDependencies {
16
+ clock?: Clock
17
+ observe: ObserveUI
18
+ resolve(platform: 'android' | 'ios', deviceId: string | undefined, element: UiElement, index: number): ActionTargetResolved
19
+ }
20
+ export interface UIWaitResult {
21
+ status: string; error?: { code: string; message?: string }; matched?: number; element?: ActionTargetResolved | null; _tree?: UiTree
22
+ metrics?: { latency_ms: number; poll_count: number; attempts: number }
23
+ requested?: { selector: UiSelector; condition: UICondition; match: { index: number } | null }
24
+ observed?: { matched_count: number; condition_satisfied: boolean; selected_index: number | null; last_matched_element: ActionTargetResolved | null }
25
+ }
26
+ export function createUIWait(input: UIWaitInput, clock: Clock = systemClock) {
27
+ const tracker = currentReadinessWait(), start = clock.now()
28
+ const requestedIndex = typeof input.match?.index === 'number' ? input.match.index : null
29
+ const args = { condition: 'exists' as UICondition, timeout_ms: 60000, poll_interval_ms: 300,
30
+ retry: { max_attempts: 1, backoff_ms: 0 }, singleObservation: false, singleAttempt: false,
31
+ rejectAmbiguous: false, resolveTapTarget: false, ...input }
32
+ // Explicit undefined values have the same defaults as destructured handler parameters.
33
+ if (args.condition === undefined) args.condition = 'exists'
34
+ if (args.timeout_ms === undefined) args.timeout_ms = 60000
35
+ if (args.poll_interval_ms === undefined) args.poll_interval_ms = 300
36
+ if (args.retry === undefined) args.retry = { max_attempts: 1, backoff_ms: 0 }
37
+ const requested = { selector: args.selector ?? {}, condition: args.condition, match: requestedIndex === null ? null : { index: requestedIndex } }
38
+ return { clock, args, requestedIndex, requested, tracker, start, attempts: 0, polls: 0,
39
+ count: 0, element: null as ActionTargetResolved | null, met: false, stable: 0 }
40
+ }
41
+ export type UIWaitState = ReturnType<typeof createUIWait>
42
+ export function waitObserved(s: UIWaitState, condition = s.met) {
43
+ return { matched_count: s.count, condition_satisfied: condition, selected_index: s.element?.index ?? null, last_matched_element: s.element }
44
+ }
45
+ export function waitFailure(s: UIWaitState, code: string, message: string, observed = waitObserved(s, false)): UIWaitResult {
46
+ return { status: 'timeout', error: { code, message }, metrics: { latency_ms: s.clock.now() - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed }
47
+ }
48
+ export function validateUIWait(s: UIWaitState) {
49
+ const { selector, condition, platform } = s.args
50
+ const valid = [selector?.text, selector?.resource_id, selector?.accessibility_id].some(value => typeof value === 'string' && value.trim().length > 0)
51
+ if (!valid) return waitFailure(s, 'INVALID_SELECTOR', 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id')
52
+ if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) return waitFailure(s, 'INVALID_CONDITION', `Unsupported condition: ${condition}`)
53
+ if (platform && !['android', 'ios'].includes(platform)) return waitFailure(s, 'PLATFORM_NOT_SUPPORTED', `Unsupported platform: ${platform}`)
54
+ return null
55
+ }
@@ -0,0 +1,84 @@
1
+ import { systemClock } from '../../runtime/clock.js'
2
+ import type { UiTree } from '../observation.js'
3
+ import { remainingBudget } from '../../utils/operation-budget.js'
4
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
5
+ import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js'
6
+ import { readinessDelay } from '../../observe/readiness-service.js'
7
+ import { matchingElements, evaluateCondition } from './ui-wait-condition.js'
8
+ import { createUIWait, validateUIWait, waitObserved, waitFailure,
9
+ type UIWaitInput, type UIWaitDependencies, type UIWaitState, type UIWaitResult } from './ui-wait-state.js'
10
+
11
+ async function delay(s: UIWaitState, ms: number) {
12
+ await (s.tracker && s.clock === systemClock ? readinessDelay(ms) : s.clock.sleep(ms))
13
+ }
14
+ function acceptObservation(s: UIWaitState, tree: UiTree, now: number): UIWaitResult | null {
15
+ const age = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null
16
+ if (!(s.met && (age === null || age <= 500))) { s.stable = 0; return null }
17
+ s.stable++
18
+ if (s.stable < (s.args.singleObservation ? 1 : 2) || (s.tracker && s.tracker.summary.state !== 'settled')) return null
19
+ return { status: 'success', ...(s.args.singleObservation ? { _tree: tree } : {}), matched: s.count,
20
+ element: s.element, metrics: { latency_ms: now - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed: waitObserved(s, true) }
21
+ }
22
+ function inspectObservation(s: UIWaitState, tree: UiTree, deps: UIWaitDependencies): UIWaitResult | null {
23
+ const a = s.args, elements = tree && Array.isArray(tree.elements) ? tree.elements : []
24
+ const matches = matchingElements(elements, a.selector!)
25
+ if (a.rejectAmbiguous && matches.length > 1 && a.match?.index === undefined) return { status: 'timeout', error: { code: 'AMBIGUOUS_TARGET' } }
26
+ const index = typeof a.match?.index === 'number' ? a.match.index : undefined
27
+ const condition = evaluateCondition(elements, matches, a.condition, index, a.resolveTapTarget)
28
+ const platform = tree?.device?.platform === 'ios' ? 'ios' : a.platform || 'android'
29
+ const deviceId = tree?.device?.id || a.deviceId
30
+ s.count = matches.length; s.met = condition.met
31
+ s.element = condition.element ? deps.resolve(platform, deviceId, condition.element.el, condition.element.idx) : null
32
+ return acceptObservation(s, tree, s.clock.now())
33
+ }
34
+ function pollError(s: UIWaitState, error: unknown) {
35
+ checkDebuggerGuard()
36
+ if (error instanceof Error && error.message === 'ACTION_TIMEOUT') throw error
37
+ if (s.tracker && !String(error).includes('READINESS_')) s.tracker.unavailable()
38
+ if (s.tracker && isDeviceUnavailable(error)) throw error
39
+ if (!s.tracker) console.warn('waitForUI: poll error (non-fatal):', error instanceof Error ? error.message : String(error))
40
+ }
41
+ async function poll(s: UIWaitState, deps: UIWaitDependencies): Promise<UIWaitResult | null> {
42
+ remainingBudget(); s.polls++
43
+ try {
44
+ const started = s.clock.now(), a = s.args
45
+ const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId })
46
+ observeReadinessWait(tree, started)
47
+ if (a.resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error))) throw new Error('ACTION_TIMEOUT')
48
+ return inspectObservation(s, tree, deps)
49
+ } catch (error) { pollError(s, error) }
50
+ return null
51
+ }
52
+ async function runAttempts(s: UIWaitState, deps: UIWaitDependencies): Promise<UIWaitResult | undefined> {
53
+ const a = s.args
54
+ const interval = Math.max(50, Math.min(a.poll_interval_ms || 300, 2000))
55
+ const maximum = a.retry && a.retry.max_attempts ? Math.max(1, a.retry.max_attempts) : 1
56
+ const backoff = a.retry && a.retry.backoff_ms ? Math.max(0, a.retry.backoff_ms) : 0
57
+ while (s.attempts < maximum) {
58
+ s.attempts++
59
+ const deadline = s.clock.now() + (a.timeout_ms || 0)
60
+ while (s.clock.now() <= deadline || (a.singleAttempt && s.polls === 0)) {
61
+ const result = await poll(s, deps)
62
+ if (result) return result
63
+ if (a.singleAttempt) break
64
+ await delay(s, interval || 50)
65
+ }
66
+ if (s.attempts < maximum) { if (backoff > 0) await delay(s, backoff); continue }
67
+ const note = s.requestedIndex !== null && s.count <= s.requestedIndex
68
+ ? ` requested match.index=${s.requestedIndex} but observed ${s.count} match(es)` : ` observed ${s.count} match(es)`
69
+ return waitFailure(s, s.tracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', `Condition ${a.condition} not satisfied within timeout;${note}`, waitObserved(s))
70
+ }
71
+ }
72
+ export async function waitForUI(input: UIWaitInput, deps: UIWaitDependencies) {
73
+ const state = createUIWait(input, deps.clock)
74
+ const invalid = validateUIWait(state)
75
+ if (invalid) return invalid
76
+ try { return await runAttempts(state, deps) }
77
+ catch (error) {
78
+ checkDebuggerGuard()
79
+ if (!state.tracker && error instanceof Error && error.message === 'ACTION_TIMEOUT') throw error
80
+ if (state.tracker && isDeviceUnavailable(error)) throw error
81
+ return waitFailure(state, state.tracker && String(error).includes('ACTION_TIMEOUT') ? 'TIMEOUT' : 'INTERNAL_ERROR',
82
+ state.tracker ? 'Readiness predicate wait did not complete' : error instanceof Error ? error.message : String(error), waitObserved(state, state.tracker ? state.met : false))
83
+ }
84
+ }
@@ -41,7 +41,7 @@ export class AndroidManage {
41
41
  })
42
42
  proc.on('error', err => reject(err))
43
43
  })
44
-
44
+
45
45
  const apk = await findApk(projectPath)
46
46
  if (!apk) return { error: 'Could not find APK after build' }
47
47
  return { artifactPath: apk }
@@ -56,54 +56,7 @@ export class AndroidManage {
56
56
 
57
57
  let apkToInstall: string = apkPath
58
58
  try {
59
- const stat = await fs.stat(apkPath).catch(() => null)
60
- if (stat && stat.isDirectory()) {
61
- const detectedJavaHome = await detectJavaHome().catch(() => undefined)
62
- const env = { ...process.env }
63
- if (detectedJavaHome) {
64
- if (env.JAVA_HOME !== detectedJavaHome) {
65
- env.JAVA_HOME = detectedJavaHome
66
- env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
67
- console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome)
68
- }
69
- }
70
- try { delete env.SHELL } catch {}
71
-
72
- const gradleArgs = ['assembleDebug']
73
- if (detectedJavaHome) {
74
- gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
75
- gradleArgs.push('--no-daemon')
76
- env.GRADLE_JAVA_HOME = detectedJavaHome
77
- }
78
-
79
- const wrapperPath = path.join(apkPath, 'gradlew')
80
- const useWrapper = existsSync(wrapperPath)
81
- const execCmd = useWrapper ? wrapperPath : 'gradle'
82
- const spawnOpts: any = { cwd: apkPath, env }
83
- if (useWrapper) {
84
- await fs.chmod(wrapperPath, 0o755).catch(() => {})
85
- // Run wrapper directly to avoid shell splitting of args
86
- spawnOpts.shell = false
87
- } else {
88
- // Execute gradle directly without a shell so paths with spaces are preserved
89
- spawnOpts.shell = false
90
- }
91
-
92
- const proc = spawn(execCmd, gradleArgs, spawnOpts)
93
- let stderr = ''
94
- await new Promise<void>((resolve, reject) => {
95
- proc.stderr?.on('data', d => stderr += d.toString())
96
- proc.on('close', code => {
97
- if (code === 0) resolve()
98
- else reject(new Error(stderr || `Gradle build failed with code ${code}`))
99
- })
100
- proc.on('error', err => reject(err))
101
- })
102
-
103
- const built = await findApk(apkPath)
104
- if (!built) throw new Error('Could not locate built APK after running Gradle')
105
- apkToInstall = built
106
- }
59
+ apkToInstall = await prepareInstallApk(apkPath)
107
60
 
108
61
  try {
109
62
  const res = await spawnAdb(['install', '-r', apkToInstall], deviceId)
@@ -138,7 +91,7 @@ export class AndroidManage {
138
91
  }
139
92
  throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed')
140
93
  } finally {
141
- try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch {}
94
+ try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch { }
142
95
  }
143
96
  } catch (e) {
144
97
  // gather diagnostics for attempted adb operations
@@ -224,3 +177,56 @@ export class AndroidManage {
224
177
  }
225
178
  }
226
179
  }
180
+
181
+ async function prepareInstallApk(apkPath: string): Promise<string> {
182
+ const stat = await fs.stat(apkPath).catch(() => null)
183
+ if (stat && stat.isDirectory()) {
184
+ const detectedJavaHome = await detectJavaHome().catch(() => undefined)
185
+ const env = { ...process.env }
186
+ if (detectedJavaHome) {
187
+ if (env.JAVA_HOME !== detectedJavaHome) {
188
+ env.JAVA_HOME = detectedJavaHome
189
+ env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
190
+ console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome)
191
+ }
192
+ }
193
+ try { delete env.SHELL } catch { }
194
+
195
+ const gradleArgs = ['assembleDebug']
196
+ if (detectedJavaHome) {
197
+ gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
198
+ gradleArgs.push('--no-daemon')
199
+ env.GRADLE_JAVA_HOME = detectedJavaHome
200
+ }
201
+
202
+ const wrapperPath = path.join(apkPath, 'gradlew')
203
+ const useWrapper = existsSync(wrapperPath)
204
+ const execCmd = useWrapper ? wrapperPath : 'gradle'
205
+ const spawnOpts: import('node:child_process').SpawnOptions = { cwd: apkPath, env }
206
+ if (useWrapper) {
207
+ await fs.chmod(wrapperPath, 0o755).catch(() => { })
208
+ // Run wrapper directly to avoid shell splitting of args
209
+ spawnOpts.shell = false
210
+ } else {
211
+ // Execute gradle directly without a shell so paths with spaces are preserved
212
+ spawnOpts.shell = false
213
+ }
214
+
215
+ const proc = spawn(execCmd, gradleArgs, spawnOpts)
216
+ let stderr = ''
217
+ await new Promise<void>((resolve, reject) => {
218
+ proc.stderr?.on('data', d => stderr += d.toString())
219
+ proc.on('close', code => {
220
+ if (code === 0) resolve()
221
+ else reject(new Error(stderr || `Gradle build failed with code ${code}`))
222
+ })
223
+ proc.on('error', err => reject(err))
224
+ })
225
+
226
+ const built = await findApk(apkPath)
227
+ if (!built) throw new Error('Could not locate built APK after running Gradle')
228
+ return built
229
+ }
230
+
231
+ return apkPath
232
+ }
@@ -1,3 +1,4 @@
1
+ import type { IOSBuildOptions } from './ios.js'
1
2
  import { promises as fs } from 'fs'
2
3
  import path from 'path'
3
4
  import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js'
@@ -11,9 +12,9 @@ import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, Restar
11
12
  export { AndroidManage } from './android.js';
12
13
  export { iOSManage } from './ios.js';
13
14
 
14
- export async function detectProjectPlatform(projectPath: string): Promise<'ios'|'android'|'ambiguous'|'unknown'> {
15
+ export async function detectProjectPlatform(projectPath: string): Promise<'ios' | 'android' | 'ambiguous' | 'unknown'> {
15
16
  // Recursively scan up to a limited depth for platform markers to avoid mis-detection
16
- async function scan(dir: string, depth = 3): Promise<{ ios: boolean, android: boolean }>{
17
+ async function scan(dir: string, depth = 3): Promise<{ ios: boolean, android: boolean }> {
17
18
  const res = { ios: false, android: false }
18
19
  try {
19
20
  const ents = await fs.readdir(dir).catch(() => [])
@@ -28,15 +29,14 @@ export async function detectProjectPlatform(projectPath: string): Promise<'ios'|
28
29
  try {
29
30
  const full = path.join(dir, e)
30
31
  const st = await fs.stat(full).catch(() => null)
31
- if (st && st.isDirectory()) {
32
- const child = await scan(full, depth - 1)
33
- if (child.ios) res.ios = true
34
- if (child.android) res.android = true
35
- if (res.ios && res.android) return res
36
- }
37
- } catch {}
32
+ if (!st || !st.isDirectory()) continue
33
+ const child = await scan(full, depth - 1)
34
+ if (child.ios) res.ios = true
35
+ if (child.android) res.android = true
36
+ if (res.ios && res.android) return res
37
+ } catch { }
38
38
  }
39
- } catch {}
39
+ } catch { }
40
40
  return res
41
41
  }
42
42
 
@@ -78,7 +78,7 @@ export class ToolsManage {
78
78
  static async build_android({ projectPath, gradleTask, maxWorkers, gradleCache, forceClean }: { projectPath: string, gradleTask?: string, maxWorkers?: number, gradleCache?: boolean, forceClean?: boolean }) {
79
79
  const android = new AndroidManage()
80
80
  const task = gradleTask || 'assembleDebug'
81
- return await (android as any).build(projectPath, {
81
+ return await android.build(projectPath, {
82
82
  variant: task,
83
83
  env: mergeDefinedEnv({
84
84
  MCP_GRADLE_TASK: task,
@@ -92,7 +92,7 @@ export class ToolsManage {
92
92
  static async build_ios({ projectPath, workspace: _workspace, project: _project, scheme: _scheme, destinationUDID, derivedDataPath, buildJobs, forceClean }: { projectPath: string, workspace?: string, project?: string, scheme?: string, destinationUDID?: string, derivedDataPath?: string, buildJobs?: number, forceClean?: boolean }) {
93
93
  const ios = new iOSManage()
94
94
 
95
- const opts: any = {}
95
+ const opts: IOSBuildOptions = {}
96
96
  if (_workspace) opts.workspace = _workspace
97
97
  if (_project) opts.project = _project
98
98
  if (_scheme) opts.scheme = _scheme
@@ -103,7 +103,7 @@ export class ToolsManage {
103
103
  // prefer explicit xcodebuild path from env
104
104
  if (process.env.XCODEBUILD_PATH) opts.xcodeCmd = process.env.XCODEBUILD_PATH
105
105
 
106
- return await (ios as any).build(projectPath, {
106
+ return await ios.build(projectPath, {
107
107
  ...opts,
108
108
  env: mergeDefinedEnv({
109
109
  MCP_DERIVED_DATA: derivedDataPath,
@@ -115,7 +115,7 @@ export class ToolsManage {
115
115
  })
116
116
  }
117
117
 
118
- static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android'|'ios', buildMode?: 'debug'|'release'|'profile', maxWorkers?: number, forceClean?: boolean }) {
118
+ static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android' | 'ios', buildMode?: 'debug' | 'release' | 'profile', maxWorkers?: number, forceClean?: boolean }) {
119
119
  // Prefer using flutter CLI when available; otherwise delegate to native subproject builders
120
120
  const flutterCmd = process.env.FLUTTER_PATH || 'flutter'
121
121
  // silence unused params
@@ -131,10 +131,8 @@ export class ToolsManage {
131
131
  // Try to find built APK
132
132
  const apk = await findApk(path.join(projectPath))
133
133
  if (apk) return { artifactPath: apk, output: out }
134
- } catch (err: any) {
135
- const stdout = err && err.stdout ? String(err.stdout) : ''
136
- const stderr = err && err.stderr ? String(err.stderr) : ''
137
- throw new Error(`flutter build apk failed: ${stderr || stdout || err.message}`)
134
+ } catch (thrown: unknown) {
135
+ throw flutterBuildError(thrown, 'apk')
138
136
  }
139
137
  }
140
138
 
@@ -144,10 +142,8 @@ export class ToolsManage {
144
142
  const out = execSync(`${flutterCmd} build ios --${mode} --no-codesign`, { cwd: projectPath, encoding: 'utf8' })
145
143
  const app = await findAppBundle(path.join(projectPath))
146
144
  if (app) return { artifactPath: app, output: out }
147
- } catch (err: any) {
148
- const stdout = err && err.stdout ? String(err.stdout) : ''
149
- const stderr = err && err.stderr ? String(err.stderr) : ''
150
- throw new Error(`flutter build ios failed: ${stderr || stdout || err.message}`)
145
+ } catch (thrown: unknown) {
146
+ throw flutterBuildError(thrown, 'ios')
151
147
  }
152
148
  }
153
149
  } catch (e) {
@@ -156,38 +152,24 @@ export class ToolsManage {
156
152
  void e
157
153
  }
158
154
 
159
- // Fallback: try native subproject builds
160
- if (!platform || platform === 'android') {
161
- const androidDir = path.join(projectPath, 'android')
162
- const android = new AndroidManage()
163
- const artifact = await (android as any).build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug')
164
- return artifact
165
- }
166
- if (!platform || platform === 'ios') {
167
- const iosDir = path.join(projectPath, 'ios')
168
- const ios = new iOSManage()
169
- const artifact = await (ios as any).build(iosDir)
170
- return artifact
171
- }
172
-
173
- return { error: 'Unable to build flutter project' }
155
+ return buildFlutterNative(projectPath, platform, _forceClean)
174
156
  }
175
157
 
176
- static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android'|'ios', variant?: string, maxWorkers?: number, forceClean?: boolean }) {
158
+ static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android' | 'ios', variant?: string, maxWorkers?: number, forceClean?: boolean }) {
177
159
  // silence unused params
178
160
  void _maxWorkers; void _forceClean;
179
161
  // React Native typically uses native subprojects. Delegate to Android/iOS builders.
180
162
  if (!platform || platform === 'android') {
181
163
  const androidDir = path.join(projectPath, 'android')
182
164
  const android = new AndroidManage()
183
- const artifact = await (android as any).build(androidDir, variant || 'assembleDebug')
165
+ const artifact = await android.build(androidDir, variant || 'assembleDebug')
184
166
  return artifact
185
167
  }
186
168
  if (!platform || platform === 'ios') {
187
169
  const iosDir = path.join(projectPath, 'ios')
188
170
  // Recommend running `pod install` prior to building in CI; not performed automatically here
189
171
  const ios = new iOSManage()
190
- const artifact = await (ios as any).build(iosDir)
172
+ const artifact = await ios.build(iosDir)
191
173
  return artifact
192
174
  }
193
175
  return { error: 'Unable to build react-native project' }
@@ -199,11 +181,11 @@ export class ToolsManage {
199
181
  const chosen = platform || 'android'
200
182
  if (chosen === 'android') {
201
183
  const android = new AndroidManage()
202
- const artifact = await (android as any).build(projectPath, { variant })
184
+ const artifact = await android.build(projectPath, { variant })
203
185
  return artifact
204
186
  } else {
205
187
  const ios = new iOSManage()
206
- const artifact = await (ios as any).build(projectPath, { variant })
188
+ const artifact = await ios.build(projectPath, { variant })
207
189
  return artifact
208
190
  }
209
191
  }
@@ -214,7 +196,7 @@ export class ToolsManage {
214
196
  throw new Error('Both platform and projectType parameters are required (platform: ios|android, projectType: native|kmp|react-native|flutter).')
215
197
  }
216
198
 
217
- const chosenPlatform: 'android'|'ios' = platform
199
+ const chosenPlatform: 'android' | 'ios' = platform
218
200
 
219
201
  if (chosenPlatform === 'android') {
220
202
  const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
@@ -271,7 +253,7 @@ export class ToolsManage {
271
253
 
272
254
  static async buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType }: { platform: 'android' | 'ios', projectPath: string, deviceId?: string, timeout?: number, projectType: 'native' | 'kmp' | 'react-native' | 'flutter' }) {
273
255
  const events: string[] = []
274
- const pushEvent = (obj: any) => events.push(JSON.stringify(obj))
256
+ const pushEvent = (obj: unknown) => events.push(JSON.stringify(obj))
275
257
  const effectiveTimeout = timeout ?? 180000 // reserved for future streaming/timeouts
276
258
  void effectiveTimeout
277
259
 
@@ -281,56 +263,19 @@ export class ToolsManage {
281
263
  return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Both platform and projectType parameters are required (platform: ios|android, projectType: native|kmp|react-native|flutter).' } }
282
264
  }
283
265
 
284
- // determine platform if not provided by inspecting path or projectType hint
285
- let chosenPlatform = platform
286
- try {
287
- if (!chosenPlatform) {
288
- // If caller provided projectType, respect it as a hard override and map to platform
289
- if (projectType) {
290
- if (projectType === 'kmp' || projectType === 'react-native' || projectType === 'flutter') {
291
- chosenPlatform = 'android'
292
- pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing android platform` })
293
- } else if (projectType === 'native' || projectType === 'ios') {
294
- chosenPlatform = 'ios'
295
- pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing ios platform` })
296
- } else {
297
- pushEvent({ type: 'build', status: 'failed', error: `Unknown projectType: ${projectType}` })
298
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: `Unknown projectType: ${projectType}` } }
299
- }
300
- } else {
301
- // If autodetect is disabled, require explicit platform or projectType
302
- if (process.env.MCP_DISABLE_AUTODETECT === '1') {
303
- pushEvent({ type: 'build', status: 'failed', error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType' })
304
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType (ios|android).' } }
305
- }
306
-
307
- const det = await detectProjectPlatform(projectPath)
308
- if (det === 'ios' || det === 'android') {
309
- chosenPlatform = det
310
- } else if (det === 'ambiguous') {
311
- pushEvent({ type: 'build', status: 'failed', error: 'Ambiguous project (contains both iOS and Android). Please provide platform: "ios" or "android".' })
312
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Ambiguous project - please provide explicit platform parameter (ios|android).' } }
313
- } else {
314
- // Unknown project type - do not guess. Request explicit platform.
315
- pushEvent({ type: 'build', status: 'failed', error: 'Unknown project type - unable to autodetect platform. Please provide platform or projectType.' })
316
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Unknown project type - please provide platform or projectType (ios|android).' } }
317
- }
318
- }
319
- }
320
- } catch {
321
- // detection failed; avoid guessing a platform
322
- }
266
+ // Explicit platform/projectType were required above; no autodetection occurs.
267
+ const chosenPlatform = platform
323
268
 
324
269
  pushEvent({ type: 'build', status: 'started', platform: chosenPlatform })
325
270
 
326
- let buildRes: any
271
+ let buildRes: { artifactPath?: string; error?: string; output?: string; diagnostics?: unknown }
327
272
  try {
328
- buildRes = await ToolsManage.buildAppHandler({ platform: chosenPlatform as any, projectPath })
329
- if (buildRes && (buildRes as any).error) {
330
- pushEvent({ type: 'build', status: 'failed', error: (buildRes as any).error })
331
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: (buildRes as any).error } }
273
+ buildRes = await ToolsManage.buildAppHandler({ platform: chosenPlatform, projectPath })
274
+ if (buildRes && buildRes.error) {
275
+ pushEvent({ type: 'build', status: 'failed', error: buildRes.error })
276
+ return { ndjson: events.join('\n') + '\n', result: { success: false, error: buildRes.error } }
332
277
  }
333
- pushEvent({ type: 'build', status: 'finished', artifactPath: (buildRes as any).artifactPath })
278
+ pushEvent({ type: 'build', status: 'finished', artifactPath: buildRes.artifactPath })
334
279
  } catch (e) {
335
280
  const msg = e instanceof Error ? e.message : String(e)
336
281
  pushEvent({ type: 'build', status: 'failed', error: msg })
@@ -338,11 +283,11 @@ export class ToolsManage {
338
283
  }
339
284
 
340
285
  // Install phase
341
- const artifact = (buildRes as any).artifactPath || projectPath
286
+ const artifact = buildRes.artifactPath || projectPath
342
287
  pushEvent({ type: 'install', status: 'started', artifactPath: artifact, deviceId })
343
- let installRes: any
288
+ let installRes: InstallAppResponse
344
289
  try {
345
- installRes = await ToolsManage.installAppHandler({ platform: chosenPlatform as any, appPath: artifact, deviceId, projectType })
290
+ installRes = await ToolsManage.installAppHandler({ platform: chosenPlatform, appPath: artifact, deviceId, projectType })
346
291
  if (installRes && installRes.installed === true) {
347
292
  pushEvent({ type: 'install', status: 'finished', artifactPath: artifact, device: installRes.device })
348
293
  return { ndjson: events.join('\n') + '\n', result: { success: true, artifactPath: artifact, device: installRes.device, output: installRes.output } }
@@ -358,7 +303,32 @@ export class ToolsManage {
358
303
  }
359
304
 
360
305
  static async listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
361
- const devices = await listDevices(platform as any, appId)
306
+ const devices = await listDevices(platform, appId)
362
307
  return { devices }
363
308
  }
364
309
  }
310
+
311
+ async function buildFlutterNative(projectPath: string, platform?: 'android' | 'ios', _forceClean?: boolean) {
312
+ // Fallback: try native subproject builds
313
+ if (!platform || platform === 'android') {
314
+ const androidDir = path.join(projectPath, 'android')
315
+ const android = new AndroidManage()
316
+ const artifact = await android.build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug')
317
+ return artifact
318
+ }
319
+ if (!platform || platform === 'ios') {
320
+ const iosDir = path.join(projectPath, 'ios')
321
+ const ios = new iOSManage()
322
+ const artifact = await ios.build(iosDir)
323
+ return artifact
324
+ }
325
+
326
+ return { error: 'Unable to build flutter project' }
327
+ }
328
+
329
+ function flutterBuildError(thrown: unknown, kind: string): Error {
330
+ const err = thrown as { stdout?: unknown; stderr?: unknown; message?: string }
331
+ const stdout = err && err.stdout ? String(err.stdout) : ''
332
+ const stderr = err && err.stderr ? String(err.stderr) : ''
333
+ return new Error(`flutter build ${kind} failed: ${stderr || stdout || err.message}`)
334
+ }
@@ -0,0 +1,14 @@
1
+ export type BuildEnv = Record<string, string | undefined>
2
+
3
+ export interface IOSBuildOptions {
4
+ variant?: string
5
+ workspace?: string
6
+ project?: string
7
+ scheme?: string
8
+ destinationUDID?: string
9
+ derivedDataPath?: string
10
+ buildJobs?: number
11
+ forceClean?: boolean
12
+ xcodeCmd?: string
13
+ env?: BuildEnv
14
+ }