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,18 +1,13 @@
1
1
  import { captureEvidence } from './evidence.js'
2
- import { spawn } from "child_process"
3
- import { XMLParser } from "fast-xml-parser"
4
- import { GetLogsResponse, CaptureAndroidScreenResponse, GetUITreeResponse, GetCurrentScreenResponse, UIElement, DeviceInfo } from "../types.js"
5
- import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "../utils/android/utils.js"
6
- import { createWriteStream } from "fs"
7
- import { promises as fsPromises } from "fs"
8
- import path from "path"
9
- import { computeScreenFingerprint } from "../utils/ui/index.js"
10
- import { parsePngSize } from "../utils/image.js"
11
- import { deriveSnapshotMetadata } from "./snapshot-metadata.js"
12
- import { remainingBudget } from '../utils/operation-budget.js'
13
- import { createObservationMeasurement } from './performance-metrics.js'
14
-
15
- const activeLogStreams: Map<string, { proc: any, file: string, device?: { platform: 'android' | 'ios'; id: string } }> = new Map()
2
+ import { GetLogsResponse, CaptureAndroidScreenResponse, GetUITreeResponse, GetCurrentScreenResponse, DeviceInfo } from "../types.js"
3
+ import { getAndroidDeviceMetadata } from "../utils/android/utils.js"
4
+ import { activeLogStreams } from './android/context.js'
5
+ import { collectGetUITree } from './android/ui-tree.js'
6
+ import { collectGetLogs } from './android/logs.js'
7
+ import { collectCaptureScreen } from './android/screenshot.js'
8
+ import { collectGetScreenFingerprint } from './android/fingerprint.js'
9
+ import { startLogStream, stopLogStream, collectReadLogStream } from './android/log-stream.js'
10
+ import { collectGetCurrentScreen } from './android/current-screen.js'
16
11
 
17
12
  export class AndroidObserve {
18
13
  getLogStreamDevice(sessionId: string = 'default') { return activeLogStreams.get(sessionId)?.device ?? null }
@@ -25,548 +20,39 @@ export class AndroidObserve {
25
20
  return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId))
26
21
  }
27
22
 
28
- private async collectGetUITree(deviceId?: string): Promise<GetUITreeResponse> {
29
- const measurement = createObservationMeasurement('android')
30
- let metadata
31
- try {
32
- metadata = await measurement.timed('metadata_ms', () => getAndroidDeviceMetadata("", deviceId))
33
- } catch (error) {
34
- measurement.setOutcome('unavailable')
35
- measurement.finish()
36
- throw error
37
- }
38
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
39
-
40
- try {
41
- // Get screen resolution first
42
- const resolution = await measurement.timed('resolution_ms', () => getScreenResolution(deviceId));
43
- if (resolution.width === 0 && resolution.height === 0) {
44
- throw new Error("Failed to get screen resolution. Is the device connected and authorized?");
45
- }
46
-
47
- // Retry Logic
48
- let xmlContent = '';
49
- let attempts = 0;
50
- const maxAttempts = 3;
51
-
52
- while (attempts < maxAttempts) {
53
- remainingBudget()
54
- attempts++;
55
- try {
56
- // The first read starts immediately so it remains fresh for in-place UI changes.
57
- // Delay only a retry after a failed or invalid acquisition.
58
- if (attempts > 1) {
59
- await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
60
- }
61
- remainingBudget()
62
-
63
- xmlContent = await measurement.physicalRead(async () => {
64
- await execAdb(['shell', 'uiautomator', 'dump', '/sdcard/ui.xml'], deviceId)
65
- const content = await execAdb(['shell', 'cat', '/sdcard/ui.xml'], deviceId)
66
- if (!content || !content.trim() || content.includes('ERROR:')) throw new Error('Empty or invalid UI dump')
67
- return content
68
- })
69
- break
70
- } catch (e) {
71
- if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
72
- console.error(`Attempt ${attempts} failed: ${e}`);
73
- }
74
-
75
- if (attempts === maxAttempts) {
76
- throw new Error(`Failed to retrieve valid UI dump after ${maxAttempts} attempts.`);
77
- }
78
- }
79
-
80
- const parser = new XMLParser({
81
- ignoreAttributes: false,
82
- attributeNamePrefix: "@_"
83
- });
84
- const result = await measurement.timed('parse_ms', async () => parser.parse(xmlContent));
85
-
86
- const elements: UIElement[] = [];
87
-
88
- // The root is usually hierarchy -> node
89
- if (result.hierarchy && result.hierarchy.node) {
90
- // If the root is an array (unlikely for root, but good to be safe) or single object
91
- if (Array.isArray(result.hierarchy.node)) {
92
- result.hierarchy.node.forEach((n: any) => traverseNode(n, elements));
93
- } else {
94
- traverseNode(result.hierarchy.node, elements);
95
- }
96
- }
97
-
98
- const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, {
99
- screen: "",
100
- resolution,
101
- elements
102
- }, 'ui_tree')
103
-
104
- measurement.setOutcome('observed')
105
- const response = measurement.bindObservation({
106
- device: deviceInfo,
107
- screen: "",
108
- resolution,
109
- elements,
110
- ...snapshotMetadata
111
- })
112
- measurement.finish()
113
- return response
114
- } catch (e) {
115
- const errorMessage = `Failed to get UI tree. ADB Path: '${getAdbCmd()}'. Error: ${e instanceof Error ? e.message : String(e)}`;
116
- console.error(errorMessage);
117
- const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, null, 'ui_tree')
118
- measurement.setOutcome('unavailable')
119
- measurement.finish()
120
- return {
121
- device: deviceInfo,
122
- screen: "",
123
- resolution: { width: 0, height: 0 },
124
- elements: [],
125
- ...snapshotMetadata,
126
- error: errorMessage
127
- };
128
- }
129
- }
23
+ private async collectGetUITree(deviceId?: string): Promise<GetUITreeResponse> { return collectGetUITree(deviceId) }
130
24
 
