mobile-debug-mcp 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,143 @@
1
+ import { remainingBudget } from '../../utils/operation-budget.js'
2
+ import { execCmd } from '../../utils/exec.js'
3
+ import { createObservationMeasurement } from '../performance-metrics.js'
4
+ import { GetUITreeResponse, UIElement } from "../../types.js"
5
+ import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../../utils/ios/utils.js"
6
+ import { deriveSnapshotMetadata } from '../snapshot-metadata.js'
7
+ import { delay } from './context.js'
8
+ import { traverseIDBNode } from './ui-elements.js'
9
+ export async function collectGetUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> {
10
+ const measurement = createObservationMeasurement('ios')
11
+ let device
12
+ try {
13
+ device = await measurement.timed('metadata_ms', () => getIOSDeviceMetadata(deviceId))
14
+ } catch (error) {
15
+ measurement.setOutcome('unavailable')
16
+ measurement.finish()
17
+ throw error
18
+ }
19
+ const deviceKey = `ios:${device.id}`
20
+
21
+ const idbExists = await measurement.timed('capability_check_ms', () => isIDBInstalled());
22
+ if (!idbExists) {
23
+ const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
24
+ measurement.setOutcome('unavailable')
25
+ measurement.finish()
26
+ return missingIDBResponse(device, snapshotMetadata)
27
+ }
28
+
29
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
30
+
31
+ const acquisition = await readIOSTree(targetUdid, measurement)
32
+ if (!acquisition.success) {
33
+ const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
34
+ measurement.setOutcome('unavailable'); measurement.finish()
35
+ return {
36
+ device, screen: '', resolution: { width: 0, height: 0 }, elements: [], ...snapshotMetadata,
37
+ error: 'Failed to retrieve valid UI dump after 3 attempts.'
38
+ }
39
+ }
40
+
41
+ const jsonContent = acquisition.tree
42
+ try {
43
+ const elements: UIElement[] = await measurement.timed('conversion_ms', async () => {
44
+ const parsed: UIElement[] = []
45
+ if (Array.isArray(jsonContent)) {
46
+ for (const node of jsonContent) traverseIDBNode(node, parsed)
47
+ } else {
48
+ traverseIDBNode(jsonContent!, parsed)
49
+ }
50
+ return parsed
51
+ })
52
+
53
+ let width = 0;
54
+ let height = 0;
55
+ if (elements.length > 0) {
56
+ const rootBounds = elements[0].bounds;
57
+ width = rootBounds[2] - rootBounds[0];
58
+ height = rootBounds[3] - rootBounds[1];
59
+ }
60
+
61
+ const snapshotMetadata = deriveSnapshotMetadata(deviceKey, {
62
+ screen: "",
63
+ resolution: { width, height },
64
+ elements
65
+ }, 'ui_tree')
66
+
67
+ measurement.setOutcome('observed')
68
+ const response = measurement.bindObservation({
69
+ device,
70
+ screen: "",
71
+ resolution: { width, height },
72
+ elements,
73
+ ...snapshotMetadata
74
+ })
75
+ measurement.finish()
76
+ return response
77
+ } catch (e) {
78
+ const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
79
+ measurement.setOutcome('unavailable')
80
+ measurement.finish()
81
+ return {
82
+ device,
83
+ screen: "",
84
+ resolution: { width: 0, height: 0 },
85
+ elements: [],
86
+ ...snapshotMetadata,
87
+ error: `Failed to parse idb output: ${e instanceof Error ? e.message : String(e)}`
88
+ };
89
+ }
90
+ }
91
+
92
+ async function readIOSTree(targetUdid: string | undefined, measurement: ReturnType<typeof createObservationMeasurement>) {
93
+ let jsonContent: import('./ui-elements.js').IDBElement | import('./ui-elements.js').IDBElement[] | null = null;
94
+ let attempts = 0;
95
+ const maxAttempts = 3;
96
+
97
+ while (attempts < maxAttempts) {
98
+ remainingBudget()
99
+ attempts++;
100
+ try {
101
+ await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
102
+ remainingBudget()
103
+
104
+ const args = ['ui', 'describe-all', '--json'];
105
+ if (targetUdid) {
106
+ args.push('--udid', targetUdid);
107
+ }
108
+
109
+ const output = await measurement.physicalRead(async () => {
110
+ const result = await execCmd(getIdbCmd(), args, { timeout: remainingBudget() })
111
+ if (result.stderr === 'ACTION_TIMEOUT') throw new Error('ACTION_TIMEOUT')
112
+ if (result.exitCode !== 0) throw new Error(`idb failed (code ${result.exitCode}): ${result.stderr}`)
113
+ if (!result.stdout.trim()) throw new Error('Empty idb UI tree output')
114
+ return result.stdout
115
+ })
116
+
117
+ if (output && output.trim().length > 0) {
118
+ jsonContent = await measurement.timed('parse_ms', async () => JSON.parse(output));
119
+ break; // Success
120
+ }
121
+ } catch (e) {
122
+ if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
123
+ console.error(`Attempt ${attempts} failed: ${e}`);
124
+ }
125
+
126
+ if (attempts === maxAttempts) {
127
+ return { success: false, tree: null };
128
+ }
129
+ }
130
+
131
+ return { success: true, tree: jsonContent }
132
+ }
133
+
134
+ function missingIDBResponse(device: GetUITreeResponse['device'], snapshotMetadata: ReturnType<typeof deriveSnapshotMetadata>): GetUITreeResponse {
135
+ return {
136
+ device,
137
+ screen: "",
138
+ resolution: { width: 0, height: 0 },
139
+ elements: [],
140
+ ...snapshotMetadata,
141
+ error: "iOS UI tree retrieval requires 'idb' (iOS Device Bridge). Please install it via Homebrew: `brew tap facebook/fb && brew install idb-companion` and `pip3 install fb-idb`."
142
+ };
143
+ }
@@ -0,0 +1,55 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { XMLParser, XMLValidator } from 'fast-xml-parser'
4
+ import { getXcrunCmd } from '../../utils/ios/utils.js'
5
+ import { traceCommand, spawnTrace, type TraceSpawn } from './process.js'
6
+ import type { IosApp } from './capabilities.js'
7
+
8
+ export const IOS_TRACE_MAX_BYTES = 256 * 1024 * 1024
9
+ export async function traceSize(root: string, secure = false): Promise<number> {
10
+ const stat = await fs.lstat(root)
11
+ if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory())) throw new Error('UNSAFE_ARTIFACT')
12
+ if (secure) await fs.chmod(root, stat.isDirectory() ? 0o700 : 0o600)
13
+ if (stat.isFile()) return stat.size
14
+ let size = 0
15
+ for (const name of await fs.readdir(root)) {
16
+ size += await traceSize(path.join(root, name), secure)
17
+ if (size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
18
+ }
19
+ return size
20
+ }
21
+ interface TocProcess { '@_pid'?: string; '@_path'?: string }
22
+ interface TocRun {
23
+ processes?: { process?: TocProcess | TocProcess[] }
24
+ info?: { target?: { device?: { '@_uuid'?: string }; process?: { '@_pid'?: string; '@_name'?: string } }; summary?: { 'template-name'?: string } }
25
+ data?: { table?: { '@_schema'?: string }[] | { '@_schema'?: string } }
26
+ }
27
+ export function validateToc(xml: string, device: string, app: IosApp) {
28
+ if (XMLValidator.validate(xml) !== true) throw new Error('INVALID_TOC')
29
+ const parsed = new XMLParser({ ignoreAttributes: false }).parse(xml) as { 'trace-toc'?: { run?: TocRun | TocRun[] } }
30
+ const raw = parsed['trace-toc']?.run
31
+ if (!raw || Array.isArray(raw)) throw new Error('INVALID_TOC')
32
+ const target = raw.info?.target
33
+ if (target?.device?.['@_uuid'] !== device || Number(target.process?.['@_pid']) !== app.pid || raw.info?.summary?.['template-name'] !== 'Time Profiler') throw new Error('ARTIFACT_TARGET_MISMATCH')
34
+ const processes = raw.processes?.process
35
+ if (!(Array.isArray(processes) ? processes : [processes]).some(p => Number(p?.['@_pid']) === app.pid && p?.['@_path'] === app.executable)) throw new Error('ARTIFACT_APP_MISMATCH')
36
+ const tables = raw.data?.table
37
+ if (!(Array.isArray(tables) ? tables : [tables]).some(t => t?.['@_schema'] === 'time-sample')) throw new Error('SAMPLE_SCHEMA_MISSING')
38
+ }
39
+ export async function validateArtifact(file: string, device: string, app: IosApp, spawn: TraceSpawn = spawnTrace) {
40
+ const size = await traceSize(file, true)
41
+ if (size <= 0 || size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
42
+ const toc = path.join(path.dirname(file), 'toc.xml'), samples = path.join(path.dirname(file), 'samples.xml')
43
+ try {
44
+ await traceCommand(getXcrunCmd(), ['xctrace', 'export', '--input', file, '--toc', '--output', toc], 15000, spawn)
45
+ if ((await fs.stat(toc)).size > 1024 * 1024) throw new Error('INVALID_TOC')
46
+ validateToc(await fs.readFile(toc, 'utf8'), device, app)
47
+ await traceCommand(getXcrunCmd(), ['xctrace', 'export', '--input', file, '--xpath', '/trace-toc/run[@number="1"]/data/table[@schema="time-sample"]', '--output', samples], 15000, spawn)
48
+ if ((await fs.stat(samples)).size > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT')
49
+ const xml = await fs.readFile(samples, 'utf8')
50
+ if (XMLValidator.validate(xml) !== true || !xml.includes('<trace-query-result>')) throw new Error('INVALID_SAMPLES')
51
+ const sample_rows = (xml.match(/<row(?:\s|>)/g) ?? []).length
52
+ return { path: file, format: 'instruments' as const, kind: (await fs.stat(file)).isDirectory() ? 'directory' as const : 'file' as const,
53
+ size_bytes: size, validation: { readable: true, schema: 'time-sample', sample_rows } }
54
+ } finally { await Promise.all([toc, samples].map(p => fs.rm(p, { force: true }))) }
55
+ }
@@ -0,0 +1,74 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { getXcrunCmd } from '../../utils/ios/utils.js'
5
+ import { traceCommand, type TraceSpawn, spawnTrace } from './process.js'
6
+ import { prepareNotification } from './notification.js'
7
+
8
+ export const IOS_TEMPLATES = ['System Trace', 'Time Profiler', 'Animation Hitches', 'Hangs'] as const
9
+ export type IosTemplate = typeof IOS_TEMPLATES[number]
10
+ export interface IosTarget { id: string; platform: 'ios'; simulator: boolean; osVersion: string; model: string }
11
+ export interface IosApp { app_id: string; pid: number; executable: string; started_at: string }
12
+ export interface IosCapabilities {
13
+ supported: boolean; backend: 'xctrace'; device: IosTarget | null; xcode_version: string | null
14
+ prerequisites: Record<string, 'available' | 'missing' | 'unknown'>; reasons: string[]; recovery: string[]
15
+ discovered_templates: string[]; templates: { name: IosTemplate; supported: boolean; coverage: string; reason: string | null }[]
16
+ app: IosApp | null; limitations: string[]
17
+ }
18
+ interface Simulators { devices: Record<string, { udid: string; state: string; name: string; isAvailable: boolean }[]> }
19
+ export async function resolveIosApp(device: string, appId: string, spawn: TraceSpawn = spawnTrace): Promise<IosApp> {
20
+ const run = (args: string[]) => traceCommand(getXcrunCmd(), args, 10000, spawn)
21
+ const container = await run(['simctl', 'get_app_container', device, appId, 'app'])
22
+ if (!path.isAbsolute(container) || !container.endsWith('.app')) throw new Error('APP_IDENTITY_UNAVAILABLE')
23
+ const output = await run(['simctl', 'spawn', device, 'launchctl', 'list'])
24
+ const ids = output.split('\n').filter(line => line.includes(`UIKitApplication:${appId}[`))
25
+ .map(line => Number(line.trim().split(/\s+/)[0])).filter(pid => Number.isSafeInteger(pid) && pid > 1)
26
+ if (ids.length !== 1) throw new Error('APP_NOT_RUNNING')
27
+ const executable = await traceCommand('/bin/ps', ['-p', String(ids[0]), '-o', 'comm='], 10000, spawn)
28
+ if (!executable.startsWith(container + '/') || executable.includes('\n')) throw new Error('APP_IDENTITY_UNAVAILABLE')
29
+ const started_at = await traceCommand('/bin/ps', ['-p', String(ids[0]), '-o', 'lstart='], 10000, spawn)
30
+ if (!started_at || started_at.includes('\n')) throw new Error('APP_IDENTITY_UNAVAILABLE')
31
+ return { app_id: appId, pid: ids[0], executable, started_at }
32
+ }
33
+ export async function iosCapabilities(device: string, appId?: string, template: IosTemplate = 'Time Profiler', spawn: TraceSpawn = spawnTrace, host: string = process.platform): Promise<IosCapabilities> {
34
+ const result: IosCapabilities = {
35
+ supported: false, backend: 'xctrace', device: null, xcode_version: null,
36
+ prerequisites: { host: host === 'darwin' ? 'available' : 'missing', permissions: 'unknown', symbols: 'unknown' },
37
+ reasons: [], recovery: [], discovered_templates: [], templates: [], app: null,
38
+ limitations: ['App attachment may include sensitive system metadata, paths, logs and signposts.', 'Permissions and symbolication are not established by discovery.', 'Physical iOS capture is not supported in the initial matrix.']
39
+ }
40
+ if (host !== 'darwin') { result.reasons.push('MACOS_REQUIRED'); return result }
41
+ const probe = async (key: string, fn: () => Promise<void>, recovery: string) => {
42
+ try { await fn(); result.prerequisites[key] = 'available' }
43
+ catch (error) { result.prerequisites[key] = 'missing'; result.reasons.push(error instanceof Error && ['APP_NOT_RUNNING', 'APP_IDENTITY_UNAVAILABLE'].includes(error.message) ? error.message : key.toUpperCase() + '_UNAVAILABLE'); result.recovery.push(recovery) }
44
+ }
45
+ await probe('xcode', async () => { result.xcode_version = await traceCommand(getXcrunCmd(), ['xcodebuild', '-version'], 10000, spawn) }, 'Select a full Xcode installation with xcode-select.')
46
+ await probe('templates', async () => {
47
+ const output = await traceCommand(getXcrunCmd(), ['xctrace', 'list', 'templates'], 10000, spawn)
48
+ result.discovered_templates = output.split('\n').map(s => s.trim()).filter(s => s && !s.startsWith('=='))
49
+ }, 'Install/select Xcode with Instruments and run xcrun xctrace list templates.')
50
+ await probe('instruments_targets', async () => {
51
+ const targets = await traceCommand(getXcrunCmd(), ['xctrace', 'list', 'devices'], 15000, spawn)
52
+ if (!targets.includes(`(${device})`)) throw new Error('TARGET_UNAVAILABLE')
53
+ }, 'Check xcrun xctrace list devices; use a simulator visible to Instruments.')
54
+ await probe('target', async () => {
55
+ const data = JSON.parse(await traceCommand(getXcrunCmd(), ['simctl', 'list', 'devices', '--json'], 10000, spawn)) as Simulators
56
+ const entries = Object.entries(data.devices).flatMap(([runtime, values]) => values.map(value => ({ ...value, runtime })))
57
+ const target = entries.find(d => d.udid === device)
58
+ if (!target || !target.isAvailable || target.state !== 'Booted') throw new Error('TARGET_UNAVAILABLE')
59
+ result.device = { platform: 'ios', id: target.udid, simulator: true, model: target.name, osVersion: target.runtime.split('iOS-')[1]?.replaceAll('-', '.') ?? target.runtime }
60
+ }, 'Use an explicit booted simulator UDID. Physical devices and the Mac host are unsupported.')
61
+ await probe('notification_helper', async () => {
62
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-trace-probe-'))
63
+ try { await prepareNotification(directory, spawn) } finally { await fs.rm(directory, { recursive: true, force: true }) }
64
+ }, 'Allow Xcode clang to compile the local Darwin-notification listener.')
65
+ if (appId && result.device) await probe('app', async () => { result.app = await resolveIosApp(device, appId, spawn) }, 'Start the requested development app on the selected simulator before tracing.')
66
+ result.templates = IOS_TEMPLATES.map(name => ({ name,
67
+ supported: name === 'Time Profiler' && result.discovered_templates.includes(name) && result.device !== null,
68
+ coverage: name === 'Time Profiler' ? 'CPU samples; bundled Hangs instrument is not independently verified' : name === 'System Trace' ? 'Scheduling coverage unverified' : name === 'Hangs' ? 'Not a standalone template here' : 'Unsupported on tested simulator',
69
+ reason: name === 'Time Profiler' ? null : 'TEMPLATE_TARGET_UNSUPPORTED'
70
+ }))
71
+ if (!result.templates.find(t => t.name === template)?.supported) result.reasons.push('TEMPLATE_TARGET_UNSUPPORTED')
72
+ result.supported = result.reasons.length === 0
73
+ return result
74
+ }
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import { randomUUID } from 'node:crypto'
3
+ import path from 'node:path'
4
+ import { getXcrunCmd } from '../../utils/ios/utils.js'
5
+ import { iosCapabilities, resolveIosApp, type IosApp, type IosTemplate } from './capabilities.js'
6
+ import { prepareNotification, listenNotification } from './notification.js'
7
+ import { spawnTrace, stopTraceChild, classifyTraceFailure, type TraceSpawn, type OwnedTraceChild } from './process.js'
8
+ import { traceSize, validateArtifact, IOS_TRACE_MAX_BYTES } from './artifact.js'
9
+
10
+ export interface IosRecording { recorder: OwnedTraceChild; file: string; ready_at_ms: number }
11
+ export class IosTraceNative {
12
+ private spawn: TraceSpawn
13
+ private children = new Set<OwnedTraceChild>()
14
+ private recorders = new Set<OwnedTraceChild>()
15
+ private closing = false
16
+ constructor(spawn: TraceSpawn = spawnTrace) {
17
+ this.spawn = (command, args, timeout) => {
18
+ if (this.closing) throw new Error('TRACE_BUSY')
19
+ const child = spawn(command, args, timeout)
20
+ this.children.add(child)
21
+ if (args.includes('xctrace') && args.includes('record')) this.recorders.add(child)
22
+ void child.done.then(() => { this.children.delete(child); this.recorders.delete(child) })
23
+ return child
24
+ }
25
+ }
26
+ shutdown() {
27
+ this.closing = true
28
+ for (const child of this.children) child.signal('SIGINT')
29
+ const helpers = setTimeout(() => { for (const child of this.children) if (!this.recorders.has(child)) child.signal('SIGKILL') }, 1000)
30
+ const recorders = setTimeout(() => { for (const child of this.recorders) child.signal('SIGKILL') }, 8000)
31
+ helpers.unref(); recorders.unref()
32
+ }
33
+ capabilities(device: string, appId?: string, template?: IosTemplate) { return iosCapabilities(device, appId, template, this.spawn) }
34
+ resolve(device: string, app: string) { return resolveIosApp(device, app, this.spawn) }
35
+ async storage(directory: string) {
36
+ const stat = await fs.statfs(directory)
37
+ if (stat.bavail * stat.bsize < 1024 * 1024 * 1024) throw new Error('STORAGE_LIMIT')
38
+ }
39
+ async start(device: string, app: IosApp, directory: string, duration: number, own: (recording: IosRecording) => void): Promise<IosRecording> {
40
+ await this.storage(directory)
41
+ const binary = await prepareNotification(directory, this.spawn)
42
+ return this.startRecorder(device, app, directory, duration, binary, own)
43
+ }
44
+ private async startRecorder(device: string, app: IosApp, directory: string, duration: number, binary: string, own: (recording: IosRecording) => void) {
45
+ const name = 'tools.mobiledebug.trace.' + randomUUID()
46
+ const listener = await listenNotification(binary, name, this.spawn)
47
+ let recording: IosRecording | undefined
48
+ try {
49
+ const current = await this.resolve(device, app.app_id)
50
+ if (current.pid !== app.pid || current.executable !== app.executable || current.started_at !== app.started_at) throw new Error('APP_IDENTITY_CHANGED')
51
+ const file = path.join(directory, 'capture.trace')
52
+ const recorder = this.spawn(getXcrunCmd(), ['xctrace', 'record', '--template', 'Time Profiler', '--device', device,
53
+ '--attach', String(app.pid), '--time-limit', `${duration}ms`, '--output', file, '--notify-tracing-started', name], duration + 65000)
54
+ recording = { recorder, file, ready_at_ms: 0 }; own(recording)
55
+ await Promise.race([listener.line('READY', 35000), recorder.done.then(result => { throw new Error(classifyTraceFailure(result)) })])
56
+ if (!recorder.alive()) throw new Error('RECORDER_EXITED')
57
+ recording.ready_at_ms = Date.now()
58
+ return recording
59
+ } catch (error) {
60
+ if (recording) await stopTraceChild(recording.recorder).catch(() => {})
61
+ throw error
62
+ } finally { await stopTraceChild(listener, 1000).catch(() => {}) }
63
+ }
64
+ stop(recording: IosRecording, timeout?: number) { return stopTraceChild(recording.recorder, timeout) }
65
+ async monitor(device: string, app: IosApp, recording: IosRecording) {
66
+ const current = await this.resolve(device, app.app_id)
67
+ if (current.pid !== app.pid || current.executable !== app.executable || current.started_at !== app.started_at) throw new Error('APP_IDENTITY_CHANGED')
68
+ try { if (await traceSize(recording.file) > IOS_TRACE_MAX_BYTES) throw new Error('STORAGE_LIMIT') }
69
+ catch (error) { if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error }
70
+ }
71
+ validate(recording: IosRecording, device: string, app: IosApp) { return validateArtifact(recording.file, device, app, this.spawn) }
72
+ }
@@ -0,0 +1,30 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getXcrunCmd } from '../../utils/ios/utils.js'
4
+ import { traceCommand, spawnTrace, stopTraceChild, type TraceSpawn, type OwnedTraceChild } from './process.js'
5
+
6
+ // A private per-session listener avoids requiring a third-party notification utility.
7
+ const source = `#include <notify.h>
8
+ #include <dispatch/dispatch.h>
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ int main(int argc, char **argv) {
12
+ if (argc != 2) return 2;
13
+ int token;
14
+ if (notify_register_dispatch(argv[1], &token, dispatch_get_main_queue(), ^(int t) {
15
+ puts("READY"); fflush(stdout); notify_cancel(t); exit(0);
16
+ }) != NOTIFY_STATUS_OK) return 3;
17
+ puts("ARMED"); fflush(stdout); dispatch_main();
18
+ }`
19
+ export async function prepareNotification(directory: string, spawn: TraceSpawn = spawnTrace) {
20
+ const file = path.join(directory, 'notification.c'), binary = path.join(directory, 'notification')
21
+ await fs.writeFile(file, source, { mode: 0o600 })
22
+ await traceCommand(getXcrunCmd(), ['clang', '-fblocks', file, '-o', binary], 30000, spawn)
23
+ await fs.chmod(binary, 0o700)
24
+ return binary
25
+ }
26
+ export async function listenNotification(binary: string, name: string, spawn: TraceSpawn = spawnTrace): Promise<OwnedTraceChild> {
27
+ const listener = spawn(binary, [name], 40000)
28
+ try { await listener.line('ARMED', 5000); return listener }
29
+ catch (error) { await stopTraceChild(listener, 1000).catch(() => {}); throw error }
30
+ }
@@ -0,0 +1,82 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+
3
+ export interface TraceCommandResult { code: number | null; output: string; timedOut: boolean }
4
+ export interface OwnedTraceChild {
5
+ done: Promise<TraceCommandResult>
6
+ alive(): boolean
7
+ signal(signal: NodeJS.Signals): void
8
+ line(text: string, timeout: number): Promise<void>
9
+ }
10
+ export type TraceSpawn = (command: string, args: string[], timeout: number) => OwnedTraceChild
11
+ const MAX_OUTPUT = 64 * 1024
12
+
13
+ export const spawnTrace: TraceSpawn = (command, args, timeout) => {
14
+ const child: ChildProcess = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] })
15
+ let output = '', settled = false, timedOut = false
16
+ const waiters = new Set<() => void>()
17
+ let force: ReturnType<typeof setTimeout> | undefined
18
+ const terminate = () => {
19
+ if (settled) return
20
+ child.kill('SIGTERM')
21
+ force ??= setTimeout(() => { if (!settled) child.kill('SIGKILL') }, 1000)
22
+ force.unref()
23
+ }
24
+ const timer = setTimeout(() => { timedOut = true; terminate() }, timeout)
25
+ timer.unref()
26
+ const done = new Promise<TraceCommandResult>(resolve => {
27
+ const finish = (code: number | null) => {
28
+ if (settled) return
29
+ settled = true; clearTimeout(timer); if (force) clearTimeout(force)
30
+ for (const wake of waiters) wake()
31
+ resolve({ code, output, timedOut })
32
+ }
33
+ child.on('error', () => finish(null))
34
+ child.on('close', finish)
35
+ const data = (chunk: Buffer) => {
36
+ output = (output + chunk.toString()).slice(-MAX_OUTPUT)
37
+ for (const wake of waiters) wake()
38
+ }
39
+ child.stdout?.on('data', data); child.stderr?.on('data', data)
40
+ })
41
+ return {
42
+ done, alive: () => !settled && child.exitCode === null && child.signalCode === null,
43
+ signal: signal => { if (!settled) child.kill(signal) },
44
+ line: (text, ms) => new Promise<void>((resolve, reject) => {
45
+ const deadline = setTimeout(() => { waiters.delete(check); reject(new Error('READINESS_TIMEOUT')) }, ms)
46
+ const check = () => {
47
+ if (output.split(/\r?\n/).includes(text)) { clearTimeout(deadline); waiters.delete(check); resolve() }
48
+ else if (settled) { clearTimeout(deadline); waiters.delete(check); reject(new Error('RECORDER_EXITED')) }
49
+ }
50
+ waiters.add(check); check()
51
+ })
52
+ }
53
+ }
54
+
55
+ export async function traceCommand(command: string, args: string[], timeout = 10000, spawn: TraceSpawn = spawnTrace) {
56
+ const result = await spawn(command, args, timeout).done
57
+ if (result.code !== 0 || result.timedOut) throw new Error(result.timedOut ? 'ACTION_TIMEOUT' : 'COMMAND_FAILED')
58
+ return result.output.trim()
59
+ }
60
+ export async function stopTraceChild(child: OwnedTraceChild, timeout = 30000) {
61
+ if (child.alive()) child.signal('SIGINT')
62
+ let timer: ReturnType<typeof setTimeout> | undefined
63
+ const result = await Promise.race([
64
+ child.done,
65
+ new Promise<null>(resolve => { timer = setTimeout(() => resolve(null), timeout) })
66
+ ])
67
+ if (timer) clearTimeout(timer)
68
+ if (result) return result
69
+ child.signal('SIGKILL')
70
+ let forceTimer: ReturnType<typeof setTimeout> | undefined
71
+ await Promise.race([child.done, new Promise<void>(resolve => { forceTimer = setTimeout(resolve, 1000) })])
72
+ if (forceTimer) clearTimeout(forceTimer)
73
+ throw new Error('ACTION_TIMEOUT')
74
+ }
75
+
76
+ export function classifyTraceFailure(result: TraceCommandResult): string {
77
+ if (result.timedOut) return 'ACTION_TIMEOUT'
78
+ if (/permission|not permitted|authoriz|privacy|access denied/i.test(result.output)) return 'PROFILING_PERMISSION_REQUIRED'
79
+ if (/cannot find process|process.*not.*found/i.test(result.output)) return 'APP_NOT_RUNNING'
80
+ if (/disconnect|device.*unavailable/i.test(result.output)) return 'TARGET_DISCONNECTED'
81
+ return 'RECORDER_EXITED'
82
+ }