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,256 +1,16 @@
1
+ import { buildIOS } from './ios/build.js';
1
2
  import { evidenceDispatch } from '../observe/evidence.js';
2
3
  import { hasFiniteBudget, markDispatchStarted, remainingBudget, runWithBudget } from '../utils/operation-budget.js';
3
4
  import { promises as fs } from "fs";
4
- import { spawn, spawnSync } from "child_process";
5
+ import { spawn } from "child_process";
5
6
  import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js";
6
7
  import { iOSObserve } from "../observe/ios.js";
7
- import path from "path";
8
8
  export class iOSManage {
9
- async build(projectPath, optsOrVariant) {
10
- // Support legacy variant string as second arg
11
- const opts = typeof optsOrVariant === 'string' ? {} : (optsOrVariant || {});
12
- const env = { ...process.env, ...(opts.env || {}) };
13
- try {
14
- // Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
15
- async function findProject(root, maxDepth = 4) {
16
- try {
17
- const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
18
- for (const e of ents) {
19
- // .xcworkspace and .xcodeproj are directories on disk (bundles), not regular files
20
- if (e.name.endsWith('.xcworkspace'))
21
- return { dir: root, workspace: e.name };
22
- if (e.name.endsWith('.xcodeproj'))
23
- return { dir: root, proj: e.name };
24
- }
25
- }
26
- catch { }
27
- if (maxDepth <= 0)
28
- return null;
29
- try {
30
- const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
31
- for (const e of ents) {
32
- if (e.isDirectory()) {
33
- const candidate = await findProject(path.join(root, e.name), maxDepth - 1);
34
- if (candidate)
35
- return candidate;
36
- }
37
- }
38
- }
39
- catch { }
40
- return null;
41
- }
42
- // Resolve projectPath to an absolute path to avoid cwd-relative resolution issues
43
- const absProjectPath = path.resolve(projectPath);
44
- // If caller supplied explicit workspace/project, prefer those and set projectRootDir accordingly
45
- let projectRootDir = absProjectPath;
46
- let workspace = opts.workspace;
47
- let proj = opts.project;
48
- if (workspace) {
49
- // normalize workspace path and set root to its parent
50
- workspace = path.isAbsolute(workspace) ? workspace : path.join(absProjectPath, workspace);
51
- projectRootDir = path.dirname(workspace);
52
- workspace = path.basename(workspace);
53
- }
54
- else if (proj) {
55
- proj = path.isAbsolute(proj) ? proj : path.join(absProjectPath, proj);
56
- projectRootDir = path.dirname(proj);
57
- proj = path.basename(proj);
58
- }
59
- else {
60
- const projectInfo = await findProject(absProjectPath, 4);
61
- if (!projectInfo)
62
- return { error: 'No Xcode project or workspace found' };
63
- projectRootDir = projectInfo.dir || absProjectPath;
64
- workspace = projectInfo.workspace;
65
- proj = projectInfo.proj;
66
- }
67
- // Determine destination: prefer explicit option, then env var, otherwise use booted simulator UDID
68
- let destinationUDID = opts.destinationUDID || env.MCP_XCODE_DESTINATION_UDID || env.MCP_XCODE_DESTINATION || '';
69
- if (!destinationUDID) {
70
- try {
71
- const meta = await getIOSDeviceMetadata('booted');
72
- if (meta && meta.id)
73
- destinationUDID = meta.id;
74
- }
75
- catch { }
76
- }
77
- // Determine xcode command early so it can be used when detecting schemes
78
- const xcodeCmd = opts.xcodeCmd || env.XCODEBUILD_PATH || 'xcodebuild';
79
- // Determine available schemes by querying xcodebuild -list rather than guessing
80
- async function detectScheme(xcodeCmdInner, workspacePath, projectPathFull, cwd) {
81
- try {
82
- const args = workspacePath ? ['-list', '-workspace', workspacePath] : ['-list', '-project', projectPathFull];
83
- // Run xcodebuild directly to list schemes
84
- const res = spawnSync(xcodeCmdInner, args, { cwd: cwd || projectRootDir, encoding: 'utf8', timeout: 20000 });
85
- const out = res.stdout || '';
86
- const schemesMatch = out.match(/Schemes:\s*\n([\s\S]*?)(?:\n\n|$)/m);
87
- if (schemesMatch) {
88
- const block = schemesMatch[1];
89
- const schemes = block.split(/\n/).map(s => s.trim()).filter(Boolean);
90
- if (schemes.length)
91
- return schemes[0];
92
- }
93
- }
94
- catch { }
95
- return null;
96
- }
97
- // Prepare build flags and paths (support incremental builds)
98
- let buildArgs;
99
- let chosenScheme = opts.scheme || null;
100
- // Derived data and result bundle (agent-configurable)
101
- const derivedDataPath = opts.derivedDataPath || env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData');
102
- // Use unique result bundle path by default to avoid collisions
103
- const resultBundlePath = env.MCP_XCODE_RESULTBUNDLE_PATH || path.join(projectRootDir, 'build', 'xcresults', `ResultBundle-${Date.now()}-${Math.random().toString(36).slice(2)}.xcresult`);
104
- const xcodeJobs = typeof opts.buildJobs === 'number' ? opts.buildJobs : (parseInt(env.MCP_XCODE_JOBS || '', 10) || 4);
105
- const forceClean = typeof opts.forceClean === 'boolean' ? opts.forceClean : env.MCP_FORCE_CLEAN === '1';
106
- // ensure result dirs exist
107
- await fs.mkdir(path.dirname(resultBundlePath), { recursive: true }).catch(() => { });
108
- await fs.mkdir(derivedDataPath, { recursive: true }).catch(() => { });
109
- // remove any pre-existing result bundle path to avoid xcodebuild complaining
110
- await fs.rm(resultBundlePath, { recursive: true, force: true }).catch(() => { });
111
- if (workspace) {
112
- const workspacePath = path.join(projectRootDir, workspace);
113
- if (!chosenScheme)
114
- chosenScheme = await detectScheme(xcodeCmd, workspacePath, undefined, projectRootDir);
115
- const scheme = chosenScheme || workspace.replace(/\.xcworkspace$/, '');
116
- buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
117
- }
118
- else {
119
- const projectPathFull = path.join(projectRootDir, proj);
120
- if (!chosenScheme)
121
- chosenScheme = await detectScheme(xcodeCmd, undefined, projectPathFull, projectRootDir);
122
- const scheme = chosenScheme || proj.replace(/\.xcodeproj$/, '');
123
- buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
124
- }
125
- // Insert clean if explicitly requested via env or opts
126
- if (forceClean) {
127
- const idx = buildArgs.indexOf('build');
128
- if (idx >= 0)
129
- buildArgs.splice(idx, 0, 'clean');
130
- }
131
- // If we have a destination UDID, add an explicit destination to avoid xcodebuild picking an ambiguous target
132
- if (destinationUDID) {
133
- buildArgs.push('-destination', `platform=iOS Simulator,id=${destinationUDID}`);
134
- }
135
- // Add derived data and result bundle for diagnostics and faster incremental builds
136
- buildArgs.push('-derivedDataPath', derivedDataPath);
137
- buildArgs.push('-resultBundlePath', resultBundlePath);
138
- // parallelisation and jobs
139
- buildArgs.push('-parallelizeTargets');
140
- buildArgs.push('-jobs', String(xcodeJobs));
141
- // Prepare results directory for backwards-compatible logs
142
- const resultsDir = path.join(projectPath, 'build-results');
143
- // Remove any stale results to avoid xcodebuild complaining about existing result bundles
144
- await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => { });
145
- await fs.mkdir(resultsDir, { recursive: true }).catch(() => { });
146
- const XCODEBUILD_TIMEOUT = parseInt(env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
147
- const MAX_RETRIES = parseInt(env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
148
- const tries = MAX_RETRIES + 1;
149
- let lastStdout = '';
150
- let lastStderr = '';
151
- let lastErr = null;
152
- for (let attempt = 1; attempt <= tries; attempt++) {
153
- // Run xcodebuild with a watchdog
154
- const res = await new Promise((resolve) => {
155
- const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir, env });
156
- let stdout = '';
157
- let stderr = '';
158
- proc.stdout?.on('data', d => stdout += d.toString());
159
- proc.stderr?.on('data', d => stderr += d.toString());
160
- let killed = false;
161
- const to = setTimeout(() => {
162
- killed = true;
163
- try {
164
- proc.kill('SIGKILL');
165
- }
166
- catch { }
167
- }, XCODEBUILD_TIMEOUT);
168
- proc.on('close', (code) => {
169
- clearTimeout(to);
170
- resolve({ code, stdout, stderr, killedByWatchdog: killed });
171
- });
172
- proc.on('error', (err) => {
173
- clearTimeout(to);
174
- resolve({ code: null, stdout, stderr: String(err), killedByWatchdog: killed });
175
- });
176
- });
177
- lastStdout = res.stdout;
178
- lastStderr = res.stderr;
179
- if (res.code === 0) {
180
- // success — clear any previous error and stop retrying
181
- lastErr = null;
182
- break;
183
- }
184
- // record the failure for reporting
185
- lastErr = new Error(res.stderr || `xcodebuild failed with code ${res.code}`);
186
- lastErr.code = res.code;
187
- lastErr.exitCode = res.code;
188
- lastErr.killedByWatchdog = !!res.killedByWatchdog;
189
- // write logs for diagnostics (helpful whether killed or not)
190
- try {
191
- await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stdout.log`), res.stdout).catch(() => { });
192
- await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stderr.log`), res.stderr).catch(() => { });
193
- }
194
- catch { }
195
- // If killed by watchdog and there are remaining attempts, continue to retry
196
- if (res.killedByWatchdog && attempt < tries) {
197
- continue;
198
- }
199
- // no more retries or not a watchdog kill — break to report lastErr
200
- if (attempt >= tries)
201
- break;
202
- }
203
- if (lastErr) {
204
- // Include diagnostics and result bundle path when available; provide structured info useful for agents
205
- const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
206
- const envSnapshot = { PATH: env.PATH };
207
- return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: lastErr.code || null, invokedCommand, cwd: projectRootDir, envSnapshot } };
208
- }
209
- // Try to locate built .app. First search project tree, then DerivedData if necessary
210
- const built = await findAppBundle(projectPath);
211
- if (built)
212
- return { artifactPath: built };
213
- // Fallback: search DerivedData for matching product
214
- const dd = path.join(process.env.HOME || '', 'Library', 'Developer', 'Xcode', 'DerivedData');
215
- try {
216
- const entries = await fs.readdir(dd).catch(() => []);
217
- for (const e of entries) {
218
- const candidate = path.join(dd, e);
219
- const found = await findAppBundle(candidate).catch(() => undefined);
220
- if (found)
221
- return { artifactPath: found };
222
- }
223
- }
224
- catch { }
225
- return { error: 'Could not find .app after build' };
226
- }
227
- catch (e) {
228
- return { error: e instanceof Error ? e.message : String(e) };
229
- }
230
- }
9
+ async build(projectPath, optsOrVariant) { return buildIOS(projectPath, optsOrVariant); }
231
10
  async installApp(appPath, deviceId = "booted") {
232
11
  const device = await getIOSDeviceMetadata(deviceId);
233
12
  try {
234
- let toInstall = appPath;
235
- const stat = await fs.stat(appPath).catch(() => null);
236
- if (stat && stat.isDirectory()) {
237
- if (appPath.endsWith('.app')) {
238
- toInstall = appPath;
239
- }
240
- else {
241
- const found = await findAppBundle(appPath);
242
- if (found) {
243
- toInstall = found;
244
- }
245
- else {
246
- // Reuse the existing build() implementation to avoid duplicating the xcodebuild logic
247
- const buildRes = await this.build(appPath);
248
- if (buildRes.error)
249
- throw new Error(buildRes.error);
250
- toInstall = buildRes.artifactPath;
251
- }
252
- }
253
- }
13
+ const toInstall = await this.installBundle(appPath);
254
14
  try {
255
15
  const res = await execCommand(['simctl', 'install', deviceId, toInstall], deviceId);
256
16
  return { device, installed: true, output: res.output };
@@ -266,18 +26,7 @@ export class iOSManage {
266
26
  });
267
27
  if (idbExists) {
268
28
  // attempt idb install via spawn but include diagnostics
269
- await new Promise((resolve, reject) => {
270
- const proc = spawn(getIdbCmd(), ['install', toInstall, '--udid', device.id]);
271
- let stderr = '';
272
- proc.stderr.on('data', d => stderr += d.toString());
273
- proc.on('close', code => {
274
- if (code === 0)
275
- resolve();
276
- else
277
- reject(new Error(stderr || `idb install failed with code ${code}`));
278
- });
279
- proc.on('error', err => reject(err));
280
- });
29
+ await installWithIdb(toInstall, device.id);
281
30
  return { device, installed: true };
282
31
  }
283
32
  }
@@ -289,6 +38,29 @@ export class iOSManage {
289
38
  return { device, installed: false, error: e instanceof Error ? e.message : String(e) };
290
39
  }
291
40
  }