131
25
  async getLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
132
26
  return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters))
133
27
  }
134
28
 
135
- private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
136
- const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters
137
- const metadata = await getAndroidDeviceMetadata(appId || "", deviceId)
138
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
139
-
140
- try {
141
- const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50
142
-
143
- // If appId provided, try to get pid to filter at source
144
- let pidArg: string | null = null
145
- if (appId) {
146
- try {
147
- const pidOut = await execAdb(['shell', 'pidof', appId], deviceId).catch(() => '')
148
- const pidTrim = (pidOut || '').trim()
149
- if (pidTrim) pidArg = pidTrim.split('\n')[0]
150
- } catch (err) {
151
- // Log a warning so failures to detect PID are visible during debugging
152
- try { console.warn(`getLogs: pid detection failed for appId=${appId}:`, err instanceof Error ? err.message : String(err)) } catch { }
153
- }
154
- }
155
-
156
- const args = ['logcat', '-d', '-v', 'threadtime']
157
- // Apply pid filter via --pid if available
158
- if (pidArg) args.push(`--pid=${pidArg}`)
159
- else if (pid) args.push(`--pid=${pid}`)
160
-
161
- // Apply tag/level filter if provided
162
- if (tag && level) {
163
- // Map verbose/debug/info/warn/error to single-letter levels
164
- const levelMap: Record<string, string> = { 'VERBOSE': 'V', 'DEBUG': 'D', 'INFO': 'I', 'WARN': 'W', 'ERROR': 'E' }
165
- const L = (levelMap[(level || '').toUpperCase()] || 'V')
166
- args.push(`${tag}:${L}`)
167
- } else {
168
- // Default: show all levels
169
- args.push('*:V')
170
- }
171
-
172
- // Use -t to limit lines (apply early)
173
- args.push('-t', effectiveLimit.toString())
174
-
175
- const stdout = await execAdb(args, deviceId)
176
- const allLines = stdout ? stdout.split(/\r?\n/).filter(Boolean) : []
177
-
178
- // Parse lines into structured entries
179
- const parsed = allLines.map(l => {
180
- const entry = parseLogLine(l)
181
- // normalize level
182
- const levelChar = (entry.level || '').toUpperCase()
183
- const levelMapChar: Record<string,string> = { 'V':'VERBOSE','D':'DEBUG','I':'INFO','W':'WARN','E':'ERROR' }
184
- const normLevel = levelMapChar[levelChar] || (entry.level && String(entry.level).toUpperCase()) || 'INFO'
185
- const iso = entry._iso || (() => {
186
- const d = new Date(entry.timestamp || '')
187
- if (!isNaN(d.getTime())) return d.toISOString()
188
- return null
189
- })()
190
- let pidNum: number | null = null
191
- if (entry.pid) pidNum = Number(entry.pid)
192
- else if (pidArg) pidNum = Number(pidArg)
193
- const pidVal = (pidNum !== null && !isNaN(pidNum)) ? pidNum : null
194
- return { timestamp: iso, level: normLevel, tag: entry.tag || '', pid: pidVal, message: entry.message || '' }
195
- })
196
-
197
- // Apply client-side filters: contains, since_seconds
198
- let filtered = parsed
199
- if (contains) filtered = filtered.filter(e => e.message && e.message.includes(contains))
200
-
201
- if (since_seconds) {
202
- const sinceMs = Date.now() - (since_seconds * 1000)
203
- filtered = filtered.filter(e => {
204
- if (!e.timestamp) return false
205
- const t = new Date(e.timestamp).getTime()
206
- return t >= sinceMs
207
- })
208
- }
209
-
210
- // If appId provided and no pidArg, try to filter by appId substring in message/tag
211
- if (appId && !pidArg) {
212
- const matched = filtered.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)))
213
- if (matched.length > 0) filtered = matched
214
- }
215
-
216
- // Ensure ordering oldest -> newest (by timestamp when available)
217
- filtered.sort((a,b) => {
218
- const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0
219
- const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0
220
- return ta - tb
221
- })
222
-
223
- const limited = filtered.slice(-Math.max(0, effectiveLimit))
224
-
225
- const source = pidArg ? 'pid' : (appId ? 'package' : 'broad')
226
- const meta = { pidArg, appIdProvided: !!appId, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid }
227
- return { device: deviceInfo, logs: limited, logCount: limited.length, source, meta }
228
- } catch (e) {
229
- console.error("Error fetching logs:", e)
230
- return { device: deviceInfo, logs: [], logCount: 0, source: 'broad', meta: { error: e instanceof Error ? e.message : String(e) } }
231
- }
232
- }
29
+ private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> { return collectGetLogs(filters) }
233
30
 
