mobile-debug-mcp 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3090
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +4 -2
- package/dist/observe/readiness.js +29 -10
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace.js +22 -13
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3492
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +5 -3
- package/src/observe/readiness.ts +39 -18
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace.ts +24 -15
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
package/src/observe/ios.ts
CHANGED
|
@@ -1,299 +1,14 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
1
|
import { captureEvidence } from './evidence.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { computeScreenFingerprint } from '../utils/ui/index.js'
|
|
14
|
-
import { parsePngSize } from '../utils/image.js'
|
|
15
|
-
import { deriveSnapshotMetadata } from './snapshot-metadata.js'
|
|
16
|
-
|
|
17
|
-
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
-
let iosExecCommand = execCommand
|
|
19
|
-
|
|
20
|
-
export function _setIOSExecCommandForTests(fn: typeof execCommand) {
|
|
21
|
-
iosExecCommand = fn
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function _resetIOSExecCommandForTests() {
|
|
25
|
-
iosExecCommand = execCommand
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface IDBElement {
|
|
29
|
-
AXFrame?: { x: number | string, y: number | string, width: number | string, height: number | string, w?: number | string, h?: number | string };
|
|
30
|
-
frame?: { x: number | string, y: number | string, width: number | string, height: number | string, w?: number | string, h?: number | string };
|
|
31
|
-
AXIdentifier?: string;
|
|
32
|
-
accessibilityIdentifier?: string;
|
|
33
|
-
identifier?: string;
|
|
34
|
-
AXUniqueId?: string;
|
|
35
|
-
AXLabel?: string;
|
|
36
|
-
AXValue?: string;
|
|
37
|
-
AXTraits?: string[];
|
|
38
|
-
AXElementType?: string;
|
|
39
|
-
type?: string;
|
|
40
|
-
label?: string;
|
|
41
|
-
children?: IDBElement[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseIDBFrame(frame: any): [number, number, number, number] {
|
|
45
|
-
if (!frame) return [0, 0, 0, 0];
|
|
46
|
-
if (typeof frame === 'string') {
|
|
47
|
-
const nums = frame.match(/-?\d+(?:\.\d+)?/g);
|
|
48
|
-
if (!nums || nums.length < 4) return [0, 0, 0, 0];
|
|
49
|
-
const x = Number(nums[0]);
|
|
50
|
-
const y = Number(nums[1]);
|
|
51
|
-
const w = Number(nums[2]);
|
|
52
|
-
const h = Number(nums[3]);
|
|
53
|
-
return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const x = Number(frame.x || 0);
|
|
57
|
-
const y = Number(frame.y || 0);
|
|
58
|
-
const w = Number(frame.width || frame.w || 0);
|
|
59
|
-
const h = Number(frame.height || frame.h || 0);
|
|
60
|
-
return [Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h)];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getCenter(bounds: [number, number, number, number]): [number, number] {
|
|
64
|
-
const [x1, y1, x2, y2] = bounds;
|
|
65
|
-
return [Math.floor((x1 + x2) / 2), Math.floor((y1 + y2) / 2)];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseIOSNumber(value: unknown): number | null {
|
|
69
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
70
|
-
if (typeof value !== 'string') return null
|
|
71
|
-
const parsed = Number(value)
|
|
72
|
-
return Number.isFinite(parsed) ? parsed : null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeIOSType(value: unknown): string {
|
|
76
|
-
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function inferIOSRole(type: string, traits: string[]): string | null {
|
|
80
|
-
if (/slider|adjustable/.test(type) || traits.some((trait) => /adjustable|slider/.test(trait))) return 'slider'
|
|
81
|
-
if (/stepper/.test(type)) return 'stepper'
|
|
82
|
-
if (/picker|pop up button|dropdown/.test(type)) return 'dropdown'
|
|
83
|
-
if (/segmented control/.test(type)) return 'segmented_control'
|
|
84
|
-
if (/button/.test(type) || traits.some((trait) => /button/.test(trait))) return 'button'
|
|
85
|
-
if (/cell/.test(type)) return 'cell'
|
|
86
|
-
if (/switch/.test(type)) return 'switch'
|
|
87
|
-
if (/text field|textfield|search field/.test(type)) return 'text_field'
|
|
88
|
-
if (/image/.test(type)) return 'image'
|
|
89
|
-
if (/window|application|group|scroll view|collection view/.test(type)) return 'container'
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function getIOSStableId(node: IDBElement): string | null {
|
|
94
|
-
const candidates = [node.AXIdentifier, node.accessibilityIdentifier, node.identifier, node.AXUniqueId]
|
|
95
|
-
for (const candidate of candidates) {
|
|
96
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate
|
|
97
|
-
}
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function buildIOSSelectorConfidence(source: 'identifier' | 'label' | 'value' | 'type' | 'none'): SelectorConfidence | null {
|
|
102
|
-
switch (source) {
|
|
103
|
-
case 'identifier':
|
|
104
|
-
return { score: 1, reason: 'accessibility_identifier' }
|
|
105
|
-
case 'label':
|
|
106
|
-
return { score: 0.9, reason: 'label_match' }
|
|
107
|
-
case 'value':
|
|
108
|
-
return { score: 0.75, reason: 'value_match' }
|
|
109
|
-
case 'type':
|
|
110
|
-
return { score: 0.35, reason: 'type_match' }
|
|
111
|
-
default:
|
|
112
|
-
return null
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function buildIOSSelector(type: string, label: string | null, value: string | null, stableId: string | null): UIResolutionSelector | null {
|
|
117
|
-
if (stableId) return { value: stableId, confidence: buildIOSSelectorConfidence('identifier') }
|
|
118
|
-
if (label) return { value: label, confidence: buildIOSSelectorConfidence('label') }
|
|
119
|
-
if (value) return { value: value, confidence: buildIOSSelectorConfidence('value') }
|
|
120
|
-
if (type) return { value: type, confidence: buildIOSSelectorConfidence('type') }
|
|
121
|
-
return null
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function buildIOSSemantic(type: string, traits: string[], role: string | null, value: string | null): UIElementSemanticMetadata {
|
|
125
|
-
const semantic: UIElementSemanticMetadata = {
|
|
126
|
-
is_clickable: traits.includes("UIAccessibilityTraitButton") || /adjustable|slider/.test(type) || type === "Button" || type === "Cell",
|
|
127
|
-
is_container: /window|application|group|scroll view|collection view/.test(type)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (role === 'slider') {
|
|
131
|
-
semantic.semantic_role = 'slider'
|
|
132
|
-
semantic.adjustable = true
|
|
133
|
-
semantic.supported_actions = ['adjust']
|
|
134
|
-
semantic.state_shape = 'continuous'
|
|
135
|
-
} else if (role === 'stepper') {
|
|
136
|
-
semantic.semantic_role = 'stepper'
|
|
137
|
-
semantic.adjustable = true
|
|
138
|
-
semantic.supported_actions = ['increment', 'decrement']
|
|
139
|
-
semantic.state_shape = 'discrete'
|
|
140
|
-
} else if (role === 'dropdown') {
|
|
141
|
-
semantic.semantic_role = 'dropdown'
|
|
142
|
-
semantic.supported_actions = ['tap', 'expand']
|
|
143
|
-
semantic.state_shape = 'semantic'
|
|
144
|
-
} else if (role === 'segmented_control') {
|
|
145
|
-
semantic.semantic_role = 'segmented_control'
|
|
146
|
-
semantic.supported_actions = ['tap']
|
|
147
|
-
semantic.state_shape = 'discrete'
|
|
148
|
-
} else if (traits.some((trait) => /adjustable|slider/i.test(trait)) || /adjustable|slider/.test(type)) {
|
|
149
|
-
semantic.semantic_role = 'custom_adjustable'
|
|
150
|
-
semantic.adjustable = true
|
|
151
|
-
semantic.supported_actions = ['adjust']
|
|
152
|
-
semantic.state_shape = 'continuous'
|
|
153
|
-
} else if (semantic.is_clickable) {
|
|
154
|
-
semantic.supported_actions = ['tap']
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (semantic.state_shape === undefined && semantic.adjustable && value !== null) {
|
|
158
|
-
const numericValue = parseIOSNumber(value)
|
|
159
|
-
if (numericValue !== null && numericValue >= 0 && numericValue <= 1) {
|
|
160
|
-
semantic.state_shape = 'continuous'
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return semantic
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isIOSAdjustable(node: IDBElement, type: string, traits: string[]): boolean {
|
|
168
|
-
return /slider|adjustable|stepper/i.test(type) || traits.some((trait) => /adjustable|slider/i.test(trait))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function extractIOSState(node: IDBElement, type: string, label: string | null, value: string | null, traits: string[]): UIElementState | null {
|
|
172
|
-
const state: UIElementState = {}
|
|
173
|
-
const normalizedTraits = traits.map((trait) => String(trait).toLowerCase())
|
|
174
|
-
|
|
175
|
-
if (normalizedTraits.some((trait) => /selected/.test(trait))) {
|
|
176
|
-
state.selected = label || value || true
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (normalizedTraits.some((trait) => /focused/.test(trait))) {
|
|
180
|
-
state.focused = true
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (normalizedTraits.some((trait) => /enabled/.test(trait))) {
|
|
184
|
-
state.enabled = true
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (normalizedTraits.some((trait) => /disabled/.test(trait))) {
|
|
188
|
-
state.enabled = false
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (value && /textfield|search|text/i.test(type)) {
|
|
192
|
-
state.text_value = value
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (isIOSAdjustable(node, type, traits)) {
|
|
196
|
-
const rawValue = parseIOSNumber(value)
|
|
197
|
-
if (rawValue !== null) {
|
|
198
|
-
state.raw_value = rawValue
|
|
199
|
-
state.value = rawValue >= 0 && rawValue <= 1 ? Math.round(rawValue * 100) : rawValue
|
|
200
|
-
} else if (value) {
|
|
201
|
-
state.raw_value = value
|
|
202
|
-
state.value = value
|
|
203
|
-
}
|
|
204
|
-
} else if (value) {
|
|
205
|
-
const numericValue = parseIOSNumber(value)
|
|
206
|
-
if (numericValue !== null) {
|
|
207
|
-
state.value = numericValue
|
|
208
|
-
state.raw_value = numericValue
|
|
209
|
-
} else {
|
|
210
|
-
state.value = value
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return Object.keys(state).length > 0 ? state : null
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function traverseIDBNode(node: IDBElement, elements: UIElement[], parentIndex: number = -1, depth: number = 0): number {
|
|
218
|
-
if (!node) return -1;
|
|
219
|
-
|
|
220
|
-
let currentIndex = -1;
|
|
221
|
-
|
|
222
|
-
const type = node.AXElementType || node.type || "unknown";
|
|
223
|
-
const label = node.AXLabel || node.label || null;
|
|
224
|
-
const value = node.AXValue || null;
|
|
225
|
-
const frame = node.AXFrame || node.frame;
|
|
226
|
-
const traits = node.AXTraits || [];
|
|
227
|
-
const state = extractIOSState(node, type, label, value, traits);
|
|
228
|
-
const normalizedType = normalizeIOSType(type)
|
|
229
|
-
const stableId = getIOSStableId(node)
|
|
230
|
-
const selector = buildIOSSelector(type, label, value, stableId)
|
|
231
|
-
const role = inferIOSRole(normalizedType, traits)
|
|
232
|
-
const semantic = buildIOSSemantic(normalizedType, traits, role, value)
|
|
233
|
-
|
|
234
|
-
const clickable = traits.includes("UIAccessibilityTraitButton") || type === "Button" || type === "Cell";
|
|
235
|
-
|
|
236
|
-
const isUseful = clickable || (label && label.length > 0) || (value && value.length > 0) || type === "Application" || type === "Window";
|
|
237
|
-
|
|
238
|
-
if (isUseful) {
|
|
239
|
-
const bounds = parseIDBFrame(frame);
|
|
240
|
-
const element: UIElement = {
|
|
241
|
-
text: label,
|
|
242
|
-
contentDescription: value,
|
|
243
|
-
type: type,
|
|
244
|
-
resourceId: stableId,
|
|
245
|
-
clickable: clickable,
|
|
246
|
-
enabled: true,
|
|
247
|
-
visible: true,
|
|
248
|
-
bounds: bounds,
|
|
249
|
-
center: getCenter(bounds),
|
|
250
|
-
depth: depth,
|
|
251
|
-
state,
|
|
252
|
-
stable_id: stableId,
|
|
253
|
-
role,
|
|
254
|
-
test_tag: stableId,
|
|
255
|
-
selector,
|
|
256
|
-
semantic
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
if (parentIndex !== -1) {
|
|
260
|
-
element.parentId = parentIndex;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
elements.push(element);
|
|
264
|
-
currentIndex = elements.length - 1;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const nextParentIndex = currentIndex !== -1 ? currentIndex : parentIndex;
|
|
268
|
-
const nextDepth = currentIndex !== -1 ? depth + 1 : depth;
|
|
269
|
-
|
|
270
|
-
const childrenIndices: number[] = [];
|
|
271
|
-
|
|
272
|
-
if (node.children && Array.isArray(node.children)) {
|
|
273
|
-
for (const child of node.children) {
|
|
274
|
-
const childIndex = traverseIDBNode(child, elements, nextParentIndex, nextDepth);
|
|
275
|
-
if (childIndex !== -1) {
|
|
276
|
-
childrenIndices.push(childIndex);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (currentIndex !== -1 && childrenIndices.length > 0) {
|
|
282
|
-
elements[currentIndex].children = childrenIndices;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return currentIndex;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const iosActiveLogStreams: Map<string, { proc: ReturnType<typeof import('child_process').spawn>, file: string, device?: { platform: 'android' | 'ios'; id: string } }> = new Map()
|
|
289
|
-
|
|
290
|
-
export function _setIOSActiveLogStream(sessionId: string, file: string, device?: { platform: 'android' | 'ios'; id: string }) {
|
|
291
|
-
iosActiveLogStreams.set(sessionId, { proc: {} as any, file, device })
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function _clearIOSActiveLogStream(sessionId: string) {
|
|
295
|
-
iosActiveLogStreams.delete(sessionId)
|
|
296
|
-
}
|
|
2
|
+
import { GetLogsResponse, CaptureIOSScreenshotResponse, GetUITreeResponse, DeviceInfo } from "../types.js"
|
|
3
|
+
import { getIOSDeviceMetadata } from "../utils/ios/utils.js"
|
|
4
|
+
import { iosActiveLogStreams } from './ios/context.js'
|
|
5
|
+
import { collectGetUITree } from './ios/ui-tree.js'
|
|
6
|
+
import { collectGetLogs } from './ios/logs.js'
|
|
7
|
+
import { collectCaptureScreenshot } from './ios/screenshot.js'
|
|
8
|
+
import { collectGetScreenFingerprint } from './ios/fingerprint.js'
|
|
9
|
+
import { startLogStream, stopLogStream, readLogStream, collectReadLogStream } from './ios/log-stream.js'
|
|
10
|
+
export { traverseIDBNode } from './ios/ui-elements.js'
|
|
11
|
+
export { _setIOSExecCommandForTests, _resetIOSExecCommandForTests, _setIOSActiveLogStream, _clearIOSActiveLogStream } from './ios/context.js'
|
|
297
12
|
|
|
298
13
|
export class iOSObserve {
|
|
299
14
|
getLogStreamDevice(sessionId: string = 'default') { return iosActiveLogStreams.get(sessionId)?.device ?? null }
|
|
@@ -306,477 +21,31 @@ export class iOSObserve {
|
|
|
306
21
|
return captureEvidence('logs', { platform: 'ios', id: filters.deviceId ?? '' }, () => this.collectGetLogs(filters))
|
|
307
22
|
}
|
|
308
23
|
|
|
309
|
-
private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> {
|
|
310
|
-
const { appId, deviceId = 'booted', pid, tag, level, contains, since_seconds, limit } = filters
|
|
311
|
-
const args: string[] = ['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog']
|
|
312
|
-
|
|
313
|
-
// Default to last N seconds if no since_seconds provided; limit lines handled after parsing
|
|
314
|
-
const effectiveLimit = typeof limit === 'number' && limit > 0 ? limit : 50
|
|
315
|
-
|
|
316
|
-
if (since_seconds) {
|
|
317
|
-
// log show accepts --last <time>
|
|
318
|
-
args.push('--last', `${since_seconds}s`)
|
|
319
|
-
} else {
|
|
320
|
-
// default to last 60s to keep quick
|
|
321
|
-
args.push('--last', '60s')
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
let processNameUsed: string | undefined = undefined
|
|
325
|
-
if (appId) {
|
|
326
|
-
validateBundleId(appId)
|
|
327
|
-
// prefer matching the simple process name (last segment of bundle id), but also match full bundle id in subsystem
|
|
328
|
-
const parts = appId.split('.')
|
|
329
|
-
const simpleName = parts[parts.length - 1]
|
|
330
|
-
processNameUsed = simpleName
|
|
331
|
-
// predicate: match process by simple name or full bundle id, or subsystem contains bundle id
|
|
332
|
-
args.push('--predicate', `process == "${simpleName}" or process == "${appId}" or subsystem contains "${appId}"`)
|
|
333
|
-
} else if (tag) {
|
|
334
|
-
// predicate by subsystem/category
|
|
335
|
-
args.push('--predicate', `subsystem contains "${tag}"`)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
// Attempt pid detection if appId provided and no explicit pid supplied — prefer process name derived from bundle id
|
|
340
|
-
let detectedPid: number | null = null
|
|
341
|
-
if (appId && !pid) {
|
|
342
|
-
const parts = appId.split('.')
|
|
343
|
-
const simpleName = parts[parts.length - 1]
|
|
344
|
-
try {
|
|
345
|
-
const pgrepRes = await iosExecCommand(['simctl','spawn', deviceId, 'pgrep', '-f', simpleName], deviceId)
|
|
346
|
-
const out = pgrepRes && pgrepRes.output ? pgrepRes.output.trim() : ''
|
|
347
|
-
const firstLine = out.split(/\r?\n/).find(Boolean)
|
|
348
|
-
if (firstLine) {
|
|
349
|
-
const n = Number(firstLine.trim())
|
|
350
|
-
if (!isNaN(n) && n > 0) detectedPid = n
|
|
351
|
-
}
|
|
352
|
-
} catch {
|
|
353
|
-
// ignore pgrep failures — we'll fall back to process/bundle matching
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
const effectivePid = pid || detectedPid || null
|
|
357
|
-
const result = await iosExecCommand(args, deviceId)
|
|
358
|
-
const device = await getIOSDeviceMetadata(deviceId)
|
|
359
|
-
const rawLines = result.output ? result.output.split(/\r?\n/).filter(Boolean) : []
|
|
360
|
-
|
|
361
|
-
// Parse lines into structured entries. iOS log format: timestamp [PID:tid] <level> subsystem:category: message
|
|
362
|
-
const parsed = rawLines.map(line => {
|
|
363
|
-
// Example: 2023-08-12 12:34:56.789012+0000 pid <Debug> MyApp[123:456] <info> MySubsystem: MyCategory: Message here
|
|
364
|
-
// Simpler approach: try to extract ISO timestamp at start
|
|
365
|
-
let ts: string | null = null
|
|
366
|
-
const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)/)
|
|
367
|
-
if (tsMatch) {
|
|
368
|
-
const d = new Date(tsMatch[1])
|
|
369
|
-
if (!isNaN(d.getTime())) ts = d.toISOString()
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// level mapping
|
|
373
|
-
let lvl = 'INFO'
|
|
374
|
-
const lvlMatch = line.match(/\b(Debug|Info|Default|Error|Fault|Warning)\b/i)
|
|
375
|
-
if (lvlMatch) {
|
|
376
|
-
const map: Record<string, string> = { 'debug': 'DEBUG', 'info': 'INFO', 'default': 'DEBUG', 'error': 'ERROR', 'fault': 'ERROR', 'warning': 'WARN' }
|
|
377
|
-
lvl = map[(lvlMatch[1] || '').toLowerCase()] || 'INFO'
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// subsystem/category -> tag
|
|
381
|
-
let tagVal = ''
|
|
382
|
-
const tagMatch = line.match(/\s([A-Za-z0-9_./-]+):\s/)
|
|
383
|
-
if (tagMatch) tagVal = tagMatch[1]
|
|
384
|
-
|
|
385
|
-
// pid extraction
|
|
386
|
-
let pidNum: number | null = null
|
|
387
|
-
const pidMatch = line.match(/\[(\d+):\d+\]/)
|
|
388
|
-
if (pidMatch) pidNum = Number(pidMatch[1])
|
|
389
|
-
|
|
390
|
-
// message: extract robustly after the subsystem/category token when available
|
|
391
|
-
let message = line
|
|
392
|
-
if (tagMatch) {
|
|
393
|
-
// tagMatch[0] includes the delimiter (e.g. " MySubsystem: ") — use it to find the message start
|
|
394
|
-
const marker = tagMatch[0]
|
|
395
|
-
const idx = line.indexOf(marker)
|
|
396
|
-
if (idx !== -1) {
|
|
397
|
-
message = line.slice(idx + marker.length).trim()
|
|
398
|
-
} else {
|
|
399
|
-
// fallback: try to trim off common prefixes (timestamp, pid, level) and keep the rest
|
|
400
|
-
const afterPidMatch = line.match(/\]\s+/)
|
|
401
|
-
if (afterPidMatch) {
|
|
402
|
-
const afterPidIdx = line.indexOf(afterPidMatch[0]) + afterPidMatch[0].length
|
|
403
|
-
message = line.slice(afterPidIdx).trim()
|
|
404
|
-
} else {
|
|
405
|
-
// remove leading level tokens like <Debug> and keep remainder
|
|
406
|
-
message = line.replace(/^.*?<.*?>\s*/,'').trim()
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
} else {
|
|
410
|
-
// No tag found — strip obvious prefixes and keep remainder (preserve colons in message)
|
|
411
|
-
message = line.replace(/^.*?<.*?>\s*/,'').trim()
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return { timestamp: ts, level: lvl, tag: tagVal, pid: pidNum, message }
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
// Apply contains filter
|
|
418
|
-
let filtered = parsed
|
|
419
|
-
if (contains) filtered = filtered.filter(e => e.message && e.message.includes(contains))
|
|
420
|
-
|
|
421
|
-
// Apply since_seconds already applied by log show, but double-check timestamps
|
|
422
|
-
if (since_seconds) {
|
|
423
|
-
const sinceMs = Date.now() - (since_seconds * 1000)
|
|
424
|
-
filtered = filtered.filter(e => e.timestamp && (new Date(e.timestamp).getTime() >= sinceMs))
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// level filter
|
|
428
|
-
if (level) {
|
|
429
|
-
const L = level.toUpperCase()
|
|
430
|
-
filtered = filtered.filter(e => e.level && e.level.toUpperCase() === L)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// tag filter
|
|
434
|
-
if (tag) filtered = filtered.filter(e => e.tag && e.tag.includes(tag))
|
|
435
|
-
|
|
436
|
-
// pid filter (use detected/effective pid if available)
|
|
437
|
-
const pidToFilter = effectivePid
|
|
438
|
-
if (pidToFilter) filtered = filtered.filter(e => e.pid === pidToFilter)
|
|
439
|
-
|
|
440
|
-
// If appId present but no predicate returned lines, try substring match
|
|
441
|
-
if (appId && filtered.length === 0) {
|
|
442
|
-
const matched = parsed.filter(e => (e.message && e.message.includes(appId)) || (e.tag && e.tag.includes(appId)) || (e.message && processNameUsed && e.message.includes(processNameUsed)))
|
|
443
|
-
if (matched.length > 0) filtered = matched
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Order oldest -> newest
|
|
447
|
-
filtered.sort((a,b) => {
|
|
448
|
-
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0
|
|
449
|
-
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0
|
|
450
|
-
return ta - tb
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
const source = pidToFilter ? 'pid' : (appId ? 'process' : 'broad')
|
|
454
|
-
const meta = { appIdProvided: !!appId, processNameUsed: processNameUsed || null, detectedPid: detectedPid || null, filters: { tag, level, contains, since_seconds, limit: effectiveLimit }, pidExplicit: !!pid }
|
|
455
|
-
const limited = filtered.slice(-Math.max(0, effectiveLimit))
|
|
456
|
-
return { device, logs: limited, logCount: limited.length, source, meta }
|
|
457
|
-
} catch (err) {
|
|
458
|
-
console.error('iOS getLogs failed:', err)
|
|
459
|
-
const device = await getIOSDeviceMetadata(deviceId)
|
|
460
|
-
return { device, logs: [], logCount: 0, source: 'broad', meta: { error: err instanceof Error ? err.message : String(err) } }
|
|
461
|
-
}
|
|
462
|
-
}
|
|
24
|
+
private async collectGetLogs(filters: { appId?: string, deviceId?: string, pid?: number, tag?: string, level?: string, contains?: string, since_seconds?: number, limit?: number } = {}): Promise<GetLogsResponse> { return collectGetLogs(filters) }
|
|
463
25
|
|
|
464
26
|
async captureScreenshot(deviceId: string = "booted"): Promise<CaptureIOSScreenshotResponse> {
|
|
465
27
|
return captureEvidence('screenshot', { platform: 'ios', id: deviceId ?? '' }, () => this.collectCaptureScreenshot(deviceId))
|
|
466
28
|
}
|
|
467
29
|
|
|
468
|
-
private async collectCaptureScreenshot(deviceId: string = "booted"): Promise<CaptureIOSScreenshotResponse> {
|
|
469
|
-
const device = await getIOSDeviceMetadata(deviceId)
|
|
470
|
-
const tmpFile = `/tmp/mcp-ios-screenshot-${randomUUID()}.png`
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
await iosExecCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId)
|
|
474
|
-
|
|
475
|
-
const buffer = await fs.readFile(tmpFile)
|
|
476
|
-
const base64 = buffer.toString('base64')
|
|
477
|
-
|
|
478
|
-
const dims = parsePngSize(buffer)
|
|
479
|
-
|
|
480
|
-
// Try to generate WebP (preferred) and JPEG fallback using sharp (in-process, cross-platform)
|
|
481
|
-
try {
|
|
482
|
-
const sharpModule = await import('sharp'); const sharp = sharpModule && (sharpModule as any).default ? (sharpModule as any).default : sharpModule;
|
|
483
|
-
const img = sharp(buffer);
|
|
484
|
-
const meta = await img.metadata().catch((err: any) => { console.error('sharp.metadata failed:', err); return {} as any });
|
|
485
|
-
|
|
486
|
-
// If image has alpha channel, prefer lossless PNG to preserve transparency
|
|
487
|
-
const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
|
|
488
|
-
|
|
489
|
-
// Generate WebP and JPEG buffers; log failures
|
|
490
|
-
let webpBuf: Buffer | null = null;
|
|
491
|
-
let jpegBuf: Buffer | null = null;
|
|
492
|
-
try {
|
|
493
|
-
webpBuf = await img.webp({ quality: 80 }).toBuffer();
|
|
494
|
-
} catch (err) {
|
|
495
|
-
console.error('WebP conversion failed (iOS):', err instanceof Error ? err.message : String(err));
|
|
496
|
-
webpBuf = null;
|
|
497
|
-
}
|
|
498
|
-
try {
|
|
499
|
-
jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
|
|
500
|
-
} catch (err) {
|
|
501
|
-
console.error('JPEG conversion failed (iOS):', err instanceof Error ? err.message : String(err));
|
|
502
|
-
jpegBuf = null;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
await fs.rm(tmpFile).catch(() => {});
|
|
506
|
-
|
|
507
|
-
if (hasAlpha) {
|
|
508
|
-
// preserve alpha: return PNG if WebP not available
|
|
509
|
-
if (webpBuf) {
|
|
510
|
-
return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: base64, screenshot_fallback_mime: 'image/png', resolution: { width: dims.width, height: dims.height } }
|
|
511
|
-
}
|
|
512
|
-
// if webp unavailable, return original PNG
|
|
513
|
-
return { device, screenshot: base64, screenshot_mime: 'image/png', resolution: { width: dims.width, height: dims.height } }
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// No alpha: prefer webp, fall back to jpeg
|
|
517
|
-
if (webpBuf) {
|
|
518
|
-
return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: jpegBuf ? jpegBuf.toString('base64') : undefined, screenshot_fallback_mime: jpegBuf ? 'image/jpeg' : undefined, resolution: { width: dims.width, height: dims.height } }
|
|
519
|
-
}
|
|
520
|
-
if (jpegBuf) {
|
|
521
|
-
return { device, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: dims.width, height: dims.height } }
|
|
522
|
-
}
|
|
523
|
-
} catch (err) {
|
|
524
|
-
console.error('Screenshot conversion pipeline failed (iOS):', err instanceof Error ? err.message : String(err));
|
|
525
|
-
// fall through to png fallback
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
await fs.rm(tmpFile).catch(() => {})
|
|
529
|
-
return {
|
|
530
|
-
device,
|
|
531
|
-
screenshot: base64,
|
|
532
|
-
screenshot_mime: 'image/png',
|
|
533
|
-
resolution: { width: dims.width, height: dims.height },
|
|
534
|
-
}
|
|
535
|
-
} catch (e) {
|
|
536
|
-
await fs.rm(tmpFile).catch(() => {})
|
|
537
|
-
throw new Error(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
30
|
+
private async collectCaptureScreenshot(deviceId: string = "booted"): Promise<CaptureIOSScreenshotResponse> { return collectCaptureScreenshot(deviceId) }
|
|
540
31
|
|
|
541
32
|
async getUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> {
|
|
542
33
|
return captureEvidence('ui_tree', { platform: 'ios', id: deviceId ?? '' }, () => this.collectGetUITree(deviceId))
|
|
543
34
|
}
|
|
544
35
|
|
|
545
|
-
private async collectGetUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> {
|
|
546
|
-
const measurement = createObservationMeasurement('ios')
|
|
547
|
-
let device
|
|
548
|
-
try {
|
|
549
|
-
device = await measurement.timed('metadata_ms', () => getIOSDeviceMetadata(deviceId))
|
|
550
|
-
} catch (error) {
|
|
551
|
-
measurement.setOutcome('unavailable')
|
|
552
|
-
measurement.finish()
|
|
553
|
-
throw error
|
|
554
|
-
}
|
|
555
|
-
const deviceKey = `ios:${device.id}`
|
|
556
|
-
|
|
557
|
-
const idbExists = await measurement.timed('capability_check_ms', () => isIDBInstalled());
|
|
558
|
-
if (!idbExists) {
|
|
559
|
-
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
560
|
-
measurement.setOutcome('unavailable')
|
|
561
|
-
measurement.finish()
|
|
562
|
-
return {
|
|
563
|
-
device,
|
|
564
|
-
screen: "",
|
|
565
|
-
resolution: { width: 0, height: 0 },
|
|
566
|
-
elements: [],
|
|
567
|
-
...snapshotMetadata,
|
|
568
|
-
error: "iOS UI tree retrieval requires 'idb' (iOS Device Bridge). Please install it via Homebrew: `brew tap facebook/fb && brew install idb-companion` and `pip3 install fb-idb`."
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
|
|
573
|
-
|
|
574
|
-
let jsonContent: any = null;
|
|
575
|
-
let attempts = 0;
|
|
576
|
-
const maxAttempts = 3;
|
|
577
|
-
|
|
578
|
-
while (attempts < maxAttempts) {
|
|
579
|
-
remainingBudget()
|
|
580
|
-
attempts++;
|
|
581
|
-
try {
|
|
582
|
-
await measurement.timed('observation_wait_ms', () => delay(Math.min(remainingBudget(300 + (attempts * 100)), 300 + (attempts * 100))));
|
|
583
|
-
remainingBudget()
|
|
584
|
-
|
|
585
|
-
const args = ['ui', 'describe-all', '--json'];
|
|
586
|
-
if (targetUdid) {
|
|
587
|
-
args.push('--udid', targetUdid);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const output = await measurement.physicalRead(async () => {
|
|
591
|
-
const result = await execCmd(getIdbCmd(), args, { timeout: remainingBudget() })
|
|
592
|
-
if (result.stderr === 'ACTION_TIMEOUT') throw new Error('ACTION_TIMEOUT')
|
|
593
|
-
if (result.exitCode !== 0) throw new Error(`idb failed (code ${result.exitCode}): ${result.stderr}`)
|
|
594
|
-
if (!result.stdout.trim()) throw new Error('Empty idb UI tree output')
|
|
595
|
-
return result.stdout
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
if (output && output.trim().length > 0) {
|
|
599
|
-
jsonContent = await measurement.timed('parse_ms', async () => JSON.parse(output));
|
|
600
|
-
break; // Success
|
|
601
|
-
}
|
|
602
|
-
} catch (e) {
|
|
603
|
-
if (e instanceof Error && e.message === 'ACTION_TIMEOUT') throw e
|
|
604
|
-
console.error(`Attempt ${attempts} failed: ${e}`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (attempts === maxAttempts) {
|
|
608
|
-
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
609
|
-
measurement.setOutcome('unavailable')
|
|
610
|
-
measurement.finish()
|
|
611
|
-
return {
|
|
612
|
-
device,
|
|
613
|
-
screen: "",
|
|
614
|
-
resolution: { width: 0, height: 0 },
|
|
615
|
-
elements: [],
|
|
616
|
-
...snapshotMetadata,
|
|
617
|
-
error: `Failed to retrieve valid UI dump after ${maxAttempts} attempts.`
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
const elements: UIElement[] = await measurement.timed('conversion_ms', async () => {
|
|
624
|
-
const parsed: UIElement[] = []
|
|
625
|
-
if (Array.isArray(jsonContent)) {
|
|
626
|
-
for (const node of jsonContent) traverseIDBNode(node, parsed)
|
|
627
|
-
} else {
|
|
628
|
-
traverseIDBNode(jsonContent, parsed)
|
|
629
|
-
}
|
|
630
|
-
return parsed
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
let width = 0;
|
|
634
|
-
let height = 0;
|
|
635
|
-
if (elements.length > 0) {
|
|
636
|
-
const rootBounds = elements[0].bounds;
|
|
637
|
-
width = rootBounds[2] - rootBounds[0];
|
|
638
|
-
height = rootBounds[3] - rootBounds[1];
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, {
|
|
642
|
-
screen: "",
|
|
643
|
-
resolution: { width, height },
|
|
644
|
-
elements
|
|
645
|
-
}, 'ui_tree')
|
|
646
|
-
|
|
647
|
-
measurement.setOutcome('observed')
|
|
648
|
-
const response = measurement.bindObservation({
|
|
649
|
-
device,
|
|
650
|
-
screen: "",
|
|
651
|
-
resolution: { width, height },
|
|
652
|
-
elements,
|
|
653
|
-
...snapshotMetadata
|
|
654
|
-
})
|
|
655
|
-
measurement.finish()
|
|
656
|
-
return response
|
|
657
|
-
} catch (e) {
|
|
658
|
-
const snapshotMetadata = deriveSnapshotMetadata(deviceKey, null, 'ui_tree')
|
|
659
|
-
measurement.setOutcome('unavailable')
|
|
660
|
-
measurement.finish()
|
|
661
|
-
return {
|
|
662
|
-
device,
|
|
663
|
-
screen: "",
|
|
664
|
-
resolution: { width: 0, height: 0 },
|
|
665
|
-
elements: [],
|
|
666
|
-
...snapshotMetadata,
|
|
667
|
-
error: `Failed to parse idb output: ${e instanceof Error ? e.message : String(e)}`
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
}
|
|
36
|
+
private async collectGetUITree(deviceId: string = "booted"): Promise<GetUITreeResponse> { return collectGetUITree(deviceId) }
|
|
671
37
|
|
|
672
38
|
async getScreenFingerprint(deviceId: string = 'booted'): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
673
39
|
return captureEvidence('fingerprint', { platform: 'ios', id: deviceId ?? '' }, () => this.collectGetScreenFingerprint(deviceId))
|
|
674
40
|
}
|
|
675
41
|
|
|
676
|
-
private async collectGetScreenFingerprint(deviceId: string = 'booted'): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
|
|
677
|
-
try {
|
|
678
|
-
const tree = await this.getUITree(deviceId)
|
|
679
|
-
if (!tree || tree.error) return { fingerprint: null, error: tree && (tree as any).error }
|
|
42
|
+
private async collectGetScreenFingerprint(deviceId: string = 'booted'): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> { return collectGetScreenFingerprint(this, deviceId) }
|
|
680
43
|
|
|
681
|
-
|
|
682
|
-
} catch (e) {
|
|
683
|
-
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async startLogStream(bundleId: string, deviceId: string = 'booted', sessionId: string = 'default') : Promise<{ success: boolean; stream_started?: boolean; error?: string }> {
|
|
688
|
-
try {
|
|
689
|
-
const simple = bundleId.split('.').pop() || bundleId
|
|
690
|
-
const predicate = `process == "${simple}" or process == "${bundleId}" or subsystem contains "${bundleId}"`
|
|
691
|
-
|
|
692
|
-
if (iosActiveLogStreams.has(sessionId)) {
|
|
693
|
-
try { iosActiveLogStreams.get(sessionId)!.proc.kill() } catch {}
|
|
694
|
-
iosActiveLogStreams.delete(sessionId)
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const args = ['simctl', 'spawn', deviceId, 'log', 'stream', '--style', 'syslog', '--predicate', predicate]
|
|
698
|
-
const proc = spawn(getXcrunCmd(), args)
|
|
44
|
+
async startLogStream(bundleId: string, deviceId: string = 'booted', sessionId: string = 'default'): Promise<{ success: boolean; stream_started?: boolean; error?: string }> { return startLogStream(bundleId, deviceId, sessionId) }
|
|
699
45
|
|
|
700
|
-
|
|
701
|
-
const file = path.join(tmpDir, `mobile-debug-ios-log-${sessionId}.ndjson`)
|
|
702
|
-
const stream = createWriteStream(file, { flags: 'a' })
|
|
46
|
+
async stopLogStream(sessionId: string = 'default'): Promise<{ success: boolean }> { return stopLogStream(sessionId) }
|
|
703
47
|
|
|
704
|
-
|
|
705
|
-
const text = chunk.toString()
|
|
706
|
-
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
707
|
-
for (const l of lines) {
|
|
708
|
-
const entry = parseLogLine(l)
|
|
709
|
-
stream.write(JSON.stringify(entry) + '\n')
|
|
710
|
-
}
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
proc.stderr.on('data', (chunk) => {
|
|
714
|
-
const text = chunk.toString()
|
|
715
|
-
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
716
|
-
for (const l of lines) {
|
|
717
|
-
const entry = { timestamp: '', level: 'E', tag: 'xcrun', message: l }
|
|
718
|
-
stream.write(JSON.stringify(entry) + '\n')
|
|
719
|
-
}
|
|
720
|
-
})
|
|
721
|
-
|
|
722
|
-
proc.on('close', () => {
|
|
723
|
-
stream.end()
|
|
724
|
-
if (iosActiveLogStreams.get(sessionId)?.proc === proc) iosActiveLogStreams.delete(sessionId)
|
|
725
|
-
})
|
|
726
|
-
|
|
727
|
-
iosActiveLogStreams.set(sessionId, { proc, file, device: { platform: 'ios', id: deviceId ?? '' } })
|
|
728
|
-
return { success: true, stream_started: true }
|
|
729
|
-
} catch {
|
|
730
|
-
return { success: false, error: 'log_stream_start_failed' }
|
|
731
|
-
}
|
|
732
|
-
}
|
|
48
|
+
async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string): Promise<{ entries: import('./logs/model.js').StreamLogEntry[], crash_summary?: { crash_detected: boolean, exception?: string, sample?: string } }> { return readLogStream(sessionId, limit, since) }
|
|
733
49
|
|
|
734
|
-
async
|
|
735
|
-
const entry = iosActiveLogStreams.get(sessionId)
|
|
736
|
-
if (!entry) return { success: true }
|
|
737
|
-
try { entry.proc.kill() } catch {}
|
|
738
|
-
iosActiveLogStreams.delete(sessionId)
|
|
739
|
-
return { success: true }
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async readLogStream(sessionId: string = 'default', limit: number = 100, since?: string): Promise<{ entries: any[], crash_summary?: { crash_detected: boolean, exception?: string, sample?: string } }> {
|
|
743
|
-
return captureEvidence('logs', this.getLogStreamDevice(sessionId), () => this.collectReadLogStream(sessionId, limit, since))
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string): Promise<{ entries: any[], crash_summary?: { crash_detected: boolean, exception?: string, sample?: string } }> {
|
|
747
|
-
const entry = iosActiveLogStreams.get(sessionId)
|
|
748
|
-
if (!entry) return { entries: [] }
|
|
749
|
-
try {
|
|
750
|
-
const data = await fsPromises.readFile(entry.file, 'utf8').catch(() => '')
|
|
751
|
-
if (!data) return { entries: [], crash_summary: { crash_detected: false } }
|
|
752
|
-
const lines = data.split(/\r?\n/).filter(Boolean)
|
|
753
|
-
const parsed = lines.map(l => {
|
|
754
|
-
try {
|
|
755
|
-
return JSON.parse(l)
|
|
756
|
-
} catch {
|
|
757
|
-
return { message: l, _iso: null, crash: false }
|
|
758
|
-
}
|
|
759
|
-
})
|
|
760
|
-
|
|
761
|
-
let filtered = parsed
|
|
762
|
-
if (since) {
|
|
763
|
-
let sinceMs: number | null = null
|
|
764
|
-
if (/^\d+$/.test(since)) sinceMs = Number(since)
|
|
765
|
-
else {
|
|
766
|
-
const sDate = new Date(since)
|
|
767
|
-
if (!isNaN(sDate.getTime())) sinceMs = sDate.getTime()
|
|
768
|
-
}
|
|
769
|
-
if (sinceMs !== null) {
|
|
770
|
-
filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs))
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const entries = filtered.slice(-Math.max(0, limit))
|
|
775
|
-
const crashEntry = entries.find(e => e.crash)
|
|
776
|
-
const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false }
|
|
777
|
-
return { entries, crash_summary }
|
|
778
|
-
} catch {
|
|
779
|
-
return { entries: [], crash_summary: { crash_detected: false } }
|
|
780
|
-
}
|
|
781
|
-
}
|
|
50
|
+
private async collectReadLogStream(sessionId: string = 'default', limit: number = 100, since?: string): Promise<{ entries: import('./logs/model.js').StreamLogEntry[], crash_summary?: { crash_detected: boolean, exception?: string, sample?: string } }> { return collectReadLogStream(sessionId, limit, since) }
|
|
782
51
|
}
|