41
+ async installBundle(appPath) {
42
+ let toInstall = appPath;
43
+ const stat = await fs.stat(appPath).catch(() => null);
44
+ if (stat && stat.isDirectory()) {
45
+ if (appPath.endsWith('.app')) {
46
+ toInstall = appPath;
47
+ }
48
+ else {
49
+ const found = await findAppBundle(appPath);
50
+ if (found) {
51
+ toInstall = found;
52
+ }
53
+ else {
54
+ // Reuse the existing build() implementation to avoid duplicating the xcodebuild logic
55
+ const buildRes = await this.build(appPath);
56
+ if ('error' in buildRes)
57
+ throw new Error(buildRes.error);
58
+ toInstall = buildRes.artifactPath;
59
+ }
60
+ }
61
+ }
62
+ return toInstall;
63
+ }
292
64
  async startApp(bundleId, deviceId = "booted", observe = true) {
293
65
  const started = performance.now();
294
66
  validateBundleId(bundleId);
@@ -382,3 +154,17 @@ export class iOSManage {
382
154
  }
383
155
  }
384
156
  }
157
+ async function installWithIdb(toInstall, deviceId) {
158
+ await new Promise((resolve, reject) => {
159
+ const proc = spawn(getIdbCmd(), ['install', toInstall, '--udid', deviceId]);
160
+ let stderr = '';
161
+ proc.stderr.on('data', d => stderr += d.toString());
162
+ proc.on('close', code => {
163
+ if (code === 0)
164
+ resolve();
165
+ else
166
+ reject(new Error(stderr || `idb install failed with code ${code}`));
167
+ });
168
+ proc.on('error', err => reject(err));
169
+ });
170
+ }
@@ -0,0 +1 @@
1
+ export const activeLogStreams = new Map();
@@ -0,0 +1,55 @@
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "../../utils/android/utils.js";
2
+ export async function collectGetCurrentScreen(deviceId) {
3
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
4
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
5
+ try {
6
+ const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 });
7
+ const lines = output.split('\n');
8
+ let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
9
+ if (!resumedLine) {
10
+ resumedLine = lines.find(line => /^\s*ResumedActivity:/.test(line));
11
+ }
12
+ if (!resumedLine) {
13
+ return {
14
+ device: deviceInfo,
15
+ package: "",
16
+ activity: "",
17
+ shortActivity: "",
18
+ error: "Could not find 'mResumedActivity' in dumpsys output"
19
+ };
20
+ }
21
+ const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
22
+ if (match) {
23
+ const packageName = match[1];
24
+ let activityName = match[2];
25
+ if (activityName.startsWith('.')) {
26
+ activityName = packageName + activityName;
27
+ }
28
+ const shortActivity = activityName.split('.').pop() || activityName;
29
+ return {
30
+ device: deviceInfo,
31
+ package: packageName,
32
+ activity: activityName,
33
+ shortActivity: shortActivity
34
+ };
35
+ }
36
+ else {
37
+ return {
38
+ device: deviceInfo,
39
+ package: "",
40
+ activity: "",
41
+ shortActivity: "",
42
+ error: `Found resumed activity line but failed to parse: '${resumedLine.trim()}'`
43
+ };
44
+ }
45
+ }
46
+ catch (e) {
47
+ return {
48
+ device: deviceInfo,
49
+ package: "",
50
+ activity: "",
51
+ shortActivity: "",
52
+ error: e instanceof Error ? e.message : String(e)
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,13 @@
1
+ import { computeScreenFingerprint } from "../../utils/ui/index.js";
2
+ export async function collectGetScreenFingerprint(source, deviceId) {
3
+ try {
4
+ const tree = await source.getUITree(deviceId);
5
+ if (!tree || tree.error)
6
+ return { fingerprint: null, error: tree.error };
7
+ const current = await source.getCurrentScreen(deviceId).catch(() => null);
8
+ return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms };
9
+ }
10
+ catch (e) {
11
+ return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
12
+ }
13
+ }
@@ -0,0 +1,132 @@
1
+ import { spawn } from "child_process";
2
+ import { getAdbCmd, execAdb, parseLogLine } from "../../utils/android/utils.js";
3
+ import { createWriteStream } from "fs";
4
+ import { promises as fsPromises } from "fs";
5
+ import path from "path";
6
+ import { activeLogStreams } from './context.js';
7
+ export async function startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') {
8
+ try {
9
+ const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '');
10
+ const pid = (pidOutput || '').trim();
11
+ if (!pid)
12
+ return { success: false, error: 'app_not_running' };
13
+ const levelMap = { error: '*:E', warn: '*:W', info: '*:I', debug: '*:D' };
14
+ const filter = levelMap[level] || levelMap['error'];
15
+ if (activeLogStreams.has(sessionId)) {
16
+ try {
17
+ activeLogStreams.get(sessionId).proc.kill();
18
+ }
19
+ catch { }
20
+ activeLogStreams.delete(sessionId);
21
+ }
22
+ const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter];
23
+ const proc = spawn(getAdbCmd(), args);
24
+ const tmpDir = process.env.TMPDIR || '/tmp';
25
+ const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
26
+ const stream = createWriteStream(file, { flags: 'a' });
27
+ proc.stdout.on('data', (chunk) => {
28
+ const text = chunk.toString();
29
+ const lines = text.split(/\r?\n/).filter(Boolean);
30
+ for (const l of lines) {
31
+ const entry = parseLogLine(l);
32
+ stream.write(JSON.stringify(entry) + '\n');
33
+ }
34
+ });
35
+ proc.stderr.on('data', (chunk) => {
36
+ const text = chunk.toString();
37
+ const lines = text.split(/\r?\n/).filter(Boolean);
38
+ for (const l of lines) {
39
+ const entry = { timestamp: '', level: 'E', tag: 'adb', message: l };
40
+ stream.write(JSON.stringify(entry) + '\n');
41
+ }
42
+ });
43
+ proc.on('close', () => {
44
+ stream.end();
45
+ if (activeLogStreams.get(sessionId)?.proc === proc)
46
+ activeLogStreams.delete(sessionId);
47
+ });
48
+ activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } });
49
+ return { success: true, stream_started: true };
50
+ }
51
+ catch (e) {
52
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
53
+ }
54
+ }
55
+ export async function stopLogStream(sessionId = 'default') {
56
+ const entry = activeLogStreams.get(sessionId);
57
+ if (!entry)
58
+ return { success: true };
59
+ try {
60
+ entry.proc.kill();
61
+ }
62
+ catch { }
63
+ activeLogStreams.delete(sessionId);
64
+ return { success: true };
65
+ }
66
+ export async function collectReadLogStream(sessionId = 'default', limit = 100, since) {
67
+ const entry = activeLogStreams.get(sessionId);
68
+ let file;
69
+ if (entry && entry.file)
70
+ file = entry.file;
71
+ else {
72
+ const tmpDir = process.env.TMPDIR || '/tmp';
73
+ const candidate = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
74
+ file = candidate;
75
+ }
76
+ try {
77
+ const data = await fsPromises.readFile(file, 'utf8').catch(() => '');
78
+ if (!data)
79
+ return { entries: [], crash_summary: { crash_detected: false } };
80
+ const lines = data.split(/\r?\n/).filter(Boolean);
81
+ const parsed = lines.map(l => {
82
+ try {
83
+ const obj = JSON.parse(l);
84
+ if (typeof obj._iso === 'undefined') {
85
+ let iso = null;
86
+ if (obj.timestamp) {
87
+ const d = new Date(obj.timestamp);
88
+ if (!isNaN(d.getTime()))
89
+ iso = d.toISOString();
90
+ }
91
+ obj._iso = iso;
92
+ }
93
+ if (typeof obj.crash === 'undefined') {
94
+ const msg = (obj.message || '').toString();
95
+ const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/);
96
+ if (/FATAL EXCEPTION/i.test(msg) || exMatch) {
97
+ obj.crash = true;
98
+ if (exMatch)
99
+ obj.exception = exMatch[1];
100
+ }
101
+ else {
102
+ obj.crash = false;
103
+ }
104
+ }
105
+ return obj;
106
+ }
107
+ catch {
108
+ return { message: l, _iso: null, crash: false };
109
+ }
110
+ });
111
+ let filtered = parsed;
112
+ if (since) {
113
+ let sinceMs = null;
114
+ if (/^\d+$/.test(since))
115
+ sinceMs = Number(since);
116
+ else {
117
+ const sDate = new Date(since);
118
+ if (!isNaN(sDate.getTime()))
119
+ sinceMs = sDate.getTime();
120
+ }
121
+ if (sinceMs !== null)
122
+ filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
123
+ }
124
+ const entries = filtered.slice(-Math.max(0, limit));
125
+ const crashEntry = entries.find(e => e.crash);
126
+ const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
127
+ return { entries, crash_summary };
128
+ }
129
+ catch {
130
+ return { entries: [], crash_summary: { crash_detected: false } };
131
+ }
132
+ }
@@ -0,0 +1,107 @@
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo, parseLogLine } from "../../utils/android/utils.js";
2
+ export async function collectGetLogs(filters = {}) {
3
+ const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters;
4
+ const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
5
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
6
+ try {
7
+ const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50;
8
+ const { pidArg, args } = await logcatArguments(filters, effectiveLimit);
9
+ const stdout = await execAdb(args, deviceId);
10
+ const allLines = stdout ? stdout.split(/\r?\n/).filter(Boolean) : [];
11
+ // Parse lines into structured entries
12
+ const parsed = allLines.map(line => parseAndroidLog(line, pidArg));
13
+ // Apply client-side filters: contains, since_seconds
14
+ let filtered = parsed;
15
+ if (contains)
16
+ filtered = filtered.filter(e => e.message && e.message.includes(contains));
17
+ if (since_seconds) {
18
+ const sinceMs = Date.now() - (since_seconds * 1000);
19
+ filtered = filtered.filter(e => {
20
+ if (!e.timestamp)
21
+ return false;
22
+ const t = new Date(e.timestamp).getTime();
23
+ return t >= sinceMs;
24
+ });
25
+ }
26
+ // If appId provided and no pidArg, try to filter by appId substring in message/tag
27
+ if (appId && !pidArg) {
28
+ const matched = filtered.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)));
29
+ if (matched.length > 0)
30
+ filtered = matched;
31
+ }
32
+ // Ensure ordering oldest -> newest (by timestamp when available)
33
+ filtered.sort((a, b) => {
34
+ const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
35
+ const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
36
+ return ta - tb;
37
+ });
38
+ const limited = filtered.slice(-Math.max(0, effectiveLimit));
39
+ const source = pidArg ? 'pid' : (appId ? 'package' : 'broad');
40
+ const meta = { pidArg, appIdProvided: !!appId, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid };
41
+ return { device: deviceInfo, logs: limited, logCount: limited.length, source, meta };
42
+ }
43
+ catch (e) {
44
+ console.error("Error fetching logs:", e);
45
+ return { device: deviceInfo, logs: [], logCount: 0, source: 'broad', meta: { error: e instanceof Error ? e.message : String(e) } };
46
+ }
47
+ }
48
+ function parseAndroidLog(l, pidArg) {
49
+ const entry = parseLogLine(l);
50
+ // normalize level
51
+ const levelChar = (entry.level || '').toUpperCase();
52
+ const levelMapChar = { 'V': 'VERBOSE', 'D': 'DEBUG', 'I': 'INFO', 'W': 'WARN', 'E': 'ERROR' };
53
+ const normLevel = levelMapChar[levelChar] || (entry.level && String(entry.level).toUpperCase()) || 'INFO';
54
+ const iso = entry._iso || (() => {
55
+ const d = new Date(entry.timestamp || '');
56
+ if (!isNaN(d.getTime()))
57
+ return d.toISOString();
58
+ return null;
59
+ })();
60
+ let pidNum = null;
61
+ if (entry.pid)
62
+ pidNum = Number(entry.pid);
63
+ else if (pidArg)
64
+ pidNum = Number(pidArg);
65
+ const pidVal = (pidNum !== null && !isNaN(pidNum)) ? pidNum : null;
66
+ return { timestamp: iso, level: normLevel, tag: entry.tag || '', pid: pidVal, message: entry.message || '' };
67
+ }
68
+ async function logcatArguments(filters, effectiveLimit) {
69
+ const { appId, deviceId, pid, tag, level } = filters;
70
+ // If appId provided, try to get pid to filter at source
71
+ let pidArg = null;
72
+ if (appId) {
73
+ try {
74
+ const pidOut = await execAdb(['shell', 'pidof', appId], deviceId).catch(() => '');
75
+ const pidTrim = (pidOut || '').trim();
76
+ if (pidTrim)
77
+ pidArg = pidTrim.split('\n')[0];
78
+ }
79
+ catch (err) {
80
+ // Log a warning so failures to detect PID are visible during debugging
81
+ try {
82
+ console.warn(`getLogs: pid detection failed for appId=${appId}:`, err instanceof Error ? err.message : String(err));
83
+ }
84
+ catch { }
85
+ }
86
+ }
87
+ const args = ['logcat', '-d', '-v', 'threadtime'];
88
+ // Apply pid filter via --pid if available
89
+ if (pidArg)
90
+ args.push(`--pid=${pidArg}`);
91
+ else if (pid)
92
+ args.push(`--pid=${pid}`);
93
+ // Apply tag/level filter if provided
94
+ if (tag && level) {
95
+ // Map verbose/debug/info/warn/error to single-letter levels
96
+ const levelMap = { 'VERBOSE': 'V', 'DEBUG': 'D', 'INFO': 'I', 'WARN': 'W', 'ERROR': 'E' };
97
+ const L = (levelMap[(level || '').toUpperCase()] || 'V');
98
+ args.push(`${tag}:${L}`);
99
+ }
100
+ else {
101
+ // Default: show all levels
102
+ args.push('*:V');
103
+ }
104
+ // Use -t to limit lines (apply early)
105
+ args.push('-t', effectiveLimit.toString());
106
+ return { pidArg, args };
107
+ }