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.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3090
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +4 -2
- package/dist/observe/readiness.js +29 -10
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace.js +22 -13
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3492
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +5 -3
- package/src/observe/readiness.ts +39 -18
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace.ts +24 -15
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
package/src/observe/android.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { captureEvidence } from './evidence.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { deriveSnapshotMetadata } from "./snapshot-metadata.js"
|
|
12
|
-
import { remainingBudget } from '../utils/operation-budget.js'
|
|
13
|
-
import { createObservationMeasurement } from './performance-metrics.js'
|
|
14
|
-
|
|
15
|
-
const activeLogStreams: Map<string, { proc: any, file: string, device?: { platform: 'android' | 'ios'; id: string } }> = new Map()
|
|
2
|
+
import { GetLogsResponse, CaptureAndroidScreenResponse, GetUITreeResponse, GetCurrentScreenResponse, DeviceInfo } from "../types.js"
|
|
3
|
+
import { getAndroidDeviceMetadata } from "../utils/android/utils.js"
|
|
4
|
+
import { activeLogStreams } from './android/context.js'
|
|
5
|
+
import { collectGetUITree } from './android/ui-tree.js'
|
|
6
|
+
import { collectGetLogs } from './android/logs.js'
|
|
7
|
+
import { collectCaptureScreen } from './android/screenshot.js'
|
|
8
|
+
import { collectGetScreenFingerprint } from './android/fingerprint.js'
|
|
9
|
+
import { startLogStream, stopLogStream, collectReadLogStream } from './android/log-stream.js'
|
|
10
|
+
import { collectGetCurrentScreen } from './android/current-screen.js'
|
|
16
11
|
|
|
17
12
|
export class AndroidObserve {
|
|
18
13
|
getLogStreamDevice(sessionId: string = 'default') { return activeLogStreams.get(sessionId)?.device ?? null }
|
|
@@ -25,548 +20,39 @@ export class AndroidObserve {
|
|
|
25
20
|
return captureEvidence('ui_tree', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId))
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
private async collectGetUITree(deviceId?: string): Promise<GetUITreeResponse> {
|
|
29
|
-
const measurement = createObservationMeasurement('android')
|
|
30
|
-
let metadata
|
|
31
|
-
try {
|
|
32
|
-
metadata = await measurement.timed('metadata_ms', () => getAndroidDeviceMetadata("", deviceId))
|
|
33
|
-
} catch (error) {
|
|
34
|
-
measurement.setOutcome('unavailable')
|
|
35
|
-
measurement.finish()
|
|
36
|
-
throw error
|
|
37
|
-
}
|
|
38
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// Get screen resolution first
|
|
42
|
-
const resolution = await measurement.timed('resolution_ms', () => getScreenResolution(deviceId));
|
|
43
|
-
if (resolution.width === 0 && resolution.height === 0) {
|
|
44
|
-
throw new Error("Failed to get screen resolution. Is the device connected and authorized?");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Retry Logic
|
|
48
|
-
let xmlContent = '';
|
|
49
|
-
let attempts = 0;
|
|
50
|
-
const maxAttempts = 3;
|
|
51
|
-
|
|
52
|
-
while (attempts < maxAttempts) {
|
|
53
|
-
remainingBudget()
|
|
54
|
-
attempts++;
|
|
55
|
-
try {
|
|
56
|
-
// The first read starts immediately so it remains fresh for in-place UI changes.
|
|
57
|
-
// Delay only a retry after a failed or invalid acquisition.
|
|
58
|
-
if (attempts > 1) {
|
|
59
|
-
await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
|
|
60
|
-
}
|
|
61
|
-
remainingBudget()
|
|
62
|
-
|
|
63
|
-
xmlContent = await measurement.physicalRead(async () => {
|
|
64
|
-
await execAdb(['shell', 'uiautomator', 'dump', '/sdcard/ui.xml'], deviceId)
|
|
65
|
-
const content = await execAdb(['shell', 'cat', '/sdcard/ui.xml'], deviceId)
|
|
66
|
-
if (!content || !content.trim() || content.includes('ERROR:')) throw new Error('Empty or invalid UI dump')
|
|
67
|
-
return content
|
|
68
|
-
})
|
|
69
|
-
break
|
|
70
|
-
} catch (e) {
|
|
71
|
-
if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
|
|
72
|
-
console.error(`Attempt ${attempts} failed: ${e}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (attempts === maxAttempts) {
|
|
76
|
-
throw new Error(`Failed to retrieve valid UI dump after ${maxAttempts} attempts.`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const parser = new XMLParser({
|
|
81
|
-
ignoreAttributes: false,
|
|
82
|
-
attributeNamePrefix: "@_"
|
|
83
|
-
});
|
|
84
|
-
const result = await measurement.timed('parse_ms', async () => parser.parse(xmlContent));
|
|
85
|
-
|
|
86
|
-
const elements: UIElement[] = [];
|
|
87
|
-
|
|
88
|
-
// The root is usually hierarchy -> node
|
|
89
|
-
if (result.hierarchy && result.hierarchy.node) {
|
|
90
|
-
// If the root is an array (unlikely for root, but good to be safe) or single object
|
|
91
|
-
if (Array.isArray(result.hierarchy.node)) {
|
|
92
|
-
result.hierarchy.node.forEach((n: any) => traverseNode(n, elements));
|
|
93
|
-
} else {
|
|
94
|
-
traverseNode(result.hierarchy.node, elements);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, {
|
|
99
|
-
screen: "",
|
|
100
|
-
resolution,
|
|
101
|
-
elements
|
|
102
|
-
}, 'ui_tree')
|
|
103
|
-
|
|
104
|
-
measurement.setOutcome('observed')
|
|
105
|
-
const response = measurement.bindObservation({
|
|
106
|
-
device: deviceInfo,
|
|
107
|
-
screen: "",
|
|
108
|
-
resolution,
|
|
109
|
-
elements,
|
|
110
|
-
...snapshotMetadata
|
|
111
|
-
})
|
|
112
|
-
measurement.finish()
|
|
113
|
-
return response
|
|
114
|
-
} catch (e) {
|
|
115
|
-
const errorMessage = `Failed to get UI tree. ADB Path: '${getAdbCmd()}'. Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
116
|
-
console.error(errorMessage);
|
|
117
|
-
const snapshotMetadata = deriveSnapshotMetadata(`android:${deviceInfo.id}`, null, 'ui_tree')
|
|
118
|
-
measurement.setOutcome('unavailable')
|
|
119
|
-
measurement.finish()
|
|
120
|
-
return {
|
|
121
|
-
device: deviceInfo,
|
|
122
|
-
screen: "",
|
|
123
|
-
resolution: { width: 0, height: 0 },
|
|
124
|
-
elements: [],
|
|
125
|
-
...snapshotMetadata,
|
|
126
|
-
error: errorMessage
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
23
|
+
private async collectGetUITree(deviceId?: string): Promise<GetUITreeResponse> { return collectGetUITree(deviceId) }
|
|
130
24
|
|
|
131
25
|
async getLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
|
|
132
26
|
return captureEvidence('logs', { platform: 'android', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters))
|
|
133
27
|
}
|
|
134
28
|
|
|
135
|
-
private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
|
|
136
|
-
const { appId, deviceId, pid, tag, level, contains, since_seconds, limit } = filters
|
|
137
|
-
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId)
|
|
138
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50
|
|
142
|
-
|
|
143
|
-
// If appId provided, try to get pid to filter at source
|
|
144
|
-
let pidArg: string | null = null
|
|
145
|
-
if (appId) {
|
|
146
|
-
try {
|
|
147
|
-
const pidOut = await execAdb(['shell', 'pidof', appId], deviceId).catch(() => '')
|
|
148
|
-
const pidTrim = (pidOut || '').trim()
|
|
149
|
-
if (pidTrim) pidArg = pidTrim.split('\n')[0]
|
|
150
|
-
} catch (err) {
|
|
151
|
-
// Log a warning so failures to detect PID are visible during debugging
|
|
152
|
-
try { console.warn(`getLogs: pid detection failed for appId=${appId}:`, err instanceof Error ? err.message : String(err)) } catch { }
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const args = ['logcat', '-d', '-v', 'threadtime']
|
|
157
|
-
// Apply pid filter via --pid if available
|
|
158
|
-
if (pidArg) args.push(`--pid=${pidArg}`)
|
|
159
|
-
else if (pid) args.push(`--pid=${pid}`)
|
|
160
|
-
|
|
161
|
-
// Apply tag/level filter if provided
|
|
162
|
-
if (tag && level) {
|
|
163
|
-
// Map verbose/debug/info/warn/error to single-letter levels
|
|
164
|
-
const levelMap: Record<string, string> = { 'VERBOSE': 'V', 'DEBUG': 'D', 'INFO': 'I', 'WARN': 'W', 'ERROR': 'E' }
|
|
165
|
-
const L = (levelMap[(level || '').toUpperCase()] || 'V')
|
|
166
|
-
args.push(`${tag}:${L}`)
|
|
167
|
-
} else {
|
|
168
|
-
// Default: show all levels
|
|
169
|
-
args.push('*:V')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Use -t to limit lines (apply early)
|
|
173
|
-
args.push('-t', effectiveLimit.toString())
|
|
174
|
-
|
|
175
|
-
const stdout = await execAdb(args, deviceId)
|
|
176
|
-
const allLines = stdout ? stdout.split(/\r?\n/).filter(Boolean) : []
|
|
177
|
-
|
|
178
|
-
// Parse lines into structured entries
|
|
179
|
-
const parsed = allLines.map(l => {
|
|
180
|
-
const entry = parseLogLine(l)
|
|
181
|
-
// normalize level
|
|
182
|
-
const levelChar = (entry.level || '').toUpperCase()
|
|
183
|
-
const levelMapChar: Record<string,string> = { 'V':'VERBOSE','D':'DEBUG','I':'INFO','W':'WARN','E':'ERROR' }
|
|
184
|
-
const normLevel = levelMapChar[levelChar] || (entry.level && String(entry.level).toUpperCase()) || 'INFO'
|
|
185
|
-
const iso = entry._iso || (() => {
|
|
186
|
-
const d = new Date(entry.timestamp || '')
|
|
187
|
-
if (!isNaN(d.getTime())) return d.toISOString()
|
|
188
|
-
return null
|
|
189
|
-
})()
|
|
190
|
-
let pidNum: number | null = null
|
|
191
|
-
if (entry.pid) pidNum = Number(entry.pid)
|
|
192
|
-
else if (pidArg) pidNum = Number(pidArg)
|
|
193
|
-
const pidVal = (pidNum !== null && !isNaN(pidNum)) ? pidNum : null
|
|
194
|
-
return { timestamp: iso, level: normLevel, tag: entry.tag || '', pid: pidVal, message: entry.message || '' }
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Apply client-side filters: contains, since_seconds
|
|
198
|
-
let filtered = parsed
|
|
199
|
-
if (contains) filtered = filtered.filter(e => e.message && e.message.includes(contains))
|
|
200
|
-
|
|
201
|
-
if (since_seconds) {
|
|
202
|
-
const sinceMs = Date.now() - (since_seconds * 1000)
|
|
203
|
-
filtered = filtered.filter(e => {
|
|
204
|
-
if (!e.timestamp) return false
|
|
205
|
-
const t = new Date(e.timestamp).getTime()
|
|
206
|
-
return t >= sinceMs
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If appId provided and no pidArg, try to filter by appId substring in message/tag
|
|
211
|
-
if (appId && !pidArg) {
|
|
212
|
-
const matched = filtered.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)))
|
|
213
|
-
if (matched.length > 0) filtered = matched
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Ensure ordering oldest -> newest (by timestamp when available)
|
|
217
|
-
filtered.sort((a,b) => {
|
|
218
|
-
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0
|
|
219
|
-
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0
|
|
220
|
-
return ta - tb
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
const limited = filtered.slice(-Math.max(0, effectiveLimit))
|
|
224
|
-
|
|
225
|
-
const source = pidArg ? 'pid' : (appId ? 'package' : 'broad')
|
|
226
|
-
const meta = { pidArg, appIdProvided: !!appId, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid }
|
|
227
|
-
return { device: deviceInfo, logs: limited, logCount: limited.length, source, meta }
|
|
228
|
-
} catch (e) {
|
|
229
|
-
console.error("Error fetching logs:", e)
|
|
230
|
-
return { device: deviceInfo, logs: [], logCount: 0, source: 'broad', meta: { error: e instanceof Error ? e.message : String(e) } }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
29
|
+
private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> { return collectGetLogs(filters) }
|
|
233
30
|
|
|
234
31
|
async captureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
|
|
235
32
|
return captureEvidence('screenshot', { platform: 'android', id: deviceId ?? '' }, () => this.collectCaptureScreen(deviceId))
|
|
236
33
|
}
|
|
237
34
|
|
|
238
|
-
private async collectCaptureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> {
|
|
239
|
-
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
240
|
-
const deviceInfo: DeviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
241
|
-
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
const args = deviceId ? ['-s', deviceId, 'exec-out', 'screencap', '-p'] : ['exec-out', 'screencap', '-p'];
|
|
244
|
-
const child = spawn(getAdbCmd(), args)
|
|
245
|
-
|
|
246
|
-
const chunks: Buffer[] = []
|
|
247
|
-
let stderr = ''
|
|
248
|
-
|
|
249
|
-
child.stdout.on('data', (chunk) => {
|
|
250
|
-
chunks.push(Buffer.from(chunk))
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
child.stderr.on('data', (data) => {
|
|
254
|
-
stderr += data.toString()
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
const timeout = setTimeout(() => {
|
|
258
|
-
child.kill()
|
|
259
|
-
reject(new Error(`ADB screencap timed out after 10s`))
|
|
260
|
-
}, 10000)
|
|
261
|
-
|
|
262
|
-
child.on('close', async (code) => {
|
|
263
|
-
clearTimeout(timeout)
|
|
264
|
-
if (code !== 0) {
|
|
265
|
-
reject(new Error(stderr.trim() || `Screencap failed with code ${code}`))
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const screenshotBuffer = Buffer.concat(chunks)
|
|
270
|
-
const screenshotBase64 = screenshotBuffer.toString('base64')
|
|
271
|
-
|
|
272
|
-
const parsed = parsePngSize(screenshotBuffer)
|
|
273
|
-
if (parsed.width > 0 && parsed.height > 0) {
|
|
274
|
-
// Attempt to convert to WebP (preferred) and provide JPEG fallback (awaited to avoid race)
|
|
275
|
-
try {
|
|
276
|
-
const sharpModule = await import('sharp'); const sharp = sharpModule && (sharpModule as any).default ? (sharpModule as any).default : sharpModule;
|
|
277
|
-
const buf = screenshotBuffer;
|
|
278
|
-
const img = sharp(buf);
|
|
279
|
-
const meta = await img.metadata().catch((err: any) => { console.error('sharp.metadata failed (Android):', err); return {} as any });
|
|
280
|
-
const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
|
|
281
|
-
|
|
282
|
-
let webpBuf: Buffer | null = null;
|
|
283
|
-
let jpegBuf: Buffer | null = null;
|
|
284
|
-
try {
|
|
285
|
-
webpBuf = await img.webp({ quality: 80 }).toBuffer();
|
|
286
|
-
} catch (err) {
|
|
287
|
-
console.error('WebP conversion failed (Android):', err instanceof Error ? err.message : String(err));
|
|
288
|
-
webpBuf = null;
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
|
|
292
|
-
} catch (err) {
|
|
293
|
-
console.error('JPEG conversion failed (Android):', err instanceof Error ? err.message : String(err));
|
|
294
|
-
jpegBuf = null;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (hasAlpha) {
|
|
298
|
-
if (webpBuf) {
|
|
299
|
-
const webpB64 = webpBuf.toString('base64')
|
|
300
|
-
const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
|
|
301
|
-
resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
const pngB64 = buf.toString('base64')
|
|
305
|
-
resolve({ device: deviceInfo, screenshot: pngB64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (webpBuf) {
|
|
310
|
-
const webpB64 = webpBuf.toString('base64')
|
|
311
|
-
const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
|
|
312
|
-
resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
|
|
313
|
-
return
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (jpegBuf) {
|
|
317
|
-
resolve({ device: deviceInfo, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: parsed.width, height: parsed.height } })
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// No conversions succeeded; return original PNG
|
|
322
|
-
resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
|
|
323
|
-
return
|
|
324
|
-
} catch (err) {
|
|
325
|
-
console.error('Screenshot conversion pipeline failed (Android):', err instanceof Error ? err.message : String(err));
|
|
326
|
-
// Conversion failed - fall back to original PNG with parsed resolution
|
|
327
|
-
resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
} else {
|
|
331
|
-
// Fallback to querying wm size if parsing failed
|
|
332
|
-
execAdb(['shell', 'wm', 'size'], deviceId)
|
|
333
|
-
.then(sizeStdout => {
|
|
334
|
-
let width = 0
|
|
335
|
-
let height = 0
|
|
336
|
-
const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/)
|
|
337
|
-
if (match) {
|
|
338
|
-
width = parseInt(match[1], 10)
|
|
339
|
-
height = parseInt(match[2], 10)
|
|
340
|
-
}
|
|
341
|
-
resolve({
|
|
342
|
-
device: deviceInfo,
|
|
343
|
-
screenshot: screenshotBase64,
|
|
344
|
-
screenshot_mime: 'image/png',
|
|
345
|
-
resolution: { width, height }
|
|
346
|
-
})
|
|
347
|
-
})
|
|
348
|
-
.catch(() => {
|
|
349
|
-
resolve({
|
|
350
|
-
device: deviceInfo,
|
|
351
|
-
screenshot: screenshotBase64,
|
|
352
|
-
screenshot_mime: 'image/png',
|
|
353
|
-
resolution: { width: 0, height: 0 }
|
|
354
|
-
})
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
child.on('error', (err) => {
|
|
360
|
-
clearTimeout(timeout)
|
|
361
|
-
reject(err)
|
|
362
|
-
})
|
|
363
|
-
})
|
|
364
|
-
}
|
|
35
|
+
private async collectCaptureScreen(deviceId?: string): Promise<CaptureAndroidScreenResponse> { return collectCaptureScreen(deviceId) }
|
|
365
36
|
|
|
366
37
|
async getCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
|
|
367
38
|
return captureEvidence('activity', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetCurrentScreen(deviceId))
|
|
368
39
|
}
|
|
369
40
|
|
|
370
|
-
private async collectGetCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> {
|
|
371
|
-
const metadata = await getAndroidDeviceMetadata("", deviceId)
|
|
372
|
-
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 })
|
|
376
|
-
const lines = output.split('\n');
|
|
377
|
-
let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
|
|
378
|
-
|
|
379
|
-
if (!resumedLine) {
|
|
380
|
-
resumedLine = lines.find(line => /^\s*ResumedActivity:/.test(line));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (!resumedLine) {
|
|
384
|
-
return {
|
|
385
|
-
device: deviceInfo,
|
|
386
|
-
package: "",
|
|
387
|
-
activity: "",
|
|
388
|
-
shortActivity: "",
|
|
389
|
-
error: "Could not find 'mResumedActivity' in dumpsys output"
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
|
|
394
|
-
|
|
395
|
-
if (match) {
|
|
396
|
-
const packageName = match[1];
|
|
397
|
-
let activityName = match[2];
|
|
398
|
-
|
|
399
|
-
if (activityName.startsWith('.')) {
|
|
400
|
-
activityName = packageName + activityName;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const shortActivity = activityName.split('.').pop() || activityName;
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
device: deviceInfo,
|
|
407
|
-
package: packageName,
|
|
408
|
-
activity: activityName,
|
|
409
|
-
shortActivity: shortActivity
|
|
410
|
-
};
|
|
411
|
-
} else {
|
|
412
|
-
return {
|
|
413
|
-
device: deviceInfo,
|
|
414
|
-
package: "",
|
|
415
|
-
activity: "",
|
|
416
|
-
shortActivity: "",
|
|
417
|
-
error: `Found resumed activity line but failed to parse: '${resumedLine.trim()}'`
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
} catch (e) {
|
|
422
|
-
return {
|
|
423
|
-
device: deviceInfo,
|
|
424
|
-
package: "",
|
|
425
|
-
activity: "",
|
|
426
|
-
shortActivity: "",
|
|
427
|
-
error: e instanceof Error ? e.message : String(e)
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
}
|
|
41
|
+
private async collectGetCurrentScreen(deviceId?: string): Promise<GetCurrentScreenResponse> { return collectGetCurrentScreen(deviceId) }
|
|
431
42
|
|
|
432
43
|
async getScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
433
44
|
return captureEvidence('fingerprint', { platform: 'android', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId))
|
|
434
45
|
}
|
|
435
46
|
|
|
436
|
-
private async collectGetScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
437
|
-
try {
|
|
438
|
-
const tree = await this.getUITree(deviceId)
|
|
439
|
-
if (!tree || (tree as any).error) return { fingerprint: null, error: (tree as any).error }
|
|
440
|
-
|
|
441
|
-
const current = await this.getCurrentScreen(deviceId).catch(() => null)
|
|
442
|
-
return { ...computeScreenFingerprint(tree, current, 'android', 50), captured_at_ms: tree.captured_at_ms }
|
|
443
|
-
} catch (e) {
|
|
444
|
-
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async startLogStream(packageName: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId?: string, sessionId: string = 'default') {
|
|
449
|
-
try {
|
|
450
|
-
const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '')
|
|
451
|
-
const pid = (pidOutput || '').trim()
|
|
452
|
-
if (!pid) return { success: false, error: 'app_not_running' }
|
|
453
|
-
|
|
454
|
-
const levelMap: Record<string, string> = { error: '*:E', warn: '*:W', info: '*:I', debug: '*:D' }
|
|
455
|
-
const filter = levelMap[level] || levelMap['error']
|
|
456
|
-
|
|
457
|
-
if (activeLogStreams.has(sessionId)) {
|
|
458
|
-
try { activeLogStreams.get(sessionId)!.proc.kill() } catch {}
|
|
459
|
-
activeLogStreams.delete(sessionId)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const args = [...(deviceId ? ['-s', deviceId] : []), 'logcat', `--pid=${pid}`, filter]
|
|
463
|
-
const proc = spawn(getAdbCmd(), args)
|
|
47
|
+
private async collectGetScreenFingerprint(deviceId?: string): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> { return collectGetScreenFingerprint(this, deviceId) }
|
|
464
48
|
|
|
465
|
-
|
|
466
|
-
const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`)
|
|
467
|
-
const stream = createWriteStream(file, { flags: 'a' })
|
|
49
|
+
async startLogStream(packageName: string, level: 'error' | 'warn' | 'info' | 'debug' = 'error', deviceId?: string, sessionId: string = 'default') { return startLogStream(packageName, level, deviceId, sessionId) }
|
|
468
50
|
|
|
469
|
-
|
|
470
|
-
const text = chunk.toString()
|
|
471
|
-
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
472
|
-
for (const l of lines) {
|
|
473
|
-
const entry = parseLogLine(l)
|
|
474
|
-
stream.write(JSON.stringify(entry) + '\n')
|
|
475
|
-
}
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
proc.stderr.on('data', (chunk) => {
|
|
479
|
-
const text = chunk.toString()
|
|
480
|
-
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
481
|
-
for (const l of lines) {
|
|
482
|
-
const entry = { timestamp: '', level: 'E', tag: 'adb', message: l }
|
|
483
|
-
stream.write(JSON.stringify(entry) + '\n')
|
|
484
|
-
}
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
proc.on('close', () => {
|
|
488
|
-
stream.end()
|
|
489
|
-
if (activeLogStreams.get(sessionId)?.proc === proc) activeLogStreams.delete(sessionId)
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
activeLogStreams.set(sessionId, { proc, file, device: { platform: 'android', id: deviceId ?? '' } })
|
|
493
|
-
return { success: true, stream_started: true }
|
|
494
|
-
} catch (e) {
|
|
495
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) }
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async stopLogStream(sessionId: string = 'default') {
|
|
500
|
-
const entry = activeLogStreams.get(sessionId)
|
|
501
|
-
if (!entry) return { success: true }
|
|
502
|
-
try { entry.proc.kill() } catch {}
|
|
503
|
-
activeLogStreams.delete(sessionId)
|
|
504
|
-
return { success: true }
|
|
505
|
-
}
|
|
51
|
+
async stopLogStream(sessionId: string = 'default') { return stopLogStream(sessionId) }
|
|
506
52
|
|
|
507
53
|
async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
|
|
508
54
|
return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since))
|
|
509
55
|
}
|
|
510
56
|
|
|
511
|
-
private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string) {
|
|
512
|
-
const entry = activeLogStreams.get(sessionId)
|
|
513
|
-
let file: string | undefined
|
|
514
|
-
if (entry && entry.file) file = entry.file
|
|
515
|
-
else {
|
|
516
|
-
const tmpDir = process.env.TMPDIR || '/tmp'
|
|
517
|
-
const candidate = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`)
|
|
518
|
-
file = candidate
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
const data = await fsPromises.readFile(file, 'utf8').catch(() => '')
|
|
523
|
-
if (!data) return { entries: [], crash_summary: { crash_detected: false } }
|
|
524
|
-
const lines = data.split(/\r?\n/).filter(Boolean)
|
|
525
|
-
|
|
526
|
-
const parsed = lines.map(l => {
|
|
527
|
-
try {
|
|
528
|
-
const obj: any = JSON.parse(l)
|
|
529
|
-
if (typeof obj._iso === 'undefined') {
|
|
530
|
-
let iso: string | null = null
|
|
531
|
-
if (obj.timestamp) {
|
|
532
|
-
const d = new Date(obj.timestamp)
|
|
533
|
-
if (!isNaN(d.getTime())) iso = d.toISOString()
|
|
534
|
-
}
|
|
535
|
-
obj._iso = iso
|
|
536
|
-
}
|
|
537
|
-
if (typeof obj.crash === 'undefined') {
|
|
538
|
-
const msg = (obj.message || '').toString()
|
|
539
|
-
const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/)
|
|
540
|
-
if (/FATAL EXCEPTION/i.test(msg) || exMatch) {
|
|
541
|
-
obj.crash = true
|
|
542
|
-
if (exMatch) obj.exception = exMatch[1]
|
|
543
|
-
} else {
|
|
544
|
-
obj.crash = false
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return obj
|
|
548
|
-
} catch {
|
|
549
|
-
return { message: l, _iso: null, crash: false }
|
|
550
|
-
}
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
let filtered = parsed
|
|
554
|
-
if (since) {
|
|
555
|
-
let sinceMs: number | null = null
|
|
556
|
-
if (/^\d+$/.test(since)) sinceMs = Number(since)
|
|
557
|
-
else {
|
|
558
|
-
const sDate = new Date(since)
|
|
559
|
-
if (!isNaN(sDate.getTime())) sinceMs = sDate.getTime()
|
|
560
|
-
}
|
|
561
|
-
if (sinceMs !== null) filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs))
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const entries = filtered.slice(-Math.max(0, limit))
|
|
565
|
-
const crashEntry = entries.find(e => e.crash)
|
|
566
|
-
const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false }
|
|
567
|
-
return { entries, crash_summary }
|
|
568
|
-
} catch {
|
|
569
|
-
return { entries: [], crash_summary: { crash_detected: false } }
|
|
570
|
-
}
|
|
571
|
-
}
|
|
57
|
+
private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string) { return collectReadLogStream(sessionId, limit, since) }
|
|
572
58
|
}
|