mobile-debug-mcp 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +16 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -1,269 +1,13 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { captureEvidence } from './evidence.js';
3
- import { spawn } from "child_process";
4
- import { remainingBudget } from '../utils/operation-budget.js';
5
- import { execCmd } from '../utils/exec.js';
6
- import { createObservationMeasurement } from './performance-metrics.js';
7
- import { promises as fs } from "fs";
8
- import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcrunCmd, isIDBInstalled } from "../utils/ios/utils.js";
9
- import { createWriteStream, promises as fsPromises } from 'fs';
10
- import path from 'path';
11
- import { parseLogLine } from '../utils/android/utils.js';
12
- import { computeScreenFingerprint } from '../utils/ui/index.js';
13
- import { parsePngSize } from '../utils/image.js';
14
- import { deriveSnapshotMetadata } from './snapshot-metadata.js';
15
- const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
16
- let iosExecCommand = execCommand;
17
- export function _setIOSExecCommandForTests(fn) {
18
- iosExecCommand = fn;
19
- }
20
- export function _resetIOSExecCommandForTests() {
21
- iosExecCommand = execCommand;
22
- }
23
- function parseIDBFrame(frame) {
24
- if (!frame)
25
- return [0, 0, 0, 0];
26
- if (typeof frame === 'string') {
27
- const nums = frame.match(/-?\d+(?:\.\d+)?/g);
28
- if (!nums || nums.length < 4)
29
- return [0, 0, 0, 0];
30
- const x = Number(nums[0]);
31
- const y = Number(nums[1]);
32
- const w = Number(nums[2]);
33
- const h = Number(nums[3]);
34
- return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
35
- }
36
- const x = Number(frame.x || 0);
37
- const y = Number(frame.y || 0);
38
- const w = Number(frame.width || frame.w || 0);
39
- const h = Number(frame.height || frame.h || 0);
40
- return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
41
- }
42
- function getCenter(bounds) {
43
- const [x1, y1, x2, y2] = bounds;
44
- return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
45
- }
46
- function parseIOSNumber(value) {
47
- if (typeof value === 'number' && Number.isFinite(value))
48
- return value;
49
- if (typeof value !== 'string')
50
- return null;
51
- const parsed = Number(value);
52
- return Number.isFinite(parsed) ? parsed : null;
53
- }
54
- function normalizeIOSType(value) {
55
- return typeof value === 'string' ? value.trim().toLowerCase() : '';
56
- }
57
- function inferIOSRole(type, traits) {
58
- if (/slider|adjustable/.test(type) || traits.some((trait) => /adjustable|slider/.test(trait)))
59
- return 'slider';
60
- if (/stepper/.test(type))
61
- return 'stepper';
62
- if (/picker|pop up button|dropdown/.test(type))
63
- return 'dropdown';
64
- if (/segmented control/.test(type))
65
- return 'segmented_control';
66
- if (/button/.test(type) || traits.some((trait) => /button/.test(trait)))
67
- return 'button';
68
- if (/cell/.test(type))
69
- return 'cell';
70
- if (/switch/.test(type))
71
- return 'switch';
72
- if (/text field|textfield|search field/.test(type))
73
- return 'text_field';
74
- if (/image/.test(type))
75
- return 'image';
76
- if (/window|application|group|scroll view|collection view/.test(type))
77
- return 'container';
78
- return null;
79
- }
80
- function getIOSStableId(node) {
81
- const candidates = [node.AXIdentifier, node.accessibilityIdentifier, node.identifier, node.AXUniqueId];
82
- for (const candidate of candidates) {
83
- if (typeof candidate === 'string' && candidate.trim().length > 0)
84
- return candidate;
85
- }
86
- return null;
87
- }
88
- function buildIOSSelectorConfidence(source) {
89
- switch (source) {
90
- case 'identifier':
91
- return { score: 1, reason: 'accessibility_identifier' };
92
- case 'label':
93
- return { score: 0.9, reason: 'label_match' };
94
- case 'value':
95
- return { score: 0.75, reason: 'value_match' };
96
- case 'type':
97
- return { score: 0.35, reason: 'type_match' };
98
- default:
99
- return null;
100
- }
101
- }
102
- function buildIOSSelector(type, label, value, stableId) {
103
- if (stableId)
104
- return { value: stableId, confidence: buildIOSSelectorConfidence('identifier') };
105
- if (label)
106
- return { value: label, confidence: buildIOSSelectorConfidence('label') };
107
- if (value)
108
- return { value: value, confidence: buildIOSSelectorConfidence('value') };
109
- if (type)
110
- return { value: type, confidence: buildIOSSelectorConfidence('type') };
111
- return null;
112
- }
113
- function buildIOSSemantic(type, traits, role, value) {
114
- const semantic = {
115
- is_clickable: traits.includes("UIAccessibilityTraitButton") || /adjustable|slider/.test(type) || type === "Button" || type === "Cell",
116
- is_container: /window|application|group|scroll view|collection view/.test(type)
117
- };
118
- if (role === 'slider') {
119
- semantic.semantic_role = 'slider';
120
- semantic.adjustable = true;
121
- semantic.supported_actions = ['adjust'];
122
- semantic.state_shape = 'continuous';
123
- }
124
- else if (role === 'stepper') {
125
- semantic.semantic_role = 'stepper';
126
- semantic.adjustable = true;
127
- semantic.supported_actions = ['increment', 'decrement'];
128
- semantic.state_shape = 'discrete';
129
- }
130
- else if (role === 'dropdown') {
131
- semantic.semantic_role = 'dropdown';
132
- semantic.supported_actions = ['tap', 'expand'];
133
- semantic.state_shape = 'semantic';
134
- }
135
- else if (role === 'segmented_control') {
136
- semantic.semantic_role = 'segmented_control';
137
- semantic.supported_actions = ['tap'];
138
- semantic.state_shape = 'discrete';
139
- }
140
- else if (traits.some((trait) => /adjustable|slider/i.test(trait)) || /adjustable|slider/.test(type)) {
141
- semantic.semantic_role = 'custom_adjustable';
142
- semantic.adjustable = true;
143
- semantic.supported_actions = ['adjust'];
144
- semantic.state_shape = 'continuous';
145
- }
146
- else if (semantic.is_clickable) {
147
- semantic.supported_actions = ['tap'];
148
- }
149
- if (semantic.state_shape === undefined && semantic.adjustable && value !== null) {
150
- const numericValue = parseIOSNumber(value);
151
- if (numericValue !== null && numericValue >= 0 && numericValue <= 1) {
152
- semantic.state_shape = 'continuous';
153
- }
154
- }
155
- return semantic;
156
- }
157
- function isIOSAdjustable(node, type, traits) {
158
- return /slider|adjustable|stepper/i.test(type) || traits.some((trait) => /adjustable|slider/i.test(trait));
159
- }
160
- function extractIOSState(node, type, label, value, traits) {
161
- const state = {};
162
- const normalizedTraits = traits.map((trait) => String(trait).toLowerCase());
163
- if (normalizedTraits.some((trait) => /selected/.test(trait))) {
164
- state.selected = label || value || true;
165
- }
166
- if (normalizedTraits.some((trait) => /focused/.test(trait))) {
167
- state.focused = true;
168
- }
169
- if (normalizedTraits.some((trait) => /enabled/.test(trait))) {
170
- state.enabled = true;
171
- }
172
- if (normalizedTraits.some((trait) => /disabled/.test(trait))) {
173
- state.enabled = false;
174
- }
175
- if (value && /textfield|search|text/i.test(type)) {
176
- state.text_value = value;
177
- }
178
- if (isIOSAdjustable(node, type, traits)) {
179
- const rawValue = parseIOSNumber(value);
180
- if (rawValue !== null) {
181
- state.raw_value = rawValue;
182
- state.value = rawValue >= 0 && rawValue <= 1 ? Math.round(rawValue * 100) : rawValue;
183
- }
184
- else if (value) {
185
- state.raw_value = value;
186
- state.value = value;
187
- }
188
- }
189
- else if (value) {
190
- const numericValue = parseIOSNumber(value);
191
- if (numericValue !== null) {
192
- state.value = numericValue;
193
- state.raw_value = numericValue;
194
- }
195
- else {
196
- state.value = value;
197
- }
198
- }
199
- return Object.keys(state).length > 0 ? state : null;
200
- }
201
- export function traverseIDBNode(node, elements, parentIndex = -1, depth = 0) {
202
- if (!node)
203
- return -1;
204
- let currentIndex = -1;
205
- const type = node.AXElementType || node.type || "unknown";
206
- const label = node.AXLabel || node.label || null;
207
- const value = node.AXValue || null;
208
- const frame = node.AXFrame || node.frame;
209
- const traits = node.AXTraits || [];
210
- const state = extractIOSState(node, type, label, value, traits);
211
- const normalizedType = normalizeIOSType(type);
212
- const stableId = getIOSStableId(node);
213
- const selector = buildIOSSelector(type, label, value, stableId);
214
- const role = inferIOSRole(normalizedType, traits);
215
- const semantic = buildIOSSemantic(normalizedType, traits, role, value);
216
- const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
217
- const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
218
- if (isUseful) {
219
- const bounds = parseIDBFrame(frame);
220
- const element = {
221
- text: label,
222
- contentDescription: value,
223
- type: type,
224
- resourceId: stableId,
225
- clickable: clickable,
226
- enabled: true,
227
- visible: true,
228
- bounds: bounds,
229
- center: getCenter(bounds),
230
- depth: depth,
231
- state,
232
- stable_id: stableId,
233
- role,
234
- test_tag: stableId,
235
- selector,
236
- semantic
237
- };
238
- if (parentIndex !== -1) {
239
- element.parentId = parentIndex;
240
- }
241
- elements.push(element);
242
- currentIndex = elements.length - 1;
243
- }
244
- const nextParentIndex = currentIndex !== -1 ? currentIndex : parentIndex;
245
- const nextDepth = currentIndex !== -1 ? depth + 1 : depth;
246
- const childrenIndices = [];
247
- if (node.children && Array.isArray(node.children)) {
248
- for (const child of node.children) {
249
- const childIndex = traverseIDBNode(child, elements, nextParentIndex, nextDepth);
250
- if (childIndex !== -1) {
251
- childrenIndices.push(childIndex);
252
- }
253
- }
254
- }
255
- if (currentIndex !== -1 && childrenIndices.length > 0) {
256
- elements[currentIndex].children = childrenIndices;
257
- }
258
- return currentIndex;
259
- }
260
- const iosActiveLogStreams = new Map();
261
- export function _setIOSActiveLogStream(sessionId, file, device) {
262
- iosActiveLogStreams.set(sessionId, { proc: {}, file, device });
263
- }
264
- export function _clearIOSActiveLogStream(sessionId) {
265
- iosActiveLogStreams.delete(sessionId);
266
- }
2
+ import { getIOSDeviceMetadata } from "../utils/ios/utils.js";
3
+ import { iosActiveLogStreams } from './ios/context.js';
4
+ import { collectGetUITree } from './ios/ui-tree.js';
5
+ import { collectGetLogs } from './ios/logs.js';
6
+ import { collectCaptureScreenshot } from './ios/screenshot.js';
7
+ import { collectGetScreenFingerprint } from './ios/fingerprint.js';
8
+ import { startLogStream, stopLogStream, readLogStream, collectReadLogStream } from './ios/log-stream.js';
9
+ export { traverseIDBNode } from './ios/ui-elements.js';
10
+ export { _setIOSExecCommandForTests, _resetIOSExecCommandForTests, _setIOSActiveLogStream, _clearIOSActiveLogStream } from './ios/context.js';
267
11
  export class iOSObserve {
268
12
  getLogStreamDevice(sessionId = 'default') { return iosActiveLogStreams.get(sessionId)?.device ?? null; }
269
13
  async getDeviceMetadata(deviceId = "booted") {
@@ -272,463 +16,21 @@ export class iOSObserve {
272
16
  async getLogs(filters = {}) {
273
17
  return captureEvidence('logs', { platform: 'ios', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters));
274
18
  }
275
- async collectGetLogs(filters = {}) {
276
- const { appId, deviceId = 'booted', pid, tag, level, contains, since_seconds, limit } = filters;
277
- const args = ['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog'];
278
- // Default to last N seconds if no since_seconds provided; limit lines handled after parsing
279
- const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50;
280
- if (since_seconds) {
281
- // log show accepts --last <time>
282
- args.push('--last', `${since_seconds}s`);
283
- }
284
- else {
285
- // default to last 60s to keep quick
286
- args.push('--last', '60s');
287
- }
288
- let processNameUsed = undefined;
289
- if (appId) {
290
- validateBundleId(appId);
291
- // prefer matching the simple process name (last segment of bundle id), but also match full bundle id in subsystem
292
- const parts = appId.split('.');
293
- const simpleName = parts[parts.length - 1];
294
- processNameUsed = simpleName;
295
- // predicate: match process by simple name or full bundle id, or subsystem contains bundle id
296
- args.push('--predicate', `process == "${simpleName}" or process == "${appId}" or subsystem contains "${appId}"`);
297
- }
298
- else if (tag) {
299
- // predicate by subsystem/category
300
- args.push('--predicate', `subsystem contains "${tag}"`);
301
- }
302
- try {
303
- // Attempt pid detection if appId provided and no explicit pid supplied — prefer process name derived from bundle id
304
- let detectedPid = null;
305
- if (appId && !pid) {
306
- const parts = appId.split('.');
307
- const simpleName = parts[parts.length - 1];
308
- try {
309
- const pgrepRes = await iosExecCommand(['simctl', 'spawn', deviceId, 'pgrep', '-f', simpleName], deviceId);
310
- const out = pgrepRes && pgrepRes.output ? pgrepRes.output.trim() : '';
311
- const firstLine = out.split(/\r?\n/).find(Boolean);
312
- if (firstLine) {
313
- const n = Number(firstLine.trim());
314
- if (!isNaN(n) && n > 0)
315
- detectedPid = n;
316
- }
317
- }
318
- catch {
319
- // ignore pgrep failures — we'll fall back to process/bundle matching
320
- }
321
- }
322
- const effectivePid = pid || detectedPid || null;
323
- const result = await iosExecCommand(args, deviceId);
324
- const device = await getIOSDeviceMetadata(deviceId);
325
- const rawLines = result.output ? result.output.split(/\r?\n/).filter(Boolean) : [];
326
- // Parse lines into structured entries. iOS log format: timestamp [PID:tid] <level> subsystem:category: message
327
- const parsed = rawLines.map(line => {
328
- // Example: 2023-08-12 12:34:56.789012+0000 pid <Debug> MyApp[123:456] <info> MySubsystem: MyCategory: Message here
329
- // Simpler approach: try to extract ISO timestamp at start
330
- let ts = null;
331
- const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)/);
332
- if (tsMatch) {
333
- const d = new Date(tsMatch[1]);
334
- if (!isNaN(d.getTime()))
335
- ts = d.toISOString();
336
- }
337
- // level mapping
338
- let lvl = 'INFO';
339
- const lvlMatch = line.match(/\b(Debug|Info|Default|Error|Fault|Warning)\b/i);
340
- if (lvlMatch) {
341
- const map = { 'debug': 'DEBUG', 'info': 'INFO', 'default': 'DEBUG', 'error': 'ERROR', 'fault': 'ERROR', 'warning': 'WARN' };
342
- lvl = map[(lvlMatch[1] || '').toLowerCase()] || 'INFO';
343
- }
344
- // subsystem/category -> tag
345
- let tagVal = '';
346
- const tagMatch = line.match(/\s([A-Za-z0-9_./-]+):\s/);
347
- if (tagMatch)
348
- tagVal = tagMatch[1];
349
- // pid extraction
350
- let pidNum = null;
351
- const pidMatch = line.match(/\[(\d+):\d+\]/);
352
- if (pidMatch)
353
- pidNum = Number(pidMatch[1]);
354
- // message: extract robustly after the subsystem/category token when available
355
- let message = line;
356
- if (tagMatch) {
357
- // tagMatch[0] includes the delimiter (e.g. " MySubsystem: ") — use it to find the message start
358
- const marker = tagMatch[0];
359
- const idx = line.indexOf(marker);
360
- if (idx !== -1) {
361
- message = line.slice(idx + marker.length).trim();
362
- }
363
- else {
364
- // fallback: try to trim off common prefixes (timestamp, pid, level) and keep the rest
365
- const afterPidMatch = line.match(/\]\s+/);
366
- if (afterPidMatch) {
367
- const afterPidIdx = line.indexOf(afterPidMatch[0]) + afterPidMatch[0].length;
368
- message = line.slice(afterPidIdx).trim();
369
- }
370
- else {
371
- // remove leading level tokens like <Debug> and keep remainder
372
- message = line.replace(/^.*?<.*?>\s*/, '').trim();
373
- }
374
- }
375
- }
376
- else {
377
- // No tag found — strip obvious prefixes and keep remainder (preserve colons in message)
378
- message = line.replace(/^.*?<.*?>\s*/, '').trim();
379
- }
380
- return { timestamp: ts, level: lvl, tag: tagVal, pid: pidNum, message };
381
- });
382
- // Apply contains filter
383
- let filtered = parsed;
384
- if (contains)
385
- filtered = filtered.filter(e => e.message && e.message.includes(contains));
386
- // Apply since_seconds already applied by log show, but double-check timestamps
387
- if (since_seconds) {
388
- const sinceMs = Date.now() - (since_seconds * 1000);
389
- filtered = filtered.filter(e => e.timestamp && (new Date(e.timestamp).getTime() >= sinceMs));
390
- }
391
- // level filter
392
- if (level) {
393
- const L = level.toUpperCase();
394
- filtered = filtered.filter(e => e.level && e.level.toUpperCase() === L);
395
- }
396
- // tag filter
397
- if (tag)
398
- filtered = filtered.filter(e => e.tag && e.tag.includes(tag));
399
- // pid filter (use detected/effective pid if available)
400
- const pidToFilter = effectivePid;
401
- if (pidToFilter)
402
- filtered = filtered.filter(e => e.pid === pidToFilter);
403
- // If appId present but no predicate returned lines, try substring match
404
- if (appId && filtered.length === 0) {
405
- const matched = parsed.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)) || (e.message && processNameUsed && e.message.includes(processNameUsed)));
406
- if (matched.length > 0)
407
- filtered = matched;
408
- }
409
- // Order oldest -> newest
410
- filtered.sort((a, b) => {
411
- const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
412
- const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
413
- return ta - tb;
414
- });
415
- const source = pidToFilter ? 'pid' : (appId ? 'process' : 'broad');
416
- const meta = { appIdProvided: !!appId, processNameUsed: processNameUsed || null, detectedPid: detectedPid || null, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid };
417
- const limited = filtered.slice(-Math.max(0, effectiveLimit));
418
- return { device, logs: limited, logCount: limited.length, source, meta };
419
- }
420
- catch (err) {
421
- console.error('iOS getLogs failed:', err);
422
- const device = await getIOSDeviceMetadata(deviceId);
423
- return { device, logs: [], logCount: 0, source: 'broad', meta: { error: err instanceof Error ? err.message : String(err) } };
424
- }
425
- }
19
+ async collectGetLogs(filters = {}) { return collectGetLogs(filters); }
426
20
  async captureScreenshot(deviceId = "booted") {
427
21
  return captureEvidence('screenshot', { platform: 'ios', id: deviceId ?? '' }, () => this.collectCaptureScreenshot(deviceId));
428
22
  }