234
31
  async captureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
235
32
  return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId))
236
33
  }
237
34
 
238
- private async collectCaptureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
239
- const metadata = await getAndroidDeviceMetadata("", deviceId)
240
- const deviceInfo: DeviceInfo = getDeviceInfo(deviceId || 'default', metadata)
241
-
242
- return new Promise((resolve, reject) => {
243
- const args = deviceId ? ['-s', deviceId, 'exec-out', 'screencap', '-p'] : ['exec-out', 'screencap', '-p'];
244
- const child = spawn(getAdbCmd(), args)
245
-
246
- const chunks: Buffer[] = []
247
- let stderr = ''
248
-
249
- child.stdout.on('data', (chunk) => {
250
- chunks.push(Buffer.from(chunk))
251
- })
252
-
253
- child.stderr.on('data', (data) => {
254
- stderr += data.toString()
255
- })
256
-
257
- const timeout = setTimeout(() => {
258
- child.kill()
259
- reject(new Error(`ADB screencap timed out after 10s`))
260
- }, 10000)
261
-
262
- child.on('close', async (code) => {
263
- clearTimeout(timeout)
264
- if (code !== 0) {
265
- reject(new Error(stderr.trim() || `Screencap failed with code ${code}`))
266
- return
267
- }
268
-
269
- const screenshotBuffer = Buffer.concat(chunks)
270
- const screenshotBase64 = screenshotBuffer.toString('base64')
271
-
272
- const parsed = parsePngSize(screenshotBuffer)
273
- if (parsed.width > 0 && parsed.height > 0) {
274
- // Attempt to convert to WebP (preferred) and provide JPEG fallback (awaited to avoid race)
275
- try {
276
- const sharpModule = await import('sharp'); const sharp = sharpModule && (sharpModule as any).default ? (sharpModule as any).default : sharpModule;
277
- const buf = screenshotBuffer;
278
- const img = sharp(buf);
279
- const meta = await img.metadata().catch((err: any) => { console.error('sharp.metadata failed (Android):', err); return {} as any });
280
- const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
281
-
282
- let webpBuf: Buffer | null = null;
283
- let jpegBuf: Buffer | null = null;
284
- try {
285
- webpBuf = await img.webp({ quality: 80 }).toBuffer();
286
- } catch (err) {
287
- console.error('WebP conversion failed (Android):', err instanceof Error ? err.message : String(err));
288
- webpBuf = null;
289
- }
290
- try {
291
- jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
292
- } catch (err) {
293
- console.error('JPEG conversion failed (Android):', err instanceof Error ? err.message : String(err));
294
- jpegBuf = null;
295
- }
296
-
297
- if (hasAlpha) {
298
- if (webpBuf) {
299
- const webpB64 = webpBuf.toString('base64')
300
- const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
301
- resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
302
- return
303
- }
304
- const pngB64 = buf.toString('base64')
305
- resolve({ device: deviceInfo, screenshot: pngB64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
306
- return
307
- }
308
-
309
- if (webpBuf) {
310
- const webpB64 = webpBuf.toString('base64')
311
- const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
312
- resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
313
- return
314
- }
315
-
316
- if (jpegBuf) {
317
- resolve({ device: deviceInfo, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: parsed.width, height: parsed.height } })
318
- return
319
- }
320
-
321
- // No conversions succeeded; return original PNG
322
- resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
323
- return
324
- } catch (err) {
325
- console.error('Screenshot conversion pipeline failed (Android):', err instanceof Error ? err.message : String(err));
326
- // Conversion failed - fall back to original PNG with parsed resolution
327
- resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
328
- return
329
- }
330
- } else {
331
- // Fallback to querying wm size if parsing failed
332
- execAdb(['shell', 'wm', 'size'], deviceId)
333
- .then(sizeStdout => {
334
- let width = 0
335
- let height = 0
336
- const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/)
337
- if (match) {
338
- width = parseInt(match[1], 10)
339
- height = parseInt(match[2], 10)
340
- }
341
- resolve({
342
- device: deviceInfo,
343
- screenshot: screenshotBase64,
344
- screenshot_mime: 'image/png',
345
- resolution: { width, height }
346
- })
347
- })
348
- .catch(() => {
349
- resolve({
350
- device: deviceInfo,
351
- screenshot: screenshotBase64,
352
- screenshot_mime: 'image/png',
353
- resolution: { width: 0, height: 0 }
354
- })
355
- })
356
- }
357
- })
358
-
359
- child.on('error', (err) => {
360
- clearTimeout(timeout)
361
- reject(err)
362
- })
363
- })
364
- }
35
+ private async collectCaptureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> { return collectCaptureScreen(deviceId) }
365
36
 
