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.
- 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/.cursor-plugin/marketplace.json +1 -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 +218 -129
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- 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 +50 -11
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
- 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/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +133 -74
- package/dist/debugger/worker.js +59 -46
- 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 +77 -3092
- 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 +44 -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-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +244 -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 +5 -3
- package/dist/observe/readiness.js +33 -14
- 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-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +26 -21
- 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/handlers/debugger-handlers.js +67 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +77 -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 +170 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/schemas/system-trace-schema.js +22 -0
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/server.js +4 -4
- 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 +48 -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 +67 -1
- package/docs/README.md +38 -0
- package/docs/agent-plugin-installation-verification.md +39 -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 +120 -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/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +138 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +120 -4
- 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 +89 -14
- package/docs/tools/system.md +12 -5
- package/package.json +12 -8
- package/scripts/agent-plugin.mjs +55 -5
- 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 +3 -2
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +10 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +326 -155
- package/src/debugger/worker.ts +57 -41
- 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 +89 -3494
- 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 +67 -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-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +204 -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 +6 -4
- package/src/observe/readiness.ts +42 -21
- 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-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +28 -19
- 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 +58 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +86 -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 +170 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +46 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/schemas/system-trace-schema.ts +23 -0
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +4 -4
- 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 +60 -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/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -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 +3696 -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/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -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/handle-guards.test.ts +88 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -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/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
- package/test/unit/observe/readiness.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/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -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/system-trace.test.ts +12 -3
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/debugger-handlers.js +0 -38
- package/dist/server/system-trace-schema.js +0 -17
- 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/debugger-schema.ts +0 -37
- package/src/server/system-trace-schema.ts +0 -17
- 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/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -1,1008 +0,0 @@
|
|
|
1
|
-
import { handleDebugger } from './debugger-handlers.js'
|
|
2
|
-
import { debuggerContracts } from './debugger-schema.js'
|
|
3
|
-
import { debuggerSessions } from '../debugger/sessions.js'
|
|
4
|
-
import { withDebuggerGuard, guardDispatched } from '../debugger/guard.js'
|
|
5
|
-
import { systemTraces, TraceError } from '../observe/system-trace.js'
|
|
6
|
-
import { systemTraceTools, traceTargetSchema, traceStartSchema, traceControlSchema } from './system-trace-schema.js'
|
|
7
|
-
import { currentEvidenceCorrelationId, bindSystemTrace, withEvidence, currentEvidenceActionId, validateActionRef, markEvidenceVerification, refreshJourneyEvidence, completeJourneyVerification } from '../observe/evidence.js'
|
|
8
|
-
import { measure, withTiming } from '../utils/timing.js'
|
|
9
|
-
import { randomUUID } from 'node:crypto'
|
|
10
|
-
import { runWithBudget } from '../utils/operation-budget.js'
|
|
11
|
-
import { journeySchema, tapElementControlsSchema } from './journey-schema.js'
|
|
12
|
-
import { readinessSchema, readinessWaitSchema } from './readiness-schema.js'
|
|
13
|
-
import { getAppReadiness, withReadyWait } from '../observe/readiness-service.js'
|
|
14
|
-
import { validateExpectedState } from './condition-schema.js'
|
|
15
|
-
import { conditionFailureCode, createConditionalAction, notifyActionStart, withConditionalAction } from '../interact/conditional-action.js'
|
|
16
|
-
import { resolveTargetDevice, withResolvedDevice } from '../utils/resolve-device.js'
|
|
17
|
-
import type {
|
|
18
|
-
InstallAppResponse,
|
|
19
|
-
ResetAppDataResponse,
|
|
20
|
-
TerminateAppResponse
|
|
21
|
-
} from '../types.js'
|
|
22
|
-
import { AndroidManage, iOSManage, ToolsManage } from '../manage/index.js'
|
|
23
|
-
import { ToolsInteract } from '../interact/index.js'
|
|
24
|
-
import { ToolsObserve } from '../observe/index.js'
|
|
25
|
-
import { classifyActionOutcome } from '../interact/classify.js'
|
|
26
|
-
import { ToolsNetwork } from '../network/index.js'
|
|
27
|
-
import { getSystemStatus } from '../system/index.js'
|
|
28
|
-
import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js'
|
|
29
|
-
import {
|
|
30
|
-
buildActionExecutionResult,
|
|
31
|
-
captureActionFingerprint,
|
|
32
|
-
getArrayArg,
|
|
33
|
-
getBooleanArg,
|
|
34
|
-
getNumberArg,
|
|
35
|
-
getObjectArg,
|
|
36
|
-
getStringArg,
|
|
37
|
-
inferGenericFailure,
|
|
38
|
-
inferScrollFailure,
|
|
39
|
-
requireBooleanArg,
|
|
40
|
-
requireNumberArg,
|
|
41
|
-
requireObjectArg,
|
|
42
|
-
requireStringArg,
|
|
43
|
-
ToolCallArgs,
|
|
44
|
-
ToolCallResult,
|
|
45
|
-
ToolHandler,
|
|
46
|
-
wrapResponse,
|
|
47
|
-
wrapToolError
|
|
48
|
-
} from './common.js'
|
|
49
|
-
|
|
50
|
-
const systemTraceToolNames = new Set(systemTraceTools.map(tool => tool.name))
|
|
51
|
-
|
|
52
|
-
type PlatformArg = 'android' | 'ios'
|
|
53
|
-
type ProjectTypeArg = 'native' | 'kmp' | 'react-native' | 'flutter'
|
|
54
|
-
type ExpectElementSelectorArg = { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
|
|
55
|
-
type WaitForUiMatchArg = { index?: number }
|
|
56
|
-
type WaitForUiRetryArg = { max_attempts?: number, backoff_ms?: number }
|
|
57
|
-
type ScrollSelectorArg = { text?: string, resourceId?: string, contentDesc?: string, className?: string }
|
|
58
|
-
type ClassifyNetworkRequestArg = { endpoint: string, status: 'success' | 'failure' | 'retryable' }
|
|
59
|
-
type VerificationMode = 'none' | 'light' | 'full'
|
|
60
|
-
type ResponseMode = 'compact' | 'debug'
|
|
61
|
-
|
|
62
|
-
function interactionControls(args: ToolCallArgs) {
|
|
63
|
-
const verificationMode = (getStringArg(args, 'verificationMode') as VerificationMode | undefined) ?? 'full'
|
|
64
|
-
const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
|
|
65
|
-
if (!['none', 'light', 'full'].includes(verificationMode) || !['compact', 'debug'].includes(responseMode)) throw new Error('INVALID_ARGUMENT')
|
|
66
|
-
for (const key of ['verificationMode', 'responseMode']) if (args[key] !== undefined && typeof args[key] !== 'string') throw new Error('INVALID_ARGUMENT')
|
|
67
|
-
for (const key of ['actionTimeoutMs', 'verificationTimeoutMs']) if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key]))) throw new Error('INVALID_ARGUMENT')
|
|
68
|
-
const actionTimeoutMs = getNumberArg(args, 'actionTimeoutMs')
|
|
69
|
-
const verificationTimeoutMs = getNumberArg(args, 'verificationTimeoutMs')
|
|
70
|
-
return { verificationMode, responseMode,
|
|
71
|
-
actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
|
|
72
|
-
verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) }
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function compactAction(result: any, verificationMode: VerificationMode, started = true, stateDelta?: unknown, totalMs = 0) {
|
|
77
|
-
return {
|
|
78
|
-
action_id: result.action_id,
|
|
79
|
-
action_type: result.action_type,
|
|
80
|
-
success: !!result.success,
|
|
81
|
-
lifecycle_state: result.success ? (verificationMode === 'full' ? result.lifecycle_state : 'pending_verification') : 'failed',
|
|
82
|
-
...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
83
|
-
...(typeof result.retryable === 'boolean' ? { retryable: result.retryable } : {}),
|
|
84
|
-
delivery_status: result.success ? 'delivered' : (started ? 'unknown' : 'not_delivered'),
|
|
85
|
-
dispatch_started: started,
|
|
86
|
-
verification: { mode: verificationMode, status: result.success ? (verificationMode === 'none' ? 'not_requested' : verificationMode === 'light' ? 'observed' : (result.lifecycle_state === 'verified' ? 'verified' : 'observed')) : 'failed' },
|
|
87
|
-
...(stateDelta ? { state_delta: stateDelta } : {}),
|
|
88
|
-
timing: { total_ms: totalMs }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function correlatedHandler(name: string, handler: ToolHandler, action = false, journey = false): ToolHandler {
|
|
93
|
-
return async args => {
|
|
94
|
-
const ref = validateActionRef(args.actionRef)
|
|
95
|
-
const { result, evidence } = await withEvidence({ action, journey, ref }, async () => {
|
|
96
|
-
try { return await handler(args) }
|
|
97
|
-
catch (error) { return error instanceof TraceError ? { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true } : wrapToolError(name, error) }
|
|
98
|
-
})
|
|
99
|
-
const item = result.content.find(item => item.type === 'text')
|
|
100
|
-
if (item?.text) {
|
|
101
|
-
const payload = JSON.parse(item.text)
|
|
102
|
-
payload.evidence = evidence
|
|
103
|
-
item.text = JSON.stringify(payload)
|
|
104
|
-
}
|
|
105
|
-
return result
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function actionHandler(type: string, execute: ToolHandler): ToolHandler {
|
|
110
|
-
return correlatedHandler(type, async (args) => {
|
|
111
|
-
const timing: Record<string, number> = {}
|
|
112
|
-
return withTiming(timing, async () => {
|
|
113
|
-
const started = performance.now()
|
|
114
|
-
const controls = interactionControls(args)
|
|
115
|
-
const expected = validateExpectedState(type, args)
|
|
116
|
-
const condition = expected ? createConditionalAction(args.platform as PlatformArg, args.deviceId as string, expected) : undefined
|
|
117
|
-
const budgetMs = controls.actionTimeoutMs ?? ({ start_app: 10000, restart_app: 15000, scroll_to_element: 10000 }[type] ?? 5000)
|
|
118
|
-
let dispatchStarted = false
|
|
119
|
-
let result: any
|
|
120
|
-
const dispatchWithBudget = (timeoutMs: number) => runWithBudget(timeoutMs, async (budget) => {
|
|
121
|
-
try {
|
|
122
|
-
if (!condition) return payloadOf(await execute(args))
|
|
123
|
-
return await withConditionalAction(condition, async () => {
|
|
124
|
-
const device = await resolveTargetDevice({ platform: condition.platform, deviceId: condition.deviceId })
|
|
125
|
-
return withResolvedDevice(device, async () => payloadOf(await execute(args)))
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
finally { dispatchStarted = budget.dispatchStarted }
|
|
129
|
-
})
|
|
130
|
-
try {
|
|
131
|
-
// Omitted controls retain the existing adapter budgets.
|
|
132
|
-
result = args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined
|
|
133
|
-
? await dispatchWithBudget(Infinity) : await dispatchWithBudget(budgetMs)
|
|
134
|
-
if (result.success) dispatchStarted = true
|
|
135
|
-
} catch (error) {
|
|
136
|
-
if (!condition && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined && !/TIMEOUT|timed out/i.test(String(error))) return wrapToolError(type, error)
|
|
137
|
-
result = { success: false, failure_code: /TIMEOUT|timed out/i.test(String(error)) ? 'ACTION_TIMEOUT' : 'UNKNOWN', error: String(error) }
|
|
138
|
-
}
|
|
139
|
-
const blocked = condition && ['mismatch', 'unavailable'].includes(condition.result.status)
|
|
140
|
-
if (blocked) {
|
|
141
|
-
dispatchStarted = false
|
|
142
|
-
result = buildActionExecutionResult({
|
|
143
|
-
actionType: type, sourceModule: 'server', device: condition.result.observed?.device,
|
|
144
|
-
selector: result?.target?.selector ?? null, resolved: result?.target?.resolved,
|
|
145
|
-
success: false, uiFingerprintBefore: result?.ui_fingerprint_before ?? null, uiFingerprintAfter: null,
|
|
146
|
-
failure: { failureCode: conditionFailureCode(condition), retryable: false },
|
|
147
|
-
traceSteps: [{ stage: 'verify', timestamp: Date.now(), result: 'failure', attempt_index: 0, metadata: { phase: 'precondition', dispatch_started: false } }]
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
if (result.failure_code === 'TIMEOUT' || /ACTION_TIMEOUT|timeout/i.test(result.error ?? result.reason ?? result.details?.error ?? '')) result.failure_code = 'ACTION_TIMEOUT'
|
|
151
|
-
if (result.failure_code === 'ACTION_TIMEOUT') result.retryable = !dispatchStarted
|
|
152
|
-
if (!result.success && typeof result.retryable !== 'boolean') result.retryable = !dispatchStarted
|
|
153
|
-
result.action_id ??= currentEvidenceActionId() ?? randomUUID()
|
|
154
|
-
result.action_type ??= type
|
|
155
|
-
recordObservationEvent({
|
|
156
|
-
step_id: args._stepId as string | undefined,
|
|
157
|
-
action_type: type,
|
|
158
|
-
dispatch_ms: timing.dispatch_ms,
|
|
159
|
-
verification_mode: controls.verificationMode,
|
|
160
|
-
dispatch_started: dispatchStarted
|
|
161
|
-
})
|
|
162
|
-
let delta: unknown
|
|
163
|
-
let observationAvailable = false
|
|
164
|
-
if (result.success && controls.verificationMode === 'light') {
|
|
165
|
-
const verificationStarted = performance.now()
|
|
166
|
-
const tree = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'verification' }, () =>
|
|
167
|
-
measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () =>
|
|
168
|
-
ToolsObserve.getUITreeHandler({ platform: (args.platform as PlatformArg | undefined) ?? result.device?.platform, deviceId: (args.deviceId as string | undefined) ?? result.device?.id })))).catch(() => null) as any
|
|
169
|
-
markEvidenceVerification()
|
|
170
|
-
observationAvailable = !!tree && !tree.error
|
|
171
|
-
recordObservationEvent({
|
|
172
|
-
verification_budget_ms: controls.verificationTimeoutMs,
|
|
173
|
-
verification_elapsed_ms: performance.now() - verificationStarted,
|
|
174
|
-
verification_outcome: observationAvailable ? 'observed' : 'unavailable',
|
|
175
|
-
verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
|
|
176
|
-
})
|
|
177
|
-
if (observationAvailable && tree.device) delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined
|
|
178
|
-
}
|
|
179
|
-
if (!blocked && controls.verificationMode === 'full' && dispatchStarted) markEvidenceVerification()
|
|
180
|
-
const compact = compactAction(result, controls.verificationMode, dispatchStarted, delta, performance.now() - started)
|
|
181
|
-
if (blocked) compact.verification.status = 'not_requested'
|
|
182
|
-
if (result.success && controls.verificationMode === 'light' && !observationAvailable) compact.verification.status = 'unavailable'
|
|
183
|
-
const evidence = {
|
|
184
|
-
...compact,
|
|
185
|
-
...(condition ? { condition: controls.responseMode === 'compact' && condition.result.status === 'matched'
|
|
186
|
-
? { ...condition.result, observed: condition.result.observed ? {
|
|
187
|
-
snapshot_session_id: condition.result.observed.snapshot_session_id,
|
|
188
|
-
snapshot_revision: condition.result.observed.snapshot_revision,
|
|
189
|
-
captured_at_ms: condition.result.observed.captured_at_ms
|
|
190
|
-
} : null } : condition.result } : {}),
|
|
191
|
-
...(controls.verificationMode === 'light' ? { state_delta_available: !!delta } : {}),
|
|
192
|
-
...(result.success && controls.verificationMode === 'light' && !observationAvailable ? { verification_diagnostic: { code: 'VERIFICATION_UNAVAILABLE', retryable: true } } : {})
|
|
193
|
-
}
|
|
194
|
-
return wrapResponse(controls.responseMode === 'compact' ? evidence : {
|
|
195
|
-
...result, ...evidence, timing: { ...timing, total_ms: performance.now() - started },
|
|
196
|
-
effective_controls: { ...controls, actionTimeoutMs: args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined ? null : budgetMs, timeout_bound: !(args._journey !== true && controls.verificationMode === 'full' && controls.responseMode === 'debug' && controls.actionTimeoutMs === undefined) }
|
|
197
|
-
}, controls.responseMode === 'debug')
|
|
198
|
-
})
|
|
199
|
-
}, true)
|
|
200
|
-
}
|
|
201
|
-
const handleStartApp = actionHandler('start_app', executeStartApp)
|
|
202
|
-
const handleRestartApp = actionHandler('restart_app', executeRestartApp)
|
|
203
|
-
const handleTap = actionHandler('tap', executeTap)
|
|
204
|
-
const handleTapElement = actionHandler('tap_element', executeTapElement)
|
|
205
|
-
const handleSwipe = actionHandler('swipe', executeSwipe)
|
|
206
|
-
const handleScrollToElement = actionHandler('scroll_to_element', executeScrollToElement)
|
|
207
|
-
const handleTypeText = actionHandler('type_text', executeTypeText)
|
|
208
|
-
const handlePressBack = actionHandler('press_back', executePressBack)
|
|
209
|
-
|
|
210
|
-
async function executeStartApp(args: ToolCallArgs) {
|
|
211
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
212
|
-
const appId = requireStringArg(args, 'appId')
|
|
213
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
214
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
215
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
216
|
-
notifyActionStart()
|
|
217
|
-
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')))
|
|
218
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
219
|
-
const result = buildActionExecutionResult({
|
|
220
|
-
actionType: 'start_app',
|
|
221
|
-
sourceModule: 'server',
|
|
222
|
-
device: res.device,
|
|
223
|
-
selector: { appId },
|
|
224
|
-
success: !!res.appStarted,
|
|
225
|
-
uiFingerprintBefore,
|
|
226
|
-
uiFingerprintAfter,
|
|
227
|
-
failure: res.appStarted ? undefined : inferGenericFailure(res.error),
|
|
228
|
-
details: {
|
|
229
|
-
launch_time_ms: res.launchTimeMs,
|
|
230
|
-
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
231
|
-
...(res.device ? { device_id: res.device.id } : {}),
|
|
232
|
-
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
233
|
-
...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
|
|
234
|
-
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
235
|
-
}
|
|
236
|
-
})
|
|
237
|
-
return wrapResponse(result)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async function handleTerminateApp(args: ToolCallArgs) {
|
|
241
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
242
|
-
const appId = requireStringArg(args, 'appId')
|
|
243
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
244
|
-
const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
|
|
245
|
-
const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
|
|
246
|
-
return wrapResponse(response)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function executeRestartApp(args: ToolCallArgs) {
|
|
250
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
251
|
-
const appId = requireStringArg(args, 'appId')
|
|
252
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
253
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
254
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
255
|
-
notifyActionStart()
|
|
256
|
-
const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined
|
|
257
|
-
const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)))
|
|
258
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
259
|
-
const result = buildActionExecutionResult({
|
|
260
|
-
actionType: 'restart_app',
|
|
261
|
-
sourceModule: 'server',
|
|
262
|
-
device: res.device,
|
|
263
|
-
selector: { appId },
|
|
264
|
-
success: !!res.appRestarted,
|
|
265
|
-
uiFingerprintBefore,
|
|
266
|
-
uiFingerprintAfter,
|
|
267
|
-
failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
|
|
268
|
-
details: {
|
|
269
|
-
launch_time_ms: res.launchTimeMs,
|
|
270
|
-
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
271
|
-
...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
|
|
272
|
-
...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
|
|
273
|
-
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
274
|
-
...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
|
|
275
|
-
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
276
|
-
}
|
|
277
|
-
})
|
|
278
|
-
return wrapResponse(result)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function handleResetAppData(args: ToolCallArgs) {
|
|
282
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
283
|
-
const appId = requireStringArg(args, 'appId')
|
|
284
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
285
|
-
const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
|
|
286
|
-
const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
|
|
287
|
-
return wrapResponse(response)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function handleInstallApp(args: ToolCallArgs) {
|
|
291
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
292
|
-
const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
|
|
293
|
-
const appPath = requireStringArg(args, 'appPath')
|
|
294
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
295
|
-
const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
|
|
296
|
-
const response: InstallAppResponse = {
|
|
297
|
-
device: res.device,
|
|
298
|
-
installed: res.installed,
|
|
299
|
-
output: (res as any).output,
|
|
300
|
-
error: (res as any).error
|
|
301
|
-
}
|
|
302
|
-
return wrapResponse(response)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function handleBuildApp(args: ToolCallArgs) {
|
|
306
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
307
|
-
const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
|
|
308
|
-
const projectPath = requireStringArg(args, 'projectPath')
|
|
309
|
-
const variant = getStringArg(args, 'variant')
|
|
310
|
-
const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
|
|
311
|
-
return wrapResponse(res)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function handleBuildAndInstall(args: ToolCallArgs) {
|
|
315
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
316
|
-
const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
|
|
317
|
-
const projectPath = requireStringArg(args, 'projectPath')
|
|
318
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
319
|
-
const timeout = getNumberArg(args, 'timeout')
|
|
320
|
-
const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
|
|
321
|
-
return {
|
|
322
|
-
content: [
|
|
323
|
-
{ type: 'text' as const, text: res.ndjson },
|
|
324
|
-
{ type: 'text' as const, text: JSON.stringify(res.result, null, 2) }
|
|
325
|
-
]
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function handleGetLogs(args: ToolCallArgs) {
|
|
330
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
331
|
-
const appId = getStringArg(args, 'appId')
|
|
332
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
333
|
-
const pid = getNumberArg(args, 'pid')
|
|
334
|
-
const tag = getStringArg(args, 'tag')
|
|
335
|
-
const level = getStringArg(args, 'level')
|
|
336
|
-
const contains = getStringArg(args, 'contains')
|
|
337
|
-
const since_seconds = getNumberArg(args, 'since_seconds')
|
|
338
|
-
const limit = getNumberArg(args, 'limit')
|
|
339
|
-
const lines = getNumberArg(args, 'lines')
|
|
340
|
-
const res = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, pid, tag, level, contains, since_seconds, limit, lines })
|
|
341
|
-
const filtered = !!(pid || tag || level || contains || since_seconds || appId)
|
|
342
|
-
return {
|
|
343
|
-
content: [
|
|
344
|
-
{ type: 'text' as const, text: JSON.stringify({ device: res.device, result: { count: res.logCount, filtered, crashLines: (res.crashLines || []), source: res.source, meta: res.meta || {} } }, null, 2) },
|
|
345
|
-
{ type: 'text' as const, text: JSON.stringify({ logs: res.logs }, null, 2) }
|
|
346
|
-
]
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function handleListDevices(args: ToolCallArgs) {
|
|
351
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
352
|
-
const appId = getStringArg(args, 'appId')
|
|
353
|
-
const res = await ToolsManage.listDevicesHandler({ platform, appId })
|
|
354
|
-
return wrapResponse(res)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function handleGetSystemStatus() {
|
|
358
|
-
const result = await getSystemStatus()
|
|
359
|
-
return wrapResponse(result)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async function handleCaptureScreenshot(args: ToolCallArgs) {
|
|
363
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
364
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
365
|
-
const started = performance.now()
|
|
366
|
-
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
|
|
367
|
-
const screenshotMs = performance.now() - started
|
|
368
|
-
const mime = (res as any).screenshot_mime || 'image/png'
|
|
369
|
-
const content: Array<{ type: 'text' | 'image'; text?: string; data?: string; mimeType?: string }> = [
|
|
370
|
-
{ type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution, mimeType: mime }, timing: { total_ms: screenshotMs, screenshot_ms: screenshotMs } }, null, 2) },
|
|
371
|
-
{ type: 'image', data: (res as any).screenshot, mimeType: mime }
|
|
372
|
-
]
|
|
373
|
-
if ((res as any).screenshot_fallback) {
|
|
374
|
-
content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' }) })
|
|
375
|
-
content.push({ type: 'image', data: (res as any).screenshot_fallback, mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' })
|
|
376
|
-
}
|
|
377
|
-
return { content }
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function handleCaptureDebugSnapshot(args: ToolCallArgs) {
|
|
381
|
-
const reason = getStringArg(args, 'reason')
|
|
382
|
-
const includeLogs = getBooleanArg(args, 'includeLogs')
|
|
383
|
-
const logLines = getNumberArg(args, 'logLines')
|
|
384
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
385
|
-
const appId = getStringArg(args, 'appId')
|
|
386
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
387
|
-
const sessionId = getStringArg(args, 'sessionId')
|
|
388
|
-
const includeScreenshot = getBooleanArg(args, 'includeScreenshot')
|
|
389
|
-
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, includeScreenshot, logLines, platform, appId, deviceId, sessionId })
|
|
390
|
-
return wrapResponse(res)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function handleGetUITree(args: ToolCallArgs) {
|
|
394
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
395
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
396
|
-
const responseMode = (getStringArg(args, 'responseMode') as ResponseMode | undefined) ?? 'debug'
|
|
397
|
-
const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision')
|
|
398
|
-
const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined
|
|
399
|
-
const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () =>
|
|
400
|
-
ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }))
|
|
401
|
-
return wrapResponse(res)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function handleGetCurrentScreen(args: ToolCallArgs) {
|
|
405
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
406
|
-
const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
|
|
407
|
-
return wrapResponse(res)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async function handleGetScreenFingerprint(args: ToolCallArgs) {
|
|
411
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
412
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
413
|
-
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
|
|
414
|
-
return wrapResponse(res)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function handleWaitForScreenChange(args: ToolCallArgs) {
|
|
418
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
419
|
-
const previousFingerprint = requireStringArg(args, 'previousFingerprint')
|
|
420
|
-
const timeoutMs = getNumberArg(args, 'timeoutMs')
|
|
421
|
-
const pollIntervalMs = getNumberArg(args, 'pollIntervalMs')
|
|
422
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
423
|
-
const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
|
|
424
|
-
return wrapResponse(res)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function handleExpectScreen(args: ToolCallArgs) {
|
|
428
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
429
|
-
const fingerprint = getStringArg(args, 'fingerprint')
|
|
430
|
-
const screen = getStringArg(args, 'screen')
|
|
431
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
432
|
-
const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId })
|
|
433
|
-
return wrapResponse(res)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async function handleExpectElementVisible(args: ToolCallArgs) {
|
|
437
|
-
const selector = requireObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
438
|
-
const element_id = getStringArg(args, 'element_id')
|
|
439
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms')
|
|
440
|
-
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms')
|
|
441
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
442
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
443
|
-
const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId })
|
|
444
|
-
return wrapResponse(res)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async function handleExpectState(args: ToolCallArgs) {
|
|
448
|
-
const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
449
|
-
const element_id = getStringArg(args, 'element_id')
|
|
450
|
-
const property = requireStringArg(args, 'property')
|
|
451
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
452
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
453
|
-
if (!selector && !element_id) {
|
|
454
|
-
throw new Error('Missing selector or element_id argument')
|
|
455
|
-
}
|
|
456
|
-
if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
|
|
457
|
-
throw new Error('Missing expected argument')
|
|
458
|
-
}
|
|
459
|
-
const expected = args.expected as boolean | number | string | Record<string, unknown>
|
|
460
|
-
const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId })
|
|
461
|
-
return wrapResponse(res)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function handleAdjustControl(args: ToolCallArgs) {
|
|
465
|
-
const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
466
|
-
const element_id = getStringArg(args, 'element_id')
|
|
467
|
-
const property = getStringArg(args, 'property') ?? 'value'
|
|
468
|
-
const targetValue = requireNumberArg(args, 'targetValue')
|
|
469
|
-
const tolerance = getNumberArg(args, 'tolerance')
|
|
470
|
-
const maxAttempts = getNumberArg(args, 'maxAttempts')
|
|
471
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
472
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
473
|
-
if ((!selector && !element_id) || (selector && element_id)) {
|
|
474
|
-
throw new Error('Exactly one of selector or element_id argument is required')
|
|
475
|
-
}
|
|
476
|
-
const res = await ToolsInteract.adjustControlHandler({
|
|
477
|
-
selector: selector ?? undefined,
|
|
478
|
-
element_id: element_id ?? undefined,
|
|
479
|
-
property,
|
|
480
|
-
targetValue,
|
|
481
|
-
tolerance,
|
|
482
|
-
maxAttempts,
|
|
483
|
-
platform,
|
|
484
|
-
deviceId
|
|
485
|
-
})
|
|
486
|
-
markEvidenceVerification()
|
|
487
|
-
return wrapResponse(res)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
async function handleWaitForUI(args: ToolCallArgs) {
|
|
491
|
-
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
|
|
492
|
-
const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
|
|
493
|
-
const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
|
|
494
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
|
|
495
|
-
const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300
|
|
496
|
-
const match = getObjectArg<WaitForUiMatchArg>(args, 'match')
|
|
497
|
-
const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
|
|
498
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
499
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
500
|
-
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId })
|
|
501
|
-
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
|
|
502
|
-
device => execute(device.platform as PlatformArg, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute()
|
|
503
|
-
return wrapResponse(res)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async function handleWaitForUIChange(args: ToolCallArgs) {
|
|
507
|
-
if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
|
|
508
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
509
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
510
|
-
const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
|
|
511
|
-
const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300
|
|
512
|
-
const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
|
|
513
|
-
const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
|
|
514
|
-
const target = getStringArg(args, 'target')
|
|
515
|
-
const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target })
|
|
516
|
-
const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
|
|
517
|
-
device => execute(device.platform as PlatformArg, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute()
|
|
518
|
-
return wrapResponse(res)
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function handleFindElement(args: ToolCallArgs) {
|
|
522
|
-
const query = requireStringArg(args, 'query')
|
|
523
|
-
const exact = getBooleanArg(args, 'exact') ?? false
|
|
524
|
-
const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000
|
|
525
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
526
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
527
|
-
const targetStarted = performance.now()
|
|
528
|
-
const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }))
|
|
529
|
-
recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' })
|
|
530
|
-
return wrapResponse(res)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async function executeTap(args: ToolCallArgs) {
|
|
534
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
535
|
-
const x = requireNumberArg(args, 'x')
|
|
536
|
-
const y = requireNumberArg(args, 'y')
|
|
537
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
538
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
539
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
540
|
-
notifyActionStart()
|
|
541
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }))
|
|
542
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
543
|
-
const result = buildActionExecutionResult({
|
|
544
|
-
actionType: 'tap',
|
|
545
|
-
sourceModule: 'server',
|
|
546
|
-
selector: { x, y },
|
|
547
|
-
success: !!res.success,
|
|
548
|
-
uiFingerprintBefore,
|
|
549
|
-
uiFingerprintAfter,
|
|
550
|
-
failure: res.success ? undefined : inferGenericFailure((res as any).error)
|
|
551
|
-
})
|
|
552
|
-
return wrapResponse(result)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function executeTapElement(args: ToolCallArgs) {
|
|
556
|
-
const controls = tapElementControlsSchema.safeParse({ elementId: args.elementId, selector: args.selector, waitFor: args.waitFor })
|
|
557
|
-
if (!controls.success) throw new Error(`INVALID_ARGUMENT: ${controls.error.message}`)
|
|
558
|
-
const elementId = controls.data.elementId
|
|
559
|
-
const selector = controls.data.selector as ExpectElementSelectorArg | undefined
|
|
560
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
561
|
-
let resolvedId = elementId
|
|
562
|
-
let freshTree: any
|
|
563
|
-
if (selector) {
|
|
564
|
-
const waitFor = getObjectArg<WaitForUiMatchArg>(args, 'waitFor') as any
|
|
565
|
-
const targetStarted = performance.now()
|
|
566
|
-
const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000
|
|
567
|
-
const resolution = await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'target' }, () =>
|
|
568
|
-
measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })))) as any
|
|
569
|
-
recordObservationEvent({ step_id: args._stepId as string | undefined, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status })
|
|
570
|
-
if (resolution.status !== 'success' || !resolution.element?.elementId) return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true })
|
|
571
|
-
resolvedId = resolution.element.elementId
|
|
572
|
-
freshTree = resolution._tree
|
|
573
|
-
}
|
|
574
|
-
notifyActionStart()
|
|
575
|
-
if (freshTree) recordObservationConsumer(freshTree, 'target', args._stepId as string | undefined)
|
|
576
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId!, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })) as any
|
|
577
|
-
return wrapResponse(res)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function payloadOf(response: ToolCallResult) {
|
|
581
|
-
const text = response.content.find((item: any) => item.type === 'text')?.text
|
|
582
|
-
return text ? JSON.parse(text) : null
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
async function expectElementAbsent({ selector, timeoutMs, pollIntervalMs, platform, deviceId }: { selector: ExpectElementSelectorArg, timeoutMs: number, pollIntervalMs: number, platform: PlatformArg, deviceId?: string }) {
|
|
586
|
-
const deadline = performance.now() + timeoutMs
|
|
587
|
-
let consecutive = 0
|
|
588
|
-
let observed: unknown
|
|
589
|
-
while (performance.now() < deadline) {
|
|
590
|
-
const result = await ToolsInteract.waitForUIHandler({ selector, condition: 'not_exists', timeout_ms: 0, singleObservation: true, singleAttempt: true, platform, deviceId }) as any
|
|
591
|
-
observed = result.observed
|
|
592
|
-
consecutive = result.status === 'success' ? consecutive + 1 : 0
|
|
593
|
-
if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed }
|
|
594
|
-
const delay = Math.max(100, pollIntervalMs)
|
|
595
|
-
if (performance.now() + delay >= deadline) break
|
|
596
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
|
597
|
-
}
|
|
598
|
-
return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed }
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function expectFingerprint({ fingerprint, timeoutMs, platform, deviceId }: { fingerprint: string, timeoutMs: number, platform: PlatformArg, deviceId?: string }) {
|
|
602
|
-
const deadline = performance.now() + timeoutMs
|
|
603
|
-
let consecutive = 0
|
|
604
|
-
let observed: string | null = null
|
|
605
|
-
while (performance.now() < deadline) {
|
|
606
|
-
const result = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as any
|
|
607
|
-
observed = result?.fingerprint ?? null
|
|
608
|
-
consecutive = observed === fingerprint ? consecutive + 1 : 0
|
|
609
|
-
if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed_fingerprint: observed }
|
|
610
|
-
if (performance.now() + 100 >= deadline) break
|
|
611
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
612
|
-
}
|
|
613
|
-
return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed_fingerprint: observed }
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
async function handleRunJourney(args: ToolCallArgs) {
|
|
617
|
-
const parsed = journeySchema.safeParse(args)
|
|
618
|
-
if (!parsed.success) throw new Error(`INVALID_ARGUMENT: ${parsed.error.message}`)
|
|
619
|
-
const { platform, responseMode, defaults, captureOnFailure } = parsed.data
|
|
620
|
-
const steps = parsed.data.steps as Array<Record<string, any>>
|
|
621
|
-
const device = await resolveTargetDevice({ platform, deviceId: parsed.data.deviceId })
|
|
622
|
-
const deviceId = device.id
|
|
623
|
-
const finishTrace = parsed.data.systemTraceId
|
|
624
|
-
? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()!) : undefined
|
|
625
|
-
if (parsed.data.systemTraceId) bindSystemTrace(parsed.data.systemTraceId)
|
|
626
|
-
try {
|
|
627
|
-
return await withResolvedDevice(device, async () => {
|
|
628
|
-
const started = performance.now()
|
|
629
|
-
const results: any[] = []
|
|
630
|
-
let stoppedAt: string | null = null
|
|
631
|
-
let pendingLightStep: number | null = null
|
|
632
|
-
for (let index = 0; index < steps.length; index++) {
|
|
633
|
-
const step = steps[index]
|
|
634
|
-
if (platform === 'android' && debuggerSessions.paused(deviceId)) {
|
|
635
|
-
results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } })
|
|
636
|
-
stoppedAt = step.id as string
|
|
637
|
-
break
|
|
638
|
-
}
|
|
639
|
-
const requestedVerification = step.verificationMode ?? defaults?.verificationMode ?? 'full'
|
|
640
|
-
const next = steps[index + 1]
|
|
641
|
-
const lightCoveredByAssertion = step.type === 'tap' && responseMode === 'compact' && requestedVerification === 'light' && next?.type === 'assert' &&
|
|
642
|
-
next.assertion?.kind === 'element_visible'
|
|
643
|
-
const stepStart = performance.now()
|
|
644
|
-
let result: any
|
|
645
|
-
if (stoppedAt) {
|
|
646
|
-
results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } })
|
|
647
|
-
continue
|
|
648
|
-
}
|
|
649
|
-
try {
|
|
650
|
-
if (step.type === 'tap') {
|
|
651
|
-
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }))
|
|
652
|
-
} else if (step.type === 'wait') {
|
|
653
|
-
result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector as ExpectElementSelectorArg, condition: (step.condition as any) ?? 'exists', timeout_ms: (step.timeoutMs as number | undefined) ?? 5000, poll_interval_ms: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId }))
|
|
654
|
-
result = { success: result.status === 'success', ...result }
|
|
655
|
-
} else if (step.type === 'assert') {
|
|
656
|
-
result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget((step.timeoutMs as number | undefined) ?? 5000, async () => {
|
|
657
|
-
const assertion = step.assertion as any
|
|
658
|
-
if (assertion?.kind === 'element_visible') result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: (step.timeoutMs as number | undefined) ?? 5000, poll_interval_ms: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId })
|
|
659
|
-
else if (assertion?.kind === 'element_absent') result = await expectElementAbsent({ selector: assertion.selector, timeoutMs: (step.timeoutMs as number | undefined) ?? 5000, pollIntervalMs: (step.pollIntervalMs as number | undefined) ?? 100, platform, deviceId })
|
|
660
|
-
else if (assertion?.kind === 'screen_fingerprint') result = await expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs: (step.timeoutMs as number | undefined) ?? 5000, platform, deviceId })
|
|
661
|
-
else if (assertion?.kind === 'state_equals') result = await ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, exact_budget: true })
|
|
662
|
-
else result = { success: false, failure_code: 'INVALID_ASSERTION' }
|
|
663
|
-
return result
|
|
664
|
-
}))
|
|
665
|
-
} else if (step.type === 'start_app') {
|
|
666
|
-
result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }))
|
|
667
|
-
} else {
|
|
668
|
-
result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' }
|
|
669
|
-
}
|
|
670
|
-
} catch (error) {
|
|
671
|
-
result = { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) }
|
|
672
|
-
}
|
|
673
|
-
if (platform === 'android' && debuggerSessions.paused(deviceId)) { result.success = false; result.failure_code = 'DEBUGGER_PAUSED'; result.retryable = false }
|
|
674
|
-
if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? '')) result.failure_code = 'ASSERTION_TIMEOUT'
|
|
675
|
-
const success = !!result?.success
|
|
676
|
-
if (lightCoveredByAssertion) {
|
|
677
|
-
result.verification = { mode: 'light', status: success ? 'unavailable' :
|
|
678
|
-
(['mismatch', 'unavailable'].includes(result.condition?.status) ? 'not_requested' : 'failed') }
|
|
679
|
-
if (success) {
|
|
680
|
-
result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true }
|
|
681
|
-
pendingLightStep = results.length
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (step.type === 'assert' && pendingLightStep !== null) {
|
|
685
|
-
const pending = results[pendingLightStep].result
|
|
686
|
-
if (typeof result?.observed?.matched_count === 'number') {
|
|
687
|
-
pending.verification.status = 'observed'
|
|
688
|
-
if (pending.evidence) completeJourneyVerification(pending.evidence.action_id)
|
|
689
|
-
delete pending.verification_diagnostic
|
|
690
|
-
const delta = ToolsObserve.getLatestStateDelta(platform, deviceId)
|
|
691
|
-
if (delta) { pending.state_delta = delta; pending.state_delta_available = true }
|
|
692
|
-
}
|
|
693
|
-
pendingLightStep = null
|
|
694
|
-
}
|
|
695
|
-
if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type)) result = {
|
|
696
|
-
success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
|
|
697
|
-
...(result.error?.code ? { failure_code: result.error.code } : {}),
|
|
698
|
-
...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
|
|
699
|
-
...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
|
|
700
|
-
...(result.actual_state ? { observed: result.actual_state } : {})
|
|
701
|
-
}
|
|
702
|
-
results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - stepStart } })
|
|
703
|
-
if (!success) stoppedAt = step.id as string
|
|
704
|
-
}
|
|
705
|
-
const failureSnapshot = stoppedAt && captureOnFailure && responseMode === 'debug'
|
|
706
|
-
? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId, includeScreenshot: false }).catch(() => null) : undefined
|
|
707
|
-
const componentTiming: Record<string, number> = {}
|
|
708
|
-
for (const step of results) for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
|
|
709
|
-
if (key !== 'total_ms' && typeof value === 'number') componentTiming[key] = (componentTiming[key] ?? 0) + value
|
|
710
|
-
}
|
|
711
|
-
refreshJourneyEvidence(results)
|
|
712
|
-
return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: stoppedAt === null, stopped_at_step_id: stoppedAt, steps: results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...componentTiming, total_ms: performance.now() - started } }, responseMode === 'debug')
|
|
713
|
-
})
|
|
714
|
-
} finally { await finishTrace?.() }
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async function executeSwipe(args: ToolCallArgs) {
|
|
718
|
-
const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
|
|
719
|
-
const x1 = requireNumberArg(args, 'x1')
|
|
720
|
-
const y1 = requireNumberArg(args, 'y1')
|
|
721
|
-
const x2 = requireNumberArg(args, 'x2')
|
|
722
|
-
const y2 = requireNumberArg(args, 'y2')
|
|
723
|
-
const duration = requireNumberArg(args, 'duration')
|
|
724
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
725
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
726
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
727
|
-
notifyActionStart()
|
|
728
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }))
|
|
729
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
730
|
-
const result = buildActionExecutionResult({
|
|
731
|
-
actionType: 'swipe',
|
|
732
|
-
sourceModule: 'server',
|
|
733
|
-
selector: { x1, y1, x2, y2, duration },
|
|
734
|
-
success: !!res.success,
|
|
735
|
-
uiFingerprintBefore,
|
|
736
|
-
uiFingerprintAfter,
|
|
737
|
-
failure: res.success ? undefined : inferGenericFailure((res as any).error)
|
|
738
|
-
})
|
|
739
|
-
return wrapResponse(result)
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function executeScrollToElement(args: ToolCallArgs) {
|
|
743
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
744
|
-
const selector = requireObjectArg<ScrollSelectorArg>(args, 'selector')
|
|
745
|
-
const direction = getStringArg(args, 'direction') as 'down' | 'up' | undefined
|
|
746
|
-
const maxScrolls = getNumberArg(args, 'maxScrolls')
|
|
747
|
-
const scrollAmount = getNumberArg(args, 'scrollAmount')
|
|
748
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
749
|
-
const { verificationMode } = interactionControls(args)
|
|
750
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
751
|
-
notifyActionStart()
|
|
752
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }))
|
|
753
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
|
|
754
|
-
const result = buildActionExecutionResult({
|
|
755
|
-
actionType: 'scroll_to_element',
|
|
756
|
-
sourceModule: 'server',
|
|
757
|
-
selector: selector ?? null,
|
|
758
|
-
resolved: res?.success && res?.element ? {
|
|
759
|
-
elementId: null,
|
|
760
|
-
text: (res.element as any).text ?? null,
|
|
761
|
-
resource_id: (res.element as any).resourceId ?? null,
|
|
762
|
-
accessibility_id: (res.element as any).contentDesc ?? null,
|
|
763
|
-
class: (res.element as any).className ?? null,
|
|
764
|
-
bounds: (res.element as any).bounds ?? null,
|
|
765
|
-
index: null
|
|
766
|
-
} : null,
|
|
767
|
-
success: !!res.success,
|
|
768
|
-
uiFingerprintBefore,
|
|
769
|
-
uiFingerprintAfter,
|
|
770
|
-
failure: res.success ? undefined : inferScrollFailure((res as any).reason)
|
|
771
|
-
})
|
|
772
|
-
return wrapResponse(result)
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
async function executeTypeText(args: ToolCallArgs) {
|
|
776
|
-
const text = requireStringArg(args, 'text')
|
|
777
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
778
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
779
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
|
|
780
|
-
notifyActionStart()
|
|
781
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }))
|
|
782
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
|
|
783
|
-
const result = buildActionExecutionResult({
|
|
784
|
-
actionType: 'type_text',
|
|
785
|
-
sourceModule: 'server',
|
|
786
|
-
selector: { text },
|
|
787
|
-
success: !!res.success,
|
|
788
|
-
uiFingerprintBefore,
|
|
789
|
-
uiFingerprintAfter,
|
|
790
|
-
failure: res.success ? undefined : inferGenericFailure((res as any).error)
|
|
791
|
-
})
|
|
792
|
-
return wrapResponse(result)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
async function executePressBack(args: ToolCallArgs) {
|
|
796
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
797
|
-
const { verificationMode, actionTimeoutMs } = interactionControls(args)
|
|
798
|
-
const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
|
|
799
|
-
notifyActionStart()
|
|
800
|
-
const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }))
|
|
801
|
-
const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
|
|
802
|
-
const result = buildActionExecutionResult({
|
|
803
|
-
actionType: 'press_back',
|
|
804
|
-
sourceModule: 'server',
|
|
805
|
-
selector: { key: 'back' },
|
|
806
|
-
success: !!res.success,
|
|
807
|
-
uiFingerprintBefore,
|
|
808
|
-
uiFingerprintAfter,
|
|
809
|
-
failure: res.success ? undefined : inferGenericFailure((res as any).error)
|
|
810
|
-
})
|
|
811
|
-
return wrapResponse(result)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async function handleStartLogStream(args: ToolCallArgs) {
|
|
815
|
-
const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
|
|
816
|
-
const packageName = requireStringArg(args, 'packageName')
|
|
817
|
-
const level = (getStringArg(args, 'level') as 'error' | 'warn' | 'info' | 'debug' | undefined) ?? 'error'
|
|
818
|
-
const sessionId = getStringArg(args, 'sessionId')
|
|
819
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
820
|
-
const res = await ToolsObserve.startLogStreamHandler({ platform, packageName, level, sessionId, deviceId })
|
|
821
|
-
return wrapResponse(res)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
async function handleReadLogStream(args: ToolCallArgs) {
|
|
825
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
826
|
-
const sessionId = getStringArg(args, 'sessionId')
|
|
827
|
-
const limit = getNumberArg(args, 'limit')
|
|
828
|
-
const since = getStringArg(args, 'since')
|
|
829
|
-
const res = await ToolsObserve.readLogStreamHandler({ platform, sessionId, limit, since })
|
|
830
|
-
return wrapResponse(res)
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
async function handleStopLogStream(args: ToolCallArgs) {
|
|
834
|
-
const platform = getStringArg(args, 'platform') as PlatformArg | undefined
|
|
835
|
-
const sessionId = getStringArg(args, 'sessionId')
|
|
836
|
-
const res = await ToolsObserve.stopLogStreamHandler({ platform, sessionId })
|
|
837
|
-
return wrapResponse(res)
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function handleClassifyActionOutcome(args: ToolCallArgs) {
|
|
841
|
-
const uiChanged = requireBooleanArg(args, 'uiChanged')
|
|
842
|
-
const expectedElementVisible = getBooleanArg(args, 'expectedElementVisible')
|
|
843
|
-
const actionType = getStringArg(args, 'actionType')
|
|
844
|
-
const networkRequests = getArrayArg<ClassifyNetworkRequestArg>(args, 'networkRequests')
|
|
845
|
-
const hasLogErrors = getBooleanArg(args, 'hasLogErrors')
|
|
846
|
-
const result = classifyActionOutcome({
|
|
847
|
-
uiChanged,
|
|
848
|
-
expectedElementVisible: expectedElementVisible ?? null,
|
|
849
|
-
actionType: actionType ?? null,
|
|
850
|
-
networkRequests: networkRequests ?? null,
|
|
851
|
-
hasLogErrors: hasLogErrors ?? null
|
|
852
|
-
})
|
|
853
|
-
return Promise.resolve(wrapResponse(result))
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
async function handleGetNetworkActivity(args: ToolCallArgs) {
|
|
857
|
-
const platform = requireStringArg(args, 'platform') as PlatformArg
|
|
858
|
-
const deviceId = getStringArg(args, 'deviceId')
|
|
859
|
-
const result = await ToolsNetwork.getNetworkActivity({ platform, deviceId })
|
|
860
|
-
return wrapResponse(result)
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
async function handleSystemTrace(name: string, args: ToolCallArgs) {
|
|
864
|
-
const schema = name === 'start_system_trace' ? traceStartSchema : name === 'get_system_trace_capabilities' ? traceTargetSchema : traceControlSchema
|
|
865
|
-
const parsed = schema.safeParse(args)
|
|
866
|
-
if (!parsed.success) throw new TraceError('INVALID_ARGUMENT')
|
|
867
|
-
if (name === 'get_system_trace_capabilities' || name === 'start_system_trace') {
|
|
868
|
-
const target = parsed.data as { platform: PlatformArg; deviceId?: string; durationMs?: number }
|
|
869
|
-
if (target.platform !== 'android') {
|
|
870
|
-
if (name === 'start_system_trace') throw new TraceError('UNSUPPORTED_PLATFORM')
|
|
871
|
-
return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: {}, reasons: ['UNSUPPORTED_PLATFORM'], enabled_sources: [], unavailable_categories: [], limitations: ['Android only'] })
|
|
872
|
-
}
|
|
873
|
-
let device
|
|
874
|
-
try { device = await resolveTargetDevice(target) }
|
|
875
|
-
catch {
|
|
876
|
-
if (name === 'start_system_trace') throw new TraceError('TRACE_UNSUPPORTED')
|
|
877
|
-
return wrapResponse({ supported: false, backend: 'perfetto', device: null, prerequisites: { device: false }, reasons: ['DEVICE_UNAVAILABLE'], enabled_sources: [], unavailable_categories: [], limitations: ['An authorised Android device is required'] })
|
|
878
|
-
}
|
|
879
|
-
if (name === 'get_system_trace_capabilities') return wrapResponse(await systemTraces.capabilities(device.id))
|
|
880
|
-
const result = await systemTraces.start(device.id, target.durationMs)
|
|
881
|
-
return { ...wrapResponse(result), ...(result.state === 'failed' ? { isError: true } : {}) }
|
|
882
|
-
}
|
|
883
|
-
const operation = name === 'stop_system_trace' ? 'stop' : name === 'cancel_system_trace' ? 'cancel' : 'status'
|
|
884
|
-
const result = await systemTraces.control((parsed.data as { traceId: string }).traceId, operation)
|
|
885
|
-
return { ...wrapResponse(result), ...(['failed', 'cancelled'].includes(result.state) ? { isError: true } : {}) }
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
export const toolHandlers: Record<string, ToolHandler> = {
|
|
889
|
-
...Object.fromEntries(systemTraceTools.map(({ name }) => [name, (args: ToolCallArgs) => handleSystemTrace(name, args)])),
|
|
890
|
-
get_app_readiness: async args => {
|
|
891
|
-
const parsed = readinessSchema.safeParse(args)
|
|
892
|
-
if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
|
|
893
|
-
return wrapResponse(await getAppReadiness(parsed.data))
|
|
894
|
-
},
|
|
895
|
-
wait_for_app_ready: async args => {
|
|
896
|
-
const parsed = readinessWaitSchema.safeParse(args)
|
|
897
|
-
if (!parsed.success) throw new Error('INVALID_ARGUMENT: invalid readiness arguments')
|
|
898
|
-
return wrapResponse(await getAppReadiness(parsed.data, true))
|
|
899
|
-
},
|
|
900
|
-
start_app: handleStartApp,
|
|
901
|
-
terminate_app: handleTerminateApp,
|
|
902
|
-
restart_app: handleRestartApp,
|
|
903
|
-
reset_app_data: handleResetAppData,
|
|
904
|
-
install_app: handleInstallApp,
|
|
905
|
-
build_app: handleBuildApp,
|
|
906
|
-
build_and_install: handleBuildAndInstall,
|
|
907
|
-
get_logs: handleGetLogs,
|
|
908
|
-
list_devices: handleListDevices,
|
|
909
|
-
get_system_status: handleGetSystemStatus,
|
|
910
|
-
capture_screenshot: handleCaptureScreenshot,
|
|
911
|
-
capture_debug_snapshot: handleCaptureDebugSnapshot,
|
|
912
|
-
get_ui_tree: handleGetUITree,
|
|
913
|
-
get_current_screen: handleGetCurrentScreen,
|
|
914
|
-
get_screen_fingerprint: handleGetScreenFingerprint,
|
|
915
|
-
wait_for_screen_change: handleWaitForScreenChange,
|
|
916
|
-
wait_for_ui_change: handleWaitForUIChange,
|
|
917
|
-
expect_screen: handleExpectScreen,
|
|
918
|
-
expect_element_visible: handleExpectElementVisible,
|
|
919
|
-
expect_state: handleExpectState,
|
|
920
|
-
adjust_control: handleAdjustControl,
|
|
921
|
-
wait_for_ui: handleWaitForUI,
|
|
922
|
-
find_element: handleFindElement,
|
|
923
|
-
tap: handleTap,
|
|
924
|
-
tap_element: handleTapElement,
|
|
925
|
-
run_journey: handleRunJourney,
|
|
926
|
-
swipe: handleSwipe,
|
|
927
|
-
scroll_to_element: handleScrollToElement,
|
|
928
|
-
type_text: handleTypeText,
|
|
929
|
-
press_back: handlePressBack,
|
|
930
|
-
start_log_stream: handleStartLogStream,
|
|
931
|
-
read_log_stream: handleReadLogStream,
|
|
932
|
-
stop_log_stream: handleStopLogStream,
|
|
933
|
-
classify_action_outcome: handleClassifyActionOutcome,
|
|
934
|
-
get_network_activity: handleGetNetworkActivity
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
for (const name of ['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'read_log_stream', 'run_journey', 'adjust_control']) {
|
|
938
|
-
toolHandlers[name] = correlatedHandler(name, toolHandlers[name], name === 'adjust_control', name === 'run_journey')
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
export async function handleToolCall(name: string, args: ToolCallArgs = {}, signal?: AbortSignal) {
|
|
942
|
-
if (Object.hasOwn(debuggerContracts, name)) return handleDebugger(name, args, signal)
|
|
943
|
-
const handler = toolHandlers[name]
|
|
944
|
-
if (!handler) throw new Error(`Unknown tool: ${name}`)
|
|
945
|
-
|
|
946
|
-
const started = performance.now()
|
|
947
|
-
const measured: Record<string, number> = {}
|
|
948
|
-
const addTiming = (response: ToolCallResult) => {
|
|
949
|
-
if (!['start_app', 'restart_app', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'wait_for_ui', 'get_ui_tree', 'capture_screenshot', 'capture_debug_snapshot'].includes(name)) return response
|
|
950
|
-
const item = response.content.find(item => item.type === 'text')
|
|
951
|
-
if (item?.text) {
|
|
952
|
-
try {
|
|
953
|
-
const value = JSON.parse(item.text)
|
|
954
|
-
value.timing = { ...measured, ...value.timing, total_ms: performance.now() - started }
|
|
955
|
-
item.text = JSON.stringify(value)
|
|
956
|
-
} catch { /* Non-JSON text responses retain their original content. */ }
|
|
957
|
-
}
|
|
958
|
-
return response
|
|
959
|
-
}
|
|
960
|
-
return withTiming(measured, async () => {
|
|
961
|
-
try {
|
|
962
|
-
if (!systemTraceToolNames.has(name)) validateExpectedState(name, args)
|
|
963
|
-
const guarded = ['start_app', 'terminate_app', 'restart_app', 'reset_app_data', 'install_app', 'build_and_install', 'tap', 'tap_element', 'swipe', 'scroll_to_element', 'type_text', 'press_back', 'adjust_control', 'run_journey', 'wait_for_ui', 'wait_for_ui_change', 'wait_for_screen_change', 'expect_screen', 'expect_element_visible', 'expect_state'].includes(name)
|
|
964
|
-
if (guarded && args.platform !== 'ios' && debuggerSessions.hasActiveSessions()) {
|
|
965
|
-
const device = await resolveTargetDevice({ platform: 'android', deviceId: args.deviceId as string | undefined })
|
|
966
|
-
return withResolvedDevice(device, () => withDebuggerGuard('android', device.id, async () => {
|
|
967
|
-
const before = debuggerSessions.paused(device.id)
|
|
968
|
-
if (before) return wrapResponse({ success: false, failure_code: 'DEBUGGER_PAUSED', delivery_status: 'not_delivered', dispatch_started: false, debugger: before })
|
|
969
|
-
let response: ToolCallResult
|
|
970
|
-
try { response = await handler(args) } catch (error) {
|
|
971
|
-
if (!debuggerSessions.paused(device.id)) throw error
|
|
972
|
-
response = wrapResponse({ success: false })
|
|
973
|
-
}
|
|
974
|
-
const pause = debuggerSessions.paused(device.id)
|
|
975
|
-
if (pause) {
|
|
976
|
-
const item = response.content.find(item => item.type === 'text')
|
|
977
|
-
if (item?.text) {
|
|
978
|
-
const payload = JSON.parse(item.text)
|
|
979
|
-
Object.assign(payload, { success: false, failure_code: 'DEBUGGER_PAUSED', retryable: false,
|
|
980
|
-
dispatch_started: guardDispatched(), delivery_status: payload.success ? 'delivered' : guardDispatched() ? 'unknown' : 'not_delivered',
|
|
981
|
-
verification: { status: 'blocked', reason: 'DEBUGGER_PAUSED' }, debugger: pause })
|
|
982
|
-
item.text = JSON.stringify(payload)
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
return addTiming(response)
|
|
986
|
-
}))
|
|
987
|
-
}
|
|
988
|
-
const response = await handler(args)
|
|
989
|
-
if (['capture_screenshot', 'get_ui_tree', 'get_screen_fingerprint', 'capture_debug_snapshot', 'get_logs', 'get_current_screen', 'read_log_stream'].includes(name) && args.platform !== 'ios') {
|
|
990
|
-
const item = response.content.find(item => item.type === 'text')
|
|
991
|
-
if (item?.text) {
|
|
992
|
-
try {
|
|
993
|
-
const payload = JSON.parse(item.text)
|
|
994
|
-
const device = payload.device ?? payload.evidence?.device
|
|
995
|
-
const id = device?.id ?? args.deviceId
|
|
996
|
-
const pause = id && device?.platform !== 'ios' ? debuggerSessions.paused(String(id)) : null
|
|
997
|
-
if (pause) item.text = JSON.stringify({ ...payload, debugger: pause })
|
|
998
|
-
} catch { /* Non-JSON observation */ }
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return addTiming(response)
|
|
1002
|
-
} catch (error) {
|
|
1003
|
-
if (error instanceof TraceError) return { ...wrapResponse({ error: { tool: name, code: error.code, message: error.message } }), isError: true }
|
|
1004
|
-
console.error(`Error executing tool ${name}:`, error)
|
|
1005
|
-
return addTiming(wrapToolError(name, error))
|
|
1006
|
-
}
|
|
1007
|
-
})
|
|
1008
|
-
}
|