429
- async collectCaptureScreenshot(deviceId = "booted") {
430
- const device = await getIOSDeviceMetadata(deviceId);
431
- const tmpFile = `/tmp/mcp-ios-screenshot-${randomUUID()}.png`;
432
- try {
433
- await iosExecCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId);
434
- const buffer = await fs.readFile(tmpFile);
435
- const base64 = buffer.toString('base64');
436
- const dims = parsePngSize(buffer);
437
- // Try to generate WebP (preferred) and JPEG fallback using sharp (in-process, cross-platform)
438
- try {
439
- const sharpModule = await import('sharp');
440
- const sharp = sharpModule && sharpModule.default ? sharpModule.default : sharpModule;
441
- const img = sharp(buffer);
442
- const meta = await img.metadata().catch((err) => { console.error('sharp.metadata failed:', err); return {}; });
443
- // If image has alpha channel, prefer lossless PNG to preserve transparency
444
- const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
445
- // Generate WebP and JPEG buffers; log failures
446
- let webpBuf = null;
447
- let jpegBuf = null;
448
- try {
449
- webpBuf = await img.webp({ quality: 80 }).toBuffer();
450
- }
451
- catch (err) {
452
- console.error('WebP conversion failed (iOS):', err instanceof Error ? err.message : String(err));
453
- webpBuf = null;
454
- }
455
- try {
456
- jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
457
- }
458
- catch (err) {
459
- console.error('JPEG conversion failed (iOS):', err instanceof Error ? err.message : String(err));
460
- jpegBuf = null;
461
- }
462
- await fs.rm(tmpFile).catch(() => { });
463
- if (hasAlpha) {
464
- // preserve alpha: return PNG if WebP not available
465
- if (webpBuf) {
466
- return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: base64, screenshot_fallback_mime: 'image/png', resolution: { width: dims.width, height: dims.height } };
467
- }
468
- // if webp unavailable, return original PNG
469
- return { device, screenshot: base64, screenshot_mime: 'image/png', resolution: { width: dims.width, height: dims.height } };
470
- }
471
- // No alpha: prefer webp, fall back to jpeg
472
- if (webpBuf) {
473
- return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: jpegBuf ? jpegBuf.toString('base64') : undefined, screenshot_fallback_mime: jpegBuf ? 'image/jpeg' : undefined, resolution: { width: dims.width, height: dims.height } };
474
- }
475
- if (jpegBuf) {
476
- return { device, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: dims.width, height: dims.height } };
477
- }
478
- }
479
- catch (err) {
480
- console.error('Screenshot conversion pipeline failed (iOS):', err instanceof Error ? err.message : String(err));
481
- // fall through to png fallback
482
- }
483
- await fs.rm(tmpFile).catch(() => { });
484
- return {
485
- device,
486
- screenshot: base64,
487
- screenshot_mime: 'image/png',
488
- resolution: { width: dims.width, height: dims.height },
489
- };
490
- }
491
- catch (e) {
492
- await fs.rm(tmpFile).catch(() => { });
493
- throw new Error(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`);
494
- }
495
- }
23
+ async collectCaptureScreenshot(deviceId = "booted") { return collectCaptureScreenshot(deviceId); }
496
24
  async getUITree(deviceId = "booted") {
497
25
  return captureEvidence('ui_tree', { platform: 'ios', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId));
498
26
  }
499
- async collectGetUITree(deviceId = "booted") {
500
- const measurement = createObservationMeasurement('ios');
501
- let device;
502
- try {
503
- device = await measurement.timed('metadata_ms', () => getIOSDeviceMetadata(deviceId));
504
- }
505
- catch (error) {
506
- measurement.setOutcome('unavailable');
507
- measurement.finish();
508
- throw error;
509
- }
510
- const deviceKey = `ios:${device.id}`;
511
- const idbExists = await measurement.timed('capability_check_ms', () => isIDBInstalled());
512
- if (!idbExists) {
513
- const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree');
514
- measurement.setOutcome('unavailable');
515
- measurement.finish();
516
- return {
517
- device,
518
- screen: "",
519
- resolution: { width: 0, height: 0 },
520
- elements: [],
521
- ...snapshotMetadata,
522
- error: "iOS UI tree retrieval requires 'idb' (iOS Device Bridge). Please install it via Homebrew: `brew tap facebook/fb && brew install idb-companion` and `pip3 install fb-idb`."
523
- };
524
- }
525
- const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
526
- let jsonContent = null;
527
- let attempts = 0;
528
- const maxAttempts = 3;
529
- while (attempts < maxAttempts) {
530
- remainingBudget();
531
- attempts++;
532
- try {
533
- await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
534
- remainingBudget();
535
- const args = ['ui', 'describe-all', '--json'];
536
- if (targetUdid) {
537
- args.push('--udid', targetUdid);
538
- }
539
- const output = await measurement.physicalRead(async () => {
540
- const result = await execCmd(getIdbCmd(), args, { timeout: remainingBudget() });
541
- if (result.stderr === 'ACTION_TIMEOUT')
542
- throw new Error('ACTION_TIMEOUT');
543
- if (result.exitCode !== 0)
544
- throw new Error(`idb failed (code ${result.exitCode}): ${result.stderr}`);
545
- if (!result.stdout.trim())
546
- throw new Error('Empty idb UI tree output');
547
- return result.stdout;
548
- });
549
- if (output && output.trim().length > 0) {
550
- jsonContent = await measurement.timed('parse_ms', async () => JSON.parse(output));
551
- break; // Success
552
- }
553
- }
554
- catch (e) {
555
- if (e instanceof Error && e.message === 'ACTION_TIMEOUT')
556
- throw e;
557
- console.error(`Attempt ${attempts} failed: ${e}`);
558
- }
559
- if (attempts === maxAttempts) {
560
- const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree');
561
- measurement.setOutcome('unavailable');
562
- measurement.finish();
563
- return {
564
- device,
565
- screen: "",
566
- resolution: { width: 0, height: 0 },
567
- elements: [],
568
- ...snapshotMetadata,
569
- error: `Failed to retrieve valid UI dump after ${maxAttempts} attempts.`
570
- };
571
- }
572
- }
573
- try {
574
- const elements = await measurement.timed('conversion_ms', async () => {
575
- const parsed = [];
576
- if (Array.isArray(jsonContent)) {
577
- for (const node of jsonContent)
578
- traverseIDBNode(node, parsed);
579
- }
580
- else {
581
- traverseIDBNode(jsonContent, parsed);
582
- }
583
- return parsed;
584
- });
585
- let width = 0;
586
- let height = 0;
587
- if (elements.length > 0) {
588
- const rootBounds = elements[0].bounds;
589
- width = rootBounds[2] - rootBounds[0];
590
- height = rootBounds[3] - rootBounds[1];
591
- }
592
- const snapshotMetadata = deriveSnapshotMetadata(deviceKey, {
593
- screen: "",
594
- resolution: { width, height },
595
- elements
596
- }, 'ui_tree');
597
- measurement.setOutcome('observed');
598
- const response = measurement.bindObservation({
599
- device,
600
- screen: "",
601
- resolution: { width, height },
602
- elements,
603
- ...snapshotMetadata
604
- });
605
- measurement.finish();
606
- return response;
607
- }
608
- catch (e) {
609
- const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree');
610
- measurement.setOutcome('unavailable');
611
- measurement.finish();
612
- return {
613
- device,
614
- screen: "",
615
- resolution: { width: 0, height: 0 },
616
- elements: [],
617
- ...snapshotMetadata,
618
- error: `Failed to parse idb output: ${e instanceof Error ? e.message : String(e)}`
619
- };
620
- }
621
- }
27
+ async collectGetUITree(deviceId = "booted") { return collectGetUITree(deviceId); }
622
28
  async getScreenFingerprint(deviceId = 'booted') {
623
29
  return captureEvidence('fingerprint', { platform: 'ios', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId));
624
30
  }
625
- async collectGetScreenFingerprint(deviceId = 'booted') {
626
- try {
627
- const tree = await this.getUITree(deviceId);
628
- if (!tree || tree.error)
629
- return { fingerprint: null, error: tree && tree.error };
630
- return { ...computeScreenFingerprint(tree, null, 'ios', 50), captured_at_ms: tree.captured_at_ms };
631
- }
632
- catch (e) {
633
- return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
634
- }
635
- }
636
- async startLogStream(bundleId, deviceId = 'booted', sessionId = 'default') {
637
- try {
638
- const simple = bundleId.split('.').pop() || bundleId;
639
- const predicate = `process == "${simple}" or process == "${bundleId}" or subsystem contains "${bundleId}"`;
640
- if (iosActiveLogStreams.has(sessionId)) {
641
- try {
642
- iosActiveLogStreams.get(sessionId).proc.kill();
643
- }
644
- catch { }
645
- iosActiveLogStreams.delete(sessionId);
646
- }
647
- const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate];
648
- const proc = spawn(getXcrunCmd(), args);
649
- const tmpDir = process.env.TMPDIR || '/tmp';
650
- const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`);
651
- const stream = createWriteStream(file, { flags: 'a' });
652
- proc.stdout.on('data', (chunk) => {
653
- const text = chunk.toString();
654
- const lines = text.split(/\r?\n/).filter(Boolean);
655
- for (const l of lines) {
656
- const entry = parseLogLine(l);
657
- stream.write(JSON.stringify(entry) + '\n');
658
- }
659
- });
660
- proc.stderr.on('data', (chunk) => {
661
- const text = chunk.toString();
662
- const lines = text.split(/\r?\n/).filter(Boolean);
663
- for (const l of lines) {
664
- const entry = { timestamp: '', level: 'E', tag: 'xcrun', message: l };
665
- stream.write(JSON.stringify(entry) + '\n');
666
- }
667
- });
668
- proc.on('close', () => {
669
- stream.end();
670
- if (iosActiveLogStreams.get(sessionId)?.proc === proc)
671
- iosActiveLogStreams.delete(sessionId);
672
- });
673
- iosActiveLogStreams.set(sessionId, { proc, file, device: { platform: 'ios', id: deviceId ?? '' } });
674
- return { success: true, stream_started: true };
675
- }
676
- catch {
677
- return { success: false, error: 'log_stream_start_failed' };
678
- }
679
- }
680
- async stopLogStream(sessionId = 'default') {
681
- const entry = iosActiveLogStreams.get(sessionId);
682
- if (!entry)
683
- return { success: true };
684
- try {
685
- entry.proc.kill();
686
- }
687
- catch { }
688
- iosActiveLogStreams.delete(sessionId);
689
- return { success: true };
690
- }
691
- async readLogStream(sessionId = 'default', limit = 100, since) {
692
- return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since));
693
- }
694
- async collectReadLogStream(sessionId = 'default', limit = 100, since) {
695
- const entry = iosActiveLogStreams.get(sessionId);
696
- if (!entry)
697
- return { entries: [] };
698
- try {
699
- const data = await fsPromises.readFile(entry.file, 'utf8').catch(() => '');
700
- if (!data)
701
- return { entries: [], crash_summary: { crash_detected: false } };
702
- const lines = data.split(/\r?\n/).filter(Boolean);
703
- const parsed = lines.map(l => {
704
- try {
705
- return JSON.parse(l);
706
- }
707
- catch {
708
- return { message: l, _iso: null, crash: false };
709
- }
710
- });
711
- let filtered = parsed;
712
- if (since) {
713
- let sinceMs = null;
714
- if (/^\d+$/.test(since))
715
- sinceMs = Number(since);
716
- else {
717
- const sDate = new Date(since);
718
- if (!isNaN(sDate.getTime()))
719
- sinceMs = sDate.getTime();
720
- }
721
- if (sinceMs !== null) {
722
- filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
723
- }
724
- }
725
- const entries = filtered.slice(-Math.max(0, limit));
726
- const crashEntry = entries.find(e => e.crash);
727
- const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
728
- return { entries, crash_summary };
729
- }
730
- catch {
731
- return { entries: [], crash_summary: { crash_detected: false } };
732
- }
733
- }
31
+ async collectGetScreenFingerprint(deviceId = 'booted') { return collectGetScreenFingerprint(this, deviceId); }
32
+ async startLogStream(bundleId, deviceId = 'booted', sessionId = 'default') { return startLogStream(bundleId, deviceId, sessionId); }
33
+ async stopLogStream(sessionId = 'default') { return stopLogStream(sessionId); }
34
+ async readLogStream(sessionId = 'default', limit = 100, since) { return readLogStream(sessionId, limit, since); }
35
+ async collectReadLogStream(sessionId = 'default', limit = 100, since) { return collectReadLogStream(sessionId, limit, since); }
734
36
  }