366
37
  async getCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
367
38
  return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId))
368
39
  }
369
40
 
370
- private async collectGetCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
371
- const metadata = await getAndroidDeviceMetadata("", deviceId)
372
- const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
373
-
374
- try {
375
- const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 })
376
- const lines = output.split('\n');
377
- let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
378
-
379
- if (!resumedLine) {
380
- resumedLine = lines.find(line => /^\s*ResumedActivity:/.test(line));
381
- }
382
-
383
- if (!resumedLine) {
384
- return {
385
- device: deviceInfo,
386
- package: "",
387
- activity: "",
388
- shortActivity: "",
389
- error: "Could not find 'mResumedActivity' in dumpsys output"
390
- }
391
- }
392
-
393
- const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
394
-
395
- if (match) {
396
- const packageName = match[1];
397
- let activityName = match[2];
398
-
399
- if (activityName.startsWith('.')) {
400
- activityName = packageName + activityName;
401
- }
402
-
403
- const shortActivity = activityName.split('.').pop() || activityName;
404
-
405
- return {
406
- device: deviceInfo,
407
- package: packageName,
408
- activity: activityName,
409
- shortActivity: shortActivity
410
- };
411
- } else {
412
- return {
413
- device: deviceInfo,
414
- package: "",
415
- activity: "",
416
- shortActivity: "",
417
- error: `Found resumed activity line but failed to parse: '${resumedLine.trim()}'`
418
- }
419
- }
420
-
421
- } catch (e) {
422
- return {
423
- device: deviceInfo,
424
- package: "",
425
- activity: "",
426
- shortActivity: "",
427
- error: e instanceof Error ? e.message : String(e)
428
- };
429
- }
430
- }
41
+ private async collectGetCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> { return collectGetCurrentScreen(deviceId) }
431
42
 
432
43
  async getScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
433
44
  return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId))
434
45
  }
435
46
 
436
- private async collectGetScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
437
- try {
438
- const tree = await this.getUITree(deviceId)
439
- if (!tree || (tree as any).error) return { fingerprint: null, error: (tree as any).error }
440
-
441
- const current = await this.getCurrentScreen(deviceId).catch(() => null)
442
- return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms }
443
- } catch (e) {
444
- return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
445
- }
446
- }
447
-
448
- async startLogStream(packageName: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId?: string, sessionId: string = 'default') {
449
- try {
450
- const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '')
451
- const pid = (pidOutput || '').trim()
452
- if (!pid) return { success: false, error: 'app_not_running' }
453
-
454
- const levelMap: Record<string, string> = { error: '*:E', warn: '*:W', info: '*:I', debug: '*:D' }
455
- const filter = levelMap[level] || levelMap['error']
456
-
457
- if (activeLogStreams.has(sessionId)) {
458
- try { activeLogStreams.get(sessionId)!.proc.kill() } catch {}
459
- activeLogStreams.delete(sessionId)
460
- }
461
-
462
- const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter]
463
- const proc = spawn(getAdbCmd(), args)
47
+ private async collectGetScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> { return collectGetScreenFingerprint(this, deviceId) }
464
48
 
