mobile-debug-mcp 0.32.4 → 0.34.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/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.github/copilot-instructions.md +7 -0
- package/.github/workflows/ci.yml +21 -0
- package/AGENTS.md +21 -10
- package/README.md +69 -4
- package/agent-plugin/.claude-plugin/plugin.json +20 -0
- package/agent-plugin/.codex-plugin/plugin.json +34 -0
- package/agent-plugin/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/README.md +9 -3
- package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
- package/dist/debugger/android.js +78 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/sessions.js +262 -0
- package/dist/debugger/worker.js +165 -0
- package/dist/interact/android.js +10 -4
- package/dist/interact/conditional-action.js +68 -0
- package/dist/interact/index.js +70 -14
- package/dist/interact/ios.js +14 -8
- package/dist/manage/android.js +4 -3
- package/dist/manage/ios.js +3 -2
- package/dist/observe/android.js +25 -4
- package/dist/observe/evidence.js +280 -0
- package/dist/observe/index.js +33 -6
- package/dist/observe/ios.js +25 -6
- package/dist/observe/readiness-service.js +124 -0
- package/dist/observe/readiness.js +88 -0
- package/dist/observe/recording.js +165 -0
- package/dist/observe/snapshot-metadata.js +10 -2
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +285 -0
- package/dist/server/common.js +9 -0
- package/dist/server/condition-schema.js +32 -0
- package/dist/server/debugger-handlers.js +38 -0
- package/dist/server/debugger-schema.js +42 -0
- package/dist/server/journey-schema.js +4 -2
- package/dist/server/readiness-schema.js +12 -0
- package/dist/server/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +51 -0
- package/dist/server/tool-handlers.js +294 -105
- package/dist/server-core.js +3 -3
- package/dist/server.js +34 -0
- package/dist/system/index.js +5 -4
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +4 -4
- package/dist/utils/java.js +14 -12
- package/dist/utils/operation-budget.js +26 -2
- package/dist/utils/resolve-device.js +9 -0
- package/docs/CHANGELOG.md +15 -0
- package/docs/specs/020-conditional-actions/intent.md +62 -0
- package/docs/specs/020-conditional-actions/plan.md +214 -0
- package/docs/specs/020-conditional-actions/spec.md +301 -0
- package/docs/specs/021-app-readiness-signals/intent.md +40 -0
- package/docs/specs/021-app-readiness-signals/plan.md +102 -0
- package/docs/specs/021-app-readiness-signals/spec.md +144 -0
- package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
- package/docs/specs/022-correlated-evidence/intent.md +37 -0
- package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
- package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
- package/docs/specs/022-correlated-evidence/plan.md +171 -0
- package/docs/specs/022-correlated-evidence/spec.md +123 -0
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +40 -0
- package/docs/specs/023-android-system-traces/plan.md +117 -0
- package/docs/specs/023-android-system-traces/spec.md +120 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
- package/docs/specs/process.md +213 -0
- package/docs/tools/TOOLS.md +5 -0
- package/docs/tools/debugger.md +104 -0
- package/docs/tools/evidence.md +82 -0
- package/docs/tools/interact.md +125 -2
- package/docs/tools/observe.md +70 -0
- package/docs/tools/system-traces.md +126 -0
- package/package.json +1 -1
- package/scripts/agent-plugin.mjs +68 -8
- package/src/debugger/android.ts +65 -0
- package/src/debugger/guard.ts +26 -0
- package/src/debugger/sessions.ts +168 -0
- package/src/debugger/worker.ts +122 -0
- package/src/interact/android.ts +10 -4
- package/src/interact/conditional-action.ts +75 -0
- package/src/interact/index.ts +57 -28
- package/src/interact/ios.ts +14 -8
- package/src/manage/android.ts +4 -3
- package/src/manage/ios.ts +3 -2
- package/src/observe/android.ts +33 -6
- package/src/observe/evidence.ts +262 -0
- package/src/observe/index.ts +32 -8
- package/src/observe/ios.ts +32 -8
- package/src/observe/readiness-service.ts +94 -0
- package/src/observe/readiness.ts +84 -0
- package/src/observe/snapshot-metadata.ts +12 -1
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +218 -0
- package/src/server/common.ts +7 -0
- package/src/server/condition-schema.ts +32 -0
- package/src/server/debugger-handlers.ts +29 -0
- package/src/server/debugger-schema.ts +37 -0
- package/src/server/journey-schema.ts +4 -2
- package/src/server/readiness-schema.ts +12 -0
- package/src/server/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +49 -0
- package/src/server/tool-handlers.ts +182 -21
- package/src/server-core.ts +3 -3
- package/src/server.ts +24 -0
- package/src/system/index.ts +7 -5
- package/src/types.ts +30 -1
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +4 -4
- package/src/utils/java.ts +13 -12
- package/src/utils/operation-budget.ts +24 -2
- package/src/utils/resolve-device.ts +10 -0
- package/test/device/automated/debugger/runtime.android.test.ts +203 -0
- package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
- package/test/device/manual/interact/readiness.manual.ts +99 -0
- package/test/fixtures/debugger-app/MainActivity.java +46 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +13 -0
- package/test/fixtures/debugger-app/build.ts +29 -0
- package/test/fixtures/readiness-app/MainActivity.java +23 -0
- package/test/fixtures/readiness-app/README.md +12 -0
- package/test/fixtures/readiness-app/build.ts +37 -0
- package/test/fixtures/readiness-app/main.m +27 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/guards.test.ts +46 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
- package/test/unit/observe/compact_tree_error.test.ts +24 -0
- package/test/unit/observe/conditional_snapshots.test.ts +33 -0
- package/test/unit/observe/correlated-evidence.test.ts +128 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
- package/test/unit/observe/readiness.test.ts +41 -0
- package/test/unit/observe/readiness_native.test.ts +68 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/server/conditional_actions.test.ts +299 -0
- package/test/unit/server/conditional_schema.test.ts +59 -0
- package/test/unit/server/correlated-evidence.test.ts +194 -0
- package/test/unit/server/readiness.test.ts +170 -0
- package/test/unit/server/selector_tap_target.test.ts +79 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tap_selector.test.ts +2 -0
- package/worker/DebuggerWorker.java +160 -0
- package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
- /package/{skills → .agents/skills}/README.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
- /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
- /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
- /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
- /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
package/docs/tools/interact.md
CHANGED
|
@@ -19,6 +19,90 @@ Important:
|
|
|
19
19
|
- action tools report execution success, not outcome correctness.
|
|
20
20
|
- `classify_action_outcome` should receive the runtime `action_type` when you want routing to distinguish local-state and side-effect actions.
|
|
21
21
|
|
|
22
|
+
## Conditional actions
|
|
23
|
+
|
|
24
|
+
`tap`, `tap_element`, and `swipe` (Android/iOS), plus Android `type_text` and
|
|
25
|
+
`press_back`, accept an optional expected UI snapshot. Journey `tap` steps accept
|
|
26
|
+
it per step. Other tools and step types reject `expectedState` rather than ignore
|
|
27
|
+
it; it is not supported in journey defaults or on the journey itself.
|
|
28
|
+
|
|
29
|
+
First call `get_ui_tree` on the chosen device. From a successful response, use
|
|
30
|
+
`snapshot_session_id` and `snapshot_revision`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"platform": "android",
|
|
35
|
+
"deviceId": "emulator-5554",
|
|
36
|
+
"selector": { "accessibility_id": "save" },
|
|
37
|
+
"expectedState": {
|
|
38
|
+
"snapshotSessionId": "<snapshot_session_id from get_ui_tree>",
|
|
39
|
+
"snapshotRevision": 12,
|
|
40
|
+
"timeoutMs": 15000
|
|
41
|
+
},
|
|
42
|
+
"verificationMode": "none",
|
|
43
|
+
"responseMode": "compact"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This is a `tap_element` example. Guarded standalone calls require explicit
|
|
48
|
+
`platform` and concrete `deviceId` from the observed tree; `booted` and `default`
|
|
49
|
+
are not valid guarded targets. Journey taps inherit the journey's resolved device.
|
|
50
|
+
The expected-state object is strict: nonempty session ID, positive safe-integer
|
|
51
|
+
revision, and optional integer timeout of 100–60000 ms (default 15000). Invalid or
|
|
52
|
+
unsupported conditions return an `INVALID_ARGUMENT` error before device work.
|
|
53
|
+
Calls without a condition preserve existing behaviour.
|
|
54
|
+
|
|
55
|
+
After target and adapter preparation, the server reads a new hierarchy just
|
|
56
|
+
before native input. It checks the same device, server session, and retained
|
|
57
|
+
revision, even with post-action verification disabled. It never waits for a
|
|
58
|
+
mismatch to become a match or retries/rebases the input automatically.
|
|
59
|
+
|
|
60
|
+
| Result | Meaning |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `condition.status: matched` | Condition passed; inspect the ordinary action outcome for delivery and verify its effect separately. |
|
|
63
|
+
| `PRECONDITION_FAILED`, `condition.status: mismatch` | State changed; input was not dispatched. The condition includes the exact fresh tree that blocked it. |
|
|
64
|
+
| `PRECONDITION_UNAVAILABLE`, `condition.status: unavailable` | Condition could not be established; input was not dispatched. Inspect `condition.reason` and observe again. |
|
|
65
|
+
| `condition.status: not_checked` | Target/capability preparation failed before reaching the guard; inspect the original action failure. |
|
|
66
|
+
|
|
67
|
+
Blocked input reports `success: false`, `dispatch_started: false`,
|
|
68
|
+
`delivery_status: not_delivered`, `retryable: false`, `lifecycle_state: failed`,
|
|
69
|
+
and `verification.status: not_requested`. No network action window is reset and
|
|
70
|
+
no post-input verification runs. A failed conditional journey tap stops the
|
|
71
|
+
journey; subsequent steps are `not_run` and earlier steps are not rolled back.
|
|
72
|
+
|
|
73
|
+
Condition evidence has `status`, `expected: { snapshot_session_id,
|
|
74
|
+
snapshot_revision }`, `observed`, `elapsed_ms`, and an optional `reason`.
|
|
75
|
+
Reasons are `BASELINE_UNAVAILABLE`, `SESSION_MISMATCH`, `DEVICE_MISMATCH`,
|
|
76
|
+
`OBSERVATION_FAILED`, `OBSERVATION_INVALID`, `CONDITION_TIMEOUT`, or
|
|
77
|
+
`ACTION_NOT_REACHED` for a check that was not reached. `observed` is null when
|
|
78
|
+
there is no valid fresh observation. Error trees cannot authorise input merely
|
|
79
|
+
because they contain an old revision.
|
|
80
|
+
|
|
81
|
+
Debug responses retain the full condition tree. Compact responses retain full
|
|
82
|
+
fresh evidence on mismatch/unavailable results; this is an exception to their
|
|
83
|
+
usual omission of device identifiers and tree contents. Successful compact
|
|
84
|
+
conditions contain only observed session/revision and capture time. UI trees may
|
|
85
|
+
contain private text; avoid copying them into logs unnecessarily.
|
|
86
|
+
|
|
87
|
+
The baseline must still be in this process's same-device history: up to eight
|
|
88
|
+
revisions, retained for ten minutes since a successful observation of that
|
|
89
|
+
revision. Server restart or eviction requires a new observation. A fresh read
|
|
90
|
+
cannot evict the baseline out from under its own check.
|
|
91
|
+
|
|
92
|
+
The match covers the existing normalized accessibility snapshot: element text,
|
|
93
|
+
identity, state, bounds, actionability, screen, and resolution. String identity
|
|
94
|
+
fields are trimmed/lowercased; native state values retain their representation.
|
|
95
|
+
All elements contribute, even when delta history is truncated. Pixel-only,
|
|
96
|
+
backend, inaccessible, and normalization-hidden changes are not detected; the
|
|
97
|
+
current tree does not establish foreground package/activity. Any observed
|
|
98
|
+
intervening change invalidates the old revision, even if the UI later returns.
|
|
99
|
+
|
|
100
|
+
A condition read can add seconds. Its timeout budget is separate from the action
|
|
101
|
+
budget; enclosing operation deadlines still apply. This check is not atomic with
|
|
102
|
+
OS input delivery. Concurrent clients, external input, or app updates can change
|
|
103
|
+
the UI after the observation. Sequence device actions and verify outcomes with
|
|
104
|
+
`expect_*` as usual.
|
|
105
|
+
|
|
22
106
|
## tap / swipe / type_text / press_back
|
|
23
107
|
|
|
24
108
|
These tools return a shared action execution envelope.
|
|
@@ -633,7 +717,13 @@ has unknown delivery and must be observed before retrying.
|
|
|
633
717
|
|
|
634
718
|
`tap_element` accepts exactly one `elementId` or `selector`. A selector supports
|
|
635
719
|
`text`, `resource_id`, `accessibility_id`, and `contains`. Optional `waitFor` waits
|
|
636
|
-
for the target before tapping; without it, resolution uses one observation
|
|
720
|
+
for the target before tapping; without it, resolution uses one observation with
|
|
721
|
+
the action budget (default 5,000 ms), rather than a separate shorter cutoff.
|
|
722
|
+
An explicit `waitFor.timeoutMs` remains bounded by the overall action budget.
|
|
723
|
+
Selector taps resolve matching label text to its actionable ancestor using the
|
|
724
|
+
existing clickable-wait policy, including `waitFor.condition: exists` or `visible`.
|
|
725
|
+
Hidden/disabled labels and non-actionable containers cannot be bypassed.
|
|
726
|
+
Observation timeouts report `ACTION_TIMEOUT`, rather than claiming a missing target.
|
|
637
727
|
Ambiguous targets require an explicit `waitFor.match.index`.
|
|
638
728
|
|
|
639
729
|
`run_journey` accepts 1–50 steps with unique IDs, on one platform/device. Supported
|
|
@@ -644,5 +734,38 @@ the journey; remaining steps report `not_run`. Journey responses default to comp
|
|
|
644
734
|
`defaults` supplies execution controls; step verification overrides the default.
|
|
645
735
|
`captureOnFailure: true` includes a screenshot-free failure snapshot in debug mode.
|
|
646
736
|
|
|
647
|
-
See [RFC 018](../specs
|
|
737
|
+
See [RFC 018](../specs/018-interaction-latency-and-batched-journeys/spec.md) for
|
|
648
738
|
contracts and the device acceptance criteria.
|
|
739
|
+
|
|
740
|
+
## Readiness-aware waits
|
|
741
|
+
|
|
742
|
+
`wait_for_ui` and `wait_for_ui_change` accept optional `requireReady: true`.
|
|
743
|
+
They require both their existing predicate and settled full-screen UI readiness
|
|
744
|
+
on the same current samples. Readiness uses a 500 ms stability window, shares the
|
|
745
|
+
wait's existing deadline (including retries), and adds no separate hierarchy reads.
|
|
746
|
+
|
|
747
|
+
```json
|
|
748
|
+
{
|
|
749
|
+
"platform": "android",
|
|
750
|
+
"deviceId": "emulator-5554",
|
|
751
|
+
"selector": { "text": "Ready" },
|
|
752
|
+
"condition": "visible",
|
|
753
|
+
"requireReady": true,
|
|
754
|
+
"timeout_ms": 30000
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Results retain the existing success/timeout envelope and include `readiness`
|
|
759
|
+
evidence and limitations. Opt-in shared-deadline expiry uses a `TIMEOUT` error
|
|
760
|
+
code and preserves whether the underlying predicate was observed; a readiness
|
|
761
|
+
timeout does not mean that a visible target was absent. A visible target while a loader is active cannot pass
|
|
762
|
+
this opt-in check. Observation failure resets readiness continuity; an exhausted
|
|
763
|
+
budget is not proof of an unresponsive app. Default `requireReady: false` retains
|
|
764
|
+
existing predicate semantics. There is no new journey assertion or change to
|
|
765
|
+
conditional-action preconditions. See [app readiness](observe.md#app-readiness)
|
|
766
|
+
for supported states and limits.
|
|
767
|
+
|
|
768
|
+
## Correlated evidence
|
|
769
|
+
|
|
770
|
+
Actions and journeys expose identifiers and phase timestamps for linking later
|
|
771
|
+
observations. See [correlated evidence](evidence.md) for scope and limitations.
|
package/docs/tools/observe.md
CHANGED
|
@@ -10,6 +10,12 @@ These tools are primarily for:
|
|
|
10
10
|
|
|
11
11
|
They are **not** the primary success signal when an applicable `expect_*` tool exists.
|
|
12
12
|
|
|
13
|
+
Covered observations support optional `actionRef` and return acquisition/freshness
|
|
14
|
+
metadata; see [correlated evidence](evidence.md).
|
|
15
|
+
|
|
16
|
+
Android performance investigations can use [system traces](system-traces.md)
|
|
17
|
+
to bracket a journey and retrieve a local Perfetto artifact.
|
|
18
|
+
|
|
13
19
|
## get_logs
|
|
14
20
|
|
|
15
21
|
Fetch recent logs as structured entries optimized for AI agents.
|
|
@@ -91,6 +97,11 @@ Notes:
|
|
|
91
97
|
- Elements may include a normalized `state` object when the platform exposes readable state such as checked, selected, focused, expanded, text input, or slider values.
|
|
92
98
|
- Elements may also include platform-native identity hints such as `stable_id`, `role`, `test_tag`, `selector`, and `semantic`.
|
|
93
99
|
- The tree response may include `snapshot_revision`, `captured_at_ms`, and `loading_state` when a reliable signal is available.
|
|
100
|
+
- Successful tree observations include `snapshot_session_id`, a random identifier
|
|
101
|
+
for this server process. Use it with `snapshot_revision` for
|
|
102
|
+
[conditional actions](interact.md#conditional-actions) on the same device.
|
|
103
|
+
Compact tree responses preserve the session ID and revision. Never derive an
|
|
104
|
+
expectation from an error tree, even when it carries an older revision.
|
|
94
105
|
- Prefer `wait_for_ui` for deterministic element resolution in interactive flows.
|
|
95
106
|
- Android reads the accessibility hierarchy with `uiautomator dump` through ADB. No helper APK is installed or required.
|
|
96
107
|
|
|
@@ -210,3 +221,62 @@ read_log_stream response example:
|
|
|
210
221
|
```json
|
|
211
222
|
{ "entries": [ { "timestamp": "2026-03-20T...Z", "level": "ERROR", "tag": "AppTag", "pid": 1234, "message": "FATAL EXCEPTION" } ], "crash_summary": { "crash_detected": true } }
|
|
212
223
|
```
|
|
224
|
+
|
|
225
|
+
## App readiness
|
|
226
|
+
|
|
227
|
+
`get_app_readiness` observes at most two fresh hierarchies; `wait_for_app_ready`
|
|
228
|
+
waits for settled evidence within one budget. Require `platform`; optionally
|
|
229
|
+
select `deviceId`. Each call pins its device and keeps its sampling history local.
|
|
230
|
+
|
|
231
|
+
| Control | Default | Accepted integer range |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `timeoutMs` | get: 15000; wait: 30000 | 100–60000 |
|
|
234
|
+
| `stabilityMs` | 500 | 100–10000 |
|
|
235
|
+
| `pollIntervalMs` (wait only) | 500 | 100–1000 |
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{ "platform": "android", "deviceId": "emulator-5554", "timeoutMs": 30000 }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Both return `success`, resolved `device`, `readiness`, `elapsed_ms`, and a
|
|
242
|
+
`failure_code` on failure. Get succeeds when it establishes loading, transitioning,
|
|
243
|
+
or settled; wait succeeds only for settled. Codes are `READINESS_UNAVAILABLE`,
|
|
244
|
+
`READINESS_TIMEOUT`, and `DEVICE_UNAVAILABLE`. Invalid/unknown inputs are rejected.
|
|
245
|
+
|
|
246
|
+
`readiness.state` is loading, transitioning, settled, unknown, or debugger_paused
|
|
247
|
+
for an owned Android debugger stop. Visible native
|
|
248
|
+
progress indicators establish loading; exact Loading / Please wait labels
|
|
249
|
+
(ignoring case, whitespace and trailing dots/ellipsis) are heuristic loading.
|
|
250
|
+
Loading takes priority over unchanged trees. Two different valid signatures show
|
|
251
|
+
transitioning; consecutive identical signatures spanning the stability window
|
|
252
|
+
show settled. Failed, empty, stale-session, wrong-device, or invalid observations
|
|
253
|
+
cannot establish stability and break sample continuity. Broad legacy loading
|
|
254
|
+
keywords such as a button named Progress are not sufficient loading evidence.
|
|
255
|
+
|
|
256
|
+
The summary includes `basis` (observed/heuristic/insufficient), `scope: device_ui`,
|
|
257
|
+
latest successful capture time and snapshot session/revision, sample
|
|
258
|
+
count, `stable_for_ms`, up to eight signal-code evidence records, limitations,
|
|
259
|
+
and capability flags. Evidence excludes UI text and full trees. Managed Android
|
|
260
|
+
[debugger pauses](debugger.md) are reported from session evidence without acquiring
|
|
261
|
+
a UI tree; readiness waits fail with `DEBUGGER_PAUSED`. External-debugger and iOS
|
|
262
|
+
pause detection remain unsupported. Unresponsive detection is unsupported; neither
|
|
263
|
+
slow hierarchy retrieval nor a deadline expiry proves a pause or unresponsiveness.
|
|
264
|
+
|
|
265
|
+
Tree responses (including compact and nested trees) add a single-sample readiness
|
|
266
|
+
summary without extra reads. It reports an owned debugger pause when known;
|
|
267
|
+
otherwise a single-sample summary reports loading or unknown; it does
|
|
268
|
+
not inherit stability from an earlier request. Existing loading/delta fields and
|
|
269
|
+
compact error evidence are preserved.
|
|
270
|
+
|
|
271
|
+
Readiness describes normalized foreground UI during the capture window. It does
|
|
272
|
+
not verify foreground app identity, process health, business outcome, or backend
|
|
273
|
+
completion, and does not guarantee the UI will stay unchanged. A screenshot alone
|
|
274
|
+
never establishes readiness. Network observation windows and device input are
|
|
275
|
+
untouched. Timeouts include `DEADLINE_EXCEEDED` and retain the last valid
|
|
276
|
+
readiness evidence; an aborted acquisition does not erase that observation.
|
|
277
|
+
A non-timeout observation failure still makes current state unknown. Sampling and subprocess cleanup respect outer budgets.
|
|
278
|
+
Transport cancellation does not currently abort platform reads immediately;
|
|
279
|
+
reads remain bounded by their operation deadlines.
|
|
280
|
+
|
|
281
|
+
See [readiness-aware waits](interact.md#readiness-aware-waits) to combine this
|
|
282
|
+
signal with a specific UI predicate.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Android system traces
|
|
2
|
+
|
|
3
|
+
Use system traces to collect scheduling and system annotations around a slow
|
|
4
|
+
interaction. Capture is optional, device-wide and Android-only; it does not
|
|
5
|
+
automatically diagnose performance problems. No root or Android CLI installation
|
|
6
|
+
is required. The device must provide Perfetto (Android API 29+), authorised ADB,
|
|
7
|
+
tracing services, required sched/input categories, readable process identity,
|
|
8
|
+
base64 decoding and writable `/data/misc/perfetto-traces` storage.
|
|
9
|
+
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
All inputs reject unknown fields. `platform` is `android` or `ios`; iOS is
|
|
13
|
+
unsupported and never starts capture. Optional `deviceId` uses existing device
|
|
14
|
+
resolution; supplying one is recommended when multiple devices are connected.
|
|
15
|
+
|
|
16
|
+
| Tool | Arguments | Result |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `get_system_trace_capabilities` | `platform`, `deviceId?` | `supported`, `backend`, resolved `device`, prerequisite booleans, reasons, enabled sources, unavailable categories, limitations. No recording. |
|
|
19
|
+
| `start_system_trace` | `platform`, `deviceId?`, `durationMs?` | Owned recording session after data-source acknowledgement. Duration is an integer 1,000–120,000 ms, default 30,000. |
|
|
20
|
+
| `get_system_trace_status` | `traceId` | Reconcile process/deadline and finalise ended recording. |
|
|
21
|
+
| `stop_system_trace` | `traceId` | Gracefully stop and retrieve local artifact; retry transfer/cleanup after failure. |
|
|
22
|
+
| `cancel_system_trace` | `traceId` | Stop and discard partial files. Already retrieved artifacts remain local. |
|
|
23
|
+
|
|
24
|
+
Trace control IDs are UUIDs and cannot redirect to another device. One capture
|
|
25
|
+
per device and four pending sessions per server are allowed. Pending failed
|
|
26
|
+
cleanup reserves its slot until resolved. Terminal records with complete cleanup
|
|
27
|
+
expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
|
|
28
|
+
IDs. Repeated terminal operations return the stored outcome.
|
|
29
|
+
|
|
30
|
+
## Agent workflow and interpretation
|
|
31
|
+
|
|
32
|
+
Trace a reproducible slow or stalled interaction when existing UI timing/state
|
|
33
|
+
and logs leave a performance question unresolved. Check tool discovery first:
|
|
34
|
+
older installed servers may not expose these tools. Capability failure is a
|
|
35
|
+
reported limitation; it is not evidence that the app is frozen.
|
|
36
|
+
|
|
37
|
+
Establish the initial UI state and include an outcome assertion in the journey.
|
|
38
|
+
The launch-only example below demonstrates bracketing; it does not prove an app
|
|
39
|
+
outcome. Use a fresh screenshot or semantic assertion appropriate to the actual
|
|
40
|
+
screen. Settings, for example, may resume a subpage rather than show its main
|
|
41
|
+
title. Allow enough capture time for complete observations as well as inputs.
|
|
42
|
+
|
|
43
|
+
Always finalise an owned recording, including when a journey fails. Stop to keep
|
|
44
|
+
diagnostic evidence; cancel to discard it. Check both `state` and `cleanup`, and
|
|
45
|
+
use status/retry for an unresolved session before starting another capture.
|
|
46
|
+
Do not rely on automatic duration expiry as routine cleanup.
|
|
47
|
+
|
|
48
|
+
A useful report identifies the device, journey and assertion outcomes, trace ID,
|
|
49
|
+
linked correlation IDs, coverage, artifact path and cleanup outcome. `covered`
|
|
50
|
+
describes the capture session lifetime, not proof that every event survived the
|
|
51
|
+
ring buffer. Capture success and valid trace contents are separate from a
|
|
52
|
+
performance diagnosis. These tools do not return CPU summaries, frame metrics,
|
|
53
|
+
blocked-thread analysis or a root cause. Inspect the artifact with an available
|
|
54
|
+
analyser before making those claims; otherwise report analysis **unrun** and
|
|
55
|
+
provide the local artifact for follow-up. Host/device clocks remain unaligned.
|
|
56
|
+
|
|
57
|
+
## Bracket a journey
|
|
58
|
+
|
|
59
|
+
1. `get_system_trace_capabilities({"platform":"android","deviceId":"emulator-5554"})`
|
|
60
|
+
2. `start_system_trace({"platform":"android","deviceId":"emulator-5554","durationMs":30000})`
|
|
61
|
+
3. Use the returned `trace_id` as `systemTraceId`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"platform": "android",
|
|
66
|
+
"deviceId": "emulator-5554",
|
|
67
|
+
"systemTraceId": "<trace_id>",
|
|
68
|
+
"steps": [
|
|
69
|
+
{ "id": "launch", "type": "start_app", "appId": "com.android.settings", "verificationMode": "none" }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
4. `stop_system_trace({"traceId":"<trace_id>"})` returns `artifact.path` for local
|
|
75
|
+
inspection in Perfetto. Delete the file/directory when finished.
|
|
76
|
+
|
|
77
|
+
An invalid, inactive, foreign-server or different-device reference prevents all
|
|
78
|
+
journey steps. A failed journey leaves the capture available to stop or cancel;
|
|
79
|
+
journeys do not control its lifetime. Without `systemTraceId`, journey behaviour
|
|
80
|
+
and evidence remain unchanged. Linked journey/step evidence has
|
|
81
|
+
`trace_context: {"supported":true,"trace_id":"…","markers":[]}`. The journey
|
|
82
|
+
returns `system_trace` with `trace_id`, `coverage` (`covered` or `partial` if
|
|
83
|
+
capture ended during execution), and `clock_alignment:"unknown"`. Trace results
|
|
84
|
+
list linked `correlation_ids`. These are associations, not injected markers or
|
|
85
|
+
proof of causality; host intervals are not aligned with device trace clocks.
|
|
86
|
+
|
|
87
|
+
## Session response and failures
|
|
88
|
+
|
|
89
|
+
JSON responses contain `trace_id`, process-scoped `session_id`, `device`,
|
|
90
|
+
`backend:"perfetto"`, `state` (starting/recording/stopping/completed/cancelled/failed),
|
|
91
|
+
nullable `reason` and `completion_reason`, host UTC capture bounds, nullable
|
|
92
|
+
`artifact`, `cleanup`, `correlation_ids`, `enabled_sources`, and
|
|
93
|
+
`unavailable_categories`. Capture bounds include command/acknowledgement latency.
|
|
94
|
+
|
|
95
|
+
`artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`.
|
|
96
|
+
`cleanup` contains `status` (pending/complete/failed) and `remaining_resources`.
|
|
97
|
+
Errors preserve known session metadata and set MCP `isError`. Argument/unknown-ID
|
|
98
|
+
errors return `{error:{tool,code,message}}` without a session. Codes include
|
|
99
|
+
`INVALID_ARGUMENT`, `UNSUPPORTED_PLATFORM`, `TRACE_UNSUPPORTED`, `TRACE_BUSY`,
|
|
100
|
+
`TRACE_NOT_FOUND`, `TRACE_START_FAILED`, `TRACE_STOP_FAILED`,
|
|
101
|
+
`TRACE_ARTIFACT_FAILED`, `ACTION_TIMEOUT`, and `TRACE_CANCELLED`.
|
|
102
|
+
Capability failures return ordinary `supported:false` responses with reasons.
|
|
103
|
+
Read state/cleanup independently: a successful artifact transfer can still have
|
|
104
|
+
failed device-file cleanup. Retry stop/status, or cancel to discard failed output.
|
|
105
|
+
|
|
106
|
+
## Limits and privacy
|
|
107
|
+
|
|
108
|
+
The fixed preset requests sched, freq, idle, am, wm, gfx, view and input categories,
|
|
109
|
+
a 16 MiB ring buffer and a 32 MiB maximum output. Missing optional categories are
|
|
110
|
+
reported; missing sched/input rejects start. No arbitrary config, wildcard app
|
|
111
|
+
annotations, heap, screenshot or logcat sources are enabled. Device/system
|
|
112
|
+
annotations may still contain sensitive information; this is not app-private data.
|
|
113
|
+
|
|
114
|
+
Capture has a device-side duration limit and a server finalisation timer. Start
|
|
115
|
+
acknowledgement has a 35-second budget, graceful stop 10 seconds, and transfer
|
|
116
|
+
30 seconds. Only the recorded PID/start-time/output-path owner can be signalled;
|
|
117
|
+
no kill-by-name cleanup. A device disconnect or uncertain stop reports failure,
|
|
118
|
+
never a successful artifact. Partial transfers are discarded; bounded device
|
|
119
|
+
output remains retryable. Successful transfers remove owned device files.
|
|
120
|
+
|
|
121
|
+
Host artifacts use unique restricted temporary directories and remain for
|
|
122
|
+
user-managed deletion. Nothing is uploaded or returned as base64. Normal server
|
|
123
|
+
shutdown attempts owned cleanup for up to 10 seconds. A server crash can leave
|
|
124
|
+
a uniquely named bounded device file for manual removal; restart performs no
|
|
125
|
+
broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
|
|
126
|
+
limit; the server timer bounds wall-clock lifetime while the server is running.
|
package/package.json
CHANGED
package/scripts/agent-plugin.mjs
CHANGED
|
@@ -1,20 +1,62 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { copyFileSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
6
|
const repositoryRoot = fileURLToPath(new URL('../', import.meta.url))
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const packagedLicense = path.join(
|
|
7
|
+
const pluginRoot = path.join(repositoryRoot, 'agent-plugin')
|
|
8
|
+
const packagedSkill = path.join(pluginRoot, 'skills/mobile-debug-mcp/SKILL.md')
|
|
9
|
+
const packagedLicense = path.join(pluginRoot, 'LICENSE')
|
|
10
10
|
const sourceLicense = path.join(repositoryRoot, 'LICENSE')
|
|
11
11
|
const packageInfo = JSON.parse(readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8'))
|
|
12
|
-
const plugin = JSON.parse(readFileSync(path.join(
|
|
13
|
-
const mcp = JSON.parse(readFileSync(path.join(
|
|
12
|
+
const plugin = JSON.parse(readFileSync(path.join(pluginRoot, 'plugin.json'), 'utf8'))
|
|
13
|
+
const mcp = JSON.parse(readFileSync(path.join(pluginRoot, 'mcp.json'), 'utf8'))
|
|
14
|
+
const { $schema: _pluginSchema, extensions, ...pluginMetadata } = plugin
|
|
15
|
+
const { $schema: _mcpSchema, ...codexMcp } = mcp
|
|
16
|
+
const codexManifest = {
|
|
17
|
+
...pluginMetadata,
|
|
18
|
+
skills: './skills/',
|
|
19
|
+
mcpServers: './.mcp.json',
|
|
20
|
+
interface: extensions['com.openai'].interface,
|
|
21
|
+
}
|
|
22
|
+
const sharedMetadata = Object.fromEntries(
|
|
23
|
+
['name', 'version', 'description', 'author', 'repository', 'license'].map(key => [key, plugin[key]]),
|
|
24
|
+
)
|
|
25
|
+
const claudeManifest = {
|
|
26
|
+
...sharedMetadata,
|
|
27
|
+
mcpServers: {
|
|
28
|
+
[packageInfo.name]: {
|
|
29
|
+
command: 'npx',
|
|
30
|
+
args: ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
const cursorManifest = {
|
|
35
|
+
...sharedMetadata,
|
|
36
|
+
skills: './skills/',
|
|
37
|
+
mcpServers: {
|
|
38
|
+
[packageInfo.name]: {
|
|
39
|
+
type: 'stdio',
|
|
40
|
+
command: 'npx',
|
|
41
|
+
args: ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'],
|
|
42
|
+
cwd: '${CURSOR_PLUGIN_ROOT}',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
const manifests = [
|
|
47
|
+
['.codex-plugin/plugin.json', codexManifest],
|
|
48
|
+
['.claude-plugin/plugin.json', claudeManifest],
|
|
49
|
+
['.cursor-plugin/plugin.json', cursorManifest],
|
|
50
|
+
]
|
|
14
51
|
|
|
15
52
|
if (process.argv[2] === '--sync') {
|
|
16
|
-
copyFileSync(sourceSkill, packagedSkill)
|
|
17
53
|
copyFileSync(sourceLicense, packagedLicense)
|
|
54
|
+
writeFileSync(path.join(pluginRoot, '.mcp.json'), `${JSON.stringify(codexMcp, null, 2)}\n`)
|
|
55
|
+
for (const [relativePath, manifest] of manifests) {
|
|
56
|
+
const manifestPath = path.join(pluginRoot, relativePath)
|
|
57
|
+
mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
58
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
59
|
+
}
|
|
18
60
|
}
|
|
19
61
|
else if (process.argv[2] !== '--check') throw new Error('Use --sync or --check')
|
|
20
62
|
|
|
@@ -30,8 +72,26 @@ assert.equal(server.type, 'stdio')
|
|
|
30
72
|
assert.equal(server.command, 'npx')
|
|
31
73
|
assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
|
|
32
74
|
assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
|
|
33
|
-
assert.deepEqual(readFileSync(packagedSkill), readFileSync(sourceSkill), 'packaged usage skill must match its source')
|
|
34
75
|
assert.deepEqual(readFileSync(packagedLicense), readFileSync(sourceLicense), 'packaged license must match its source')
|
|
76
|
+
assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, '.mcp.json'), 'utf8')), codexMcp, 'Codex MCP configuration must match the portable plugin')
|
|
77
|
+
for (const [relativePath, expectedManifest] of manifests) {
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
|
|
80
|
+
expectedManifest,
|
|
81
|
+
`${relativePath} must match the portable plugin`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
for (const marketplacePath of [
|
|
85
|
+
'.agents/plugins/marketplace.json',
|
|
86
|
+
'.claude-plugin/marketplace.json',
|
|
87
|
+
'.cursor-plugin/marketplace.json',
|
|
88
|
+
]) {
|
|
89
|
+
const marketplace = JSON.parse(readFileSync(path.join(repositoryRoot, marketplacePath), 'utf8'))
|
|
90
|
+
assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
|
|
91
|
+
assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
|
|
92
|
+
assert.equal(marketplace.plugins[0].name, plugin.name, `${marketplacePath} plugin name must match`)
|
|
93
|
+
assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, './agent-plugin', `${marketplacePath} source must point to the plugin`)
|
|
94
|
+
}
|
|
35
95
|
const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
|
|
36
96
|
assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
|
|
37
97
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolveAdbCmd } from '../utils/android/utils.js'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
|
|
5
|
+
const exec = promisify(execFile)
|
|
6
|
+
export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
|
|
7
|
+
export interface DebugNative {
|
|
8
|
+
candidates(device: string, app: string): Promise<DebugTarget[]>
|
|
9
|
+
identity(device: string, target: DebugTarget): Promise<DebugTarget>
|
|
10
|
+
forward(device: string, pid: number): Promise<number>
|
|
11
|
+
removeForward(device: string, port: number, pid: number): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
export class AndroidDebugNative implements DebugNative {
|
|
14
|
+
async adb(device: string, args: string[]) {
|
|
15
|
+
try { return (await exec(resolveAdbCmd(), ['-s', device, ...args], { timeout: debuggerRemaining(5000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout.trim() }
|
|
16
|
+
catch (e) { throw nativeError(e, 'DEVICE_UNAVAILABLE') }
|
|
17
|
+
}
|
|
18
|
+
async identity(device: string, target: DebugTarget): Promise<DebugTarget> {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/stat`])
|
|
21
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/)
|
|
22
|
+
const status = await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/status`])
|
|
23
|
+
const uid = Number(/^Uid:\s+(\d+)/m.exec(status)?.[1])
|
|
24
|
+
const name = (await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/cmdline`])).split('\0')[0]
|
|
25
|
+
if (!/^\d+$/.test(fields[19] ?? '') || uid !== target.uid || name !== target.process_name || (target.start_identity && fields[19] !== target.start_identity)) throw new Error('identity')
|
|
26
|
+
return { ...target, start_identity: fields[19] }
|
|
27
|
+
} catch (e) { if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEVICE_UNAVAILABLE'].includes(e.code)) throw e; throw new DebuggerError('PROCESS_CHANGED') }
|
|
28
|
+
}
|
|
29
|
+
async candidates(device: string, app: string) {
|
|
30
|
+
if (!/^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/.test(app)) throw new DebuggerError('INVALID_ARGUMENT')
|
|
31
|
+
const packages = await this.adb(device, ['shell', 'cmd', 'package', 'list', 'packages', '-U', app])
|
|
32
|
+
const uid = Number(new RegExp(`^package:${app.replaceAll('.', '\\.')} uid:(\\d+)$`, 'm').exec(packages)?.[1])
|
|
33
|
+
if (!Number.isSafeInteger(uid)) throw new DebuggerError('PROCESS_NOT_FOUND')
|
|
34
|
+
// `adb jdwp` is a subscription, not a one-shot command. Bound the sample.
|
|
35
|
+
let jdwpOutput: string
|
|
36
|
+
try { jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout }
|
|
37
|
+
catch (e: any) {
|
|
38
|
+
if (debuggerSignal()?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
|
|
39
|
+
debuggerRemaining(1)
|
|
40
|
+
if (!e.killed || e.stderr?.trim()) throw nativeError(e, 'DEVICE_UNAVAILABLE')
|
|
41
|
+
jdwpOutput = e.stdout ?? ''
|
|
42
|
+
}
|
|
43
|
+
const jdwp = new Set(jdwpOutput.split(/\s+/).filter(id => /^\d+$/.test(id)))
|
|
44
|
+
const ps = await this.adb(device, ['shell', 'ps', '-A', '-o', 'PID,NAME'])
|
|
45
|
+
const result: DebugTarget[] = []
|
|
46
|
+
for (const line of ps.split('\n').slice(1)) {
|
|
47
|
+
const [pid, name] = line.trim().split(/\s+/)
|
|
48
|
+
if (jdwp.has(pid) && (name === app || name.startsWith(app + ':'))) result.push(await this.identity(device, { app_id: app, pid: Number(pid), process_name: name, start_identity: '', uid }))
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
async forward(device: string, pid: number) {
|
|
53
|
+
const port = Number(await this.adb(device, ['forward', '--no-rebind', 'tcp:0', `jdwp:${pid}`]))
|
|
54
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new DebuggerError('DEBUGGER_ATTACH_FAILED')
|
|
55
|
+
return port
|
|
56
|
+
}
|
|
57
|
+
async removeForward(device: string, port: number, pid: number) {
|
|
58
|
+
const list = await this.adb(device, ['forward', '--list'])
|
|
59
|
+
const entry = list.split('\n').find(line => line.trim().split(/\s+/)[1] === `tcp:${port}`)
|
|
60
|
+
if (!entry) return
|
|
61
|
+
const [serial, local, remote] = entry.trim().split(/\s+/)
|
|
62
|
+
if (serial !== device || local !== `tcp:${port}` || remote !== `jdwp:${pid}`) throw new DebuggerError('DEBUGGER_CLEANUP_FAILED')
|
|
63
|
+
await this.adb(device, ['forward', '--remove', `tcp:${port}`])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { DebuggerError } from './worker.js'
|
|
3
|
+
let pauseProvider: (device: string) => any = () => null
|
|
4
|
+
export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
|
|
5
|
+
const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
|
|
6
|
+
export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
|
|
7
|
+
return context.run({ platform, device, dispatched: false }, operation)
|
|
8
|
+
}
|
|
9
|
+
// Synchronous checks and native submission share a JS turn with pause updates.
|
|
10
|
+
export function checkDebuggerGuard(dispatch = false) {
|
|
11
|
+
const current = context.getStore()
|
|
12
|
+
if (!current || current.platform !== 'android') return
|
|
13
|
+
if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
|
|
14
|
+
if (dispatch) current.dispatched = true
|
|
15
|
+
}
|
|
16
|
+
export function guardDispatched() { return context.getStore()?.dispatched ?? false }
|
|
17
|
+
export function pausedReadiness(device: string) {
|
|
18
|
+
const paused = pauseProvider(device)
|
|
19
|
+
if (!paused) return null
|
|
20
|
+
return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
|
|
21
|
+
captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
|
|
22
|
+
evidence: [{ code: 'DEBUGGER_PAUSED', source: 'debugger', captured_at_ms: paused.captured_at_ms, snapshot_revision: null }],
|
|
23
|
+
limitations: ['FOREGROUND_APP_UNVERIFIED', 'DEVICE_WIDE_UI_GUARD'],
|
|
24
|
+
capabilities: { loading: true, transitioning: true, settled: true, unresponsive: false, debugger_paused: true },
|
|
25
|
+
debug_session_id: paused.debug_session_id, target: paused.target }
|
|
26
|
+
}
|