465
- const tmpDir = process.env.TMPDIR || '/tmp'
466
- const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`)
467
- const stream = createWriteStream(file, { flags: 'a' })
49
+ async startLogStream(packageName: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId?: string, sessionId: string = 'default') { return startLogStream(packageName, level, deviceId, sessionId) }
468
50
 
469
- proc.stdout.on('data', (chunk) => {
470
- const text = chunk.toString()
471
- const lines = text.split(/\r?\n/).filter(Boolean)
472
- for (const l of lines) {
473
- const entry = parseLogLine(l)
474
- stream.write(JSON.stringify(entry) + '\n')
475
- }
476
- })
477
-
478
- proc.stderr.on('data', (chunk) => {
479
- const text = chunk.toString()
480
- const lines = text.split(/\r?\n/).filter(Boolean)
481
- for (const l of lines) {
482
- const entry = { timestamp: '', level: 'E', tag: 'adb', message: l }
483
- stream.write(JSON.stringify(entry) + '\n')
484
- }
485
- })
486
-
487
- proc.on('close', () => {
488
- stream.end()
489
- if (activeLogStreams.get(sessionId)?.proc === proc) activeLogStreams.delete(sessionId)
490
- })
491
-
492
- activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } })
493
- return { success: true, stream_started: true }
494
- } catch (e) {
495
- return { success: false, error: e instanceof Error ? e.message : String(e) }
496
- }
497
- }
498
-
499
- async stopLogStream(sessionId: string = 'default') {
500
- const entry = activeLogStreams.get(sessionId)
501
- if (!entry) return { success: true }
502
- try { entry.proc.kill() } catch {}
503
- activeLogStreams.delete(sessionId)
504
- return { success: true }
505
- }
51
+ async stopLogStream(sessionId: string = 'default') { return stopLogStream(sessionId) }
506
52
 
507
53
  async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
508
54
  return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since))
509
55
  }
510
56
 
511
- private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
512
- const entry = activeLogStreams.get(sessionId)
513
- let file: string | undefined
514
- if (entry && entry.file) file = entry.file
515
- else {
516
- const tmpDir = process.env.TMPDIR || '/tmp'
517
- const candidate = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`)
518
- file = candidate
519
- }
520
-
521
- try {
522
- const data = await fsPromises.readFile(file, 'utf8').catch(() => '')
523
- if (!data) return { entries: [], crash_summary: { crash_detected: false } }
524
- const lines = data.split(/\r?\n/).filter(Boolean)
525
-
526
- const parsed = lines.map(l => {
527
- try {
528
- const obj: any = JSON.parse(l)
529
- if (typeof obj._iso === 'undefined') {
530
- let iso: string | null = null
531
- if (obj.timestamp) {
532
- const d = new Date(obj.timestamp)
533
- if (!isNaN(d.getTime())) iso = d.toISOString()
534
- }
535
- obj._iso = iso
536
- }
537
- if (typeof obj.crash === 'undefined') {
538
- const msg = (obj.message || '').toString()
539
- const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/)
540
- if (/FATAL EXCEPTION/i.test(msg) || exMatch) {
541
- obj.crash = true
542
- if (exMatch) obj.exception = exMatch[1]
543
- } else {
544
- obj.crash = false
545
- }
546
- }
547
- return obj
548
- } catch {
549
- return { message: l, _iso: null, crash: false }
550
- }
551
- })
552
-
553
- let filtered = parsed
554
- if (since) {
555
- let sinceMs: number | null = null
556
- if (/^\d+$/.test(since)) sinceMs = Number(since)
557
- else {
558
- const sDate = new Date(since)
559
- if (!isNaN(sDate.getTime())) sinceMs = sDate.getTime()
560
- }
561
- if (sinceMs !== null) filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs))
562
- }
563
-
564
- const entries = filtered.slice(-Math.max(0, limit))
565
- const crashEntry = entries.find(e => e.crash)
566
- const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false }
567
- return { entries, crash_summary }
568
- } catch {
569
- return { entries: [], crash_summary: { crash_detected: false } }
570
- }
571
- }
57
+ private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string) { return collectReadLogStream(sessionId, limit, since) }
572
58
  }