mobile-debug-mcp 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/.cursor-plugin/marketplace.json +1 -1
  2. package/.github/workflows/ci.yml +1 -1
  3. package/README.md +96 -24
  4. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  5. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  6. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  7. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  8. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  9. package/agent-plugin/.mcp.json +1 -1
  10. package/agent-plugin/README.md +37 -8
  11. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  12. package/agent-plugin/cursor/LICENSE +201 -0
  13. package/agent-plugin/cursor/README.md +9 -0
  14. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +261 -0
  15. package/agent-plugin/mcp.json +1 -1
  16. package/agent-plugin/mcp_config.json +13 -0
  17. package/agent-plugin/plugin.json +1 -1
  18. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +46 -18
  19. package/dist/contracts/device-readiness.js +1 -0
  20. package/dist/debugger/guard.js +4 -4
  21. package/dist/debugger/ios-worker.js +62 -0
  22. package/dist/debugger/ios.js +57 -0
  23. package/dist/debugger/sessions.js +33 -16
  24. package/dist/debugger/worker.js +6 -4
  25. package/dist/interact/index.js +12 -2
  26. package/dist/interact/waits/ui-change-wait.js +9 -2
  27. package/dist/interact/waits/ui-wait.js +7 -3
  28. package/dist/manage/index.js +3 -0
  29. package/dist/manage/ios.js +2 -0
  30. package/dist/observe/device-readiness.js +116 -0
  31. package/dist/observe/evidence.js +31 -0
  32. package/dist/observe/ios-trace/artifact.js +63 -0
  33. package/dist/observe/ios-trace/capabilities.js +86 -0
  34. package/dist/observe/ios-trace/native.js +93 -0
  35. package/dist/observe/ios-trace/notification.js +35 -0
  36. package/dist/observe/ios-trace/process.js +98 -0
  37. package/dist/observe/ios-trace/sessions.js +246 -0
  38. package/dist/observe/readiness-service.js +22 -10
  39. package/dist/observe/readiness.js +34 -4
  40. package/dist/observe/screenshot-encoding.js +4 -0
  41. package/dist/observe/snapshot-collection.js +7 -2
  42. package/dist/observe/system-trace-error.js +7 -0
  43. package/dist/observe/system-trace-router.js +81 -0
  44. package/dist/observe/system-trace.js +4 -8
  45. package/dist/server/definitions/manage.js +3 -2
  46. package/dist/server/handlers/action-execution.js +23 -14
  47. package/dist/server/handlers/compact-interaction.js +106 -0
  48. package/dist/server/handlers/compact-state.js +47 -0
  49. package/dist/server/handlers/debugger-handlers.js +27 -8
  50. package/dist/server/handlers/diagnostic.js +12 -4
  51. package/dist/server/handlers/input.js +2 -0
  52. package/dist/server/handlers/interact.js +2 -0
  53. package/dist/server/handlers/interaction-diagnostics.js +62 -0
  54. package/dist/server/handlers/journey.js +18 -20
  55. package/dist/server/handlers/observe.js +2 -1
  56. package/dist/server/handlers/tool-handlers.js +28 -14
  57. package/dist/server/schemas/debugger-schema.js +10 -7
  58. package/dist/server/schemas/interaction-diagnostics.js +9 -0
  59. package/dist/server/schemas/system-trace-schema.js +14 -9
  60. package/dist/server/tool-definitions.js +9 -4
  61. package/dist/server-core.js +5 -2
  62. package/dist/server.js +5 -5
  63. package/dist/system/android.js +19 -15
  64. package/dist/system/index.js +2 -1
  65. package/dist/utils/android/utils.js +7 -16
  66. package/dist/utils/app-installation.js +77 -0
  67. package/dist/utils/benchmark-metrics.js +43 -0
  68. package/dist/utils/exec.js +4 -0
  69. package/dist/utils/ios/utils.js +7 -11
  70. package/dist/utils/resolve-device.js +3 -2
  71. package/dist/utils/timing.js +3 -3
  72. package/docs/CHANGELOG.md +48 -1
  73. package/docs/agent-plugin-installation-verification.md +39 -0
  74. package/docs/benchmarks/README.md +1 -0
  75. package/docs/benchmarks/end-to-end-latency/README.md +98 -0
  76. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
  77. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
  78. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
  79. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
  80. package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
  81. package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
  82. package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
  83. package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
  84. package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
  85. package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
  86. package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
  87. package/docs/code-structure.md +3 -1
  88. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  89. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  90. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  91. package/docs/specs/031-ios-system-traces/plan.md +166 -0
  92. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  93. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  94. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  95. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  96. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  97. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  98. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  99. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  100. package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
  101. package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
  102. package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
  103. package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
  104. package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
  105. package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
  106. package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
  107. package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
  108. package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
  109. package/docs/specs/034-app-installation-status/intent.md +41 -0
  110. package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
  111. package/docs/specs/034-app-installation-status/plan.md +73 -0
  112. package/docs/specs/034-app-installation-status/spec.md +114 -0
  113. package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
  114. package/docs/specs/035-compact-interaction-results/intent.md +38 -0
  115. package/docs/specs/035-compact-interaction-results/plan.md +43 -0
  116. package/docs/specs/035-compact-interaction-results/spec.md +118 -0
  117. package/docs/specs/035-compact-interaction-results/verification.md +111 -0
  118. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
  119. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
  120. package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
  121. package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
  122. package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
  123. package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
  124. package/docs/tools/TOOLS.md +3 -2
  125. package/docs/tools/compact-interactions.md +56 -0
  126. package/docs/tools/correlated-evidence.md +6 -0
  127. package/docs/tools/debugger.md +45 -5
  128. package/docs/tools/interact.md +17 -8
  129. package/docs/tools/manage.md +10 -2
  130. package/docs/tools/observe.md +42 -2
  131. package/docs/tools/system-traces.md +81 -14
  132. package/docs/tools/system.md +6 -0
  133. package/package.json +2 -1
  134. package/scripts/agent-plugin.mjs +55 -5
  135. package/src/contracts/device-readiness.ts +8 -0
  136. package/src/debugger/android.ts +1 -1
  137. package/src/debugger/guard.ts +5 -5
  138. package/src/debugger/ios-worker.ts +49 -0
  139. package/src/debugger/ios.ts +45 -0
  140. package/src/debugger/lldb/breakpoints.py +51 -0
  141. package/src/debugger/lldb/inspection.py +109 -0
  142. package/src/debugger/lldb/worker.py +203 -0
  143. package/src/debugger/sessions.ts +24 -12
  144. package/src/debugger/worker.ts +7 -4
  145. package/src/interact/index.ts +13 -2
  146. package/src/interact/waits/ui-change-wait.ts +7 -2
  147. package/src/interact/waits/ui-wait.ts +6 -3
  148. package/src/manage/index.ts +2 -0
  149. package/src/manage/ios.ts +2 -0
  150. package/src/observe/device-readiness.ts +96 -0
  151. package/src/observe/evidence.ts +30 -1
  152. package/src/observe/ios-trace/artifact.ts +55 -0
  153. package/src/observe/ios-trace/capabilities.ts +74 -0
  154. package/src/observe/ios-trace/native.ts +72 -0
  155. package/src/observe/ios-trace/notification.ts +30 -0
  156. package/src/observe/ios-trace/process.ts +82 -0
  157. package/src/observe/ios-trace/sessions.ts +206 -0
  158. package/src/observe/readiness-service.ts +22 -10
  159. package/src/observe/readiness.ts +37 -4
  160. package/src/observe/screenshot-encoding.ts +4 -0
  161. package/src/observe/snapshot-collection.ts +7 -2
  162. package/src/observe/system-trace-error.ts +3 -0
  163. package/src/observe/system-trace-router.ts +61 -0
  164. package/src/observe/system-trace.ts +4 -4
  165. package/src/server/common.ts +2 -2
  166. package/src/server/definitions/manage.ts +3 -2
  167. package/src/server/handlers/action-execution.ts +22 -14
  168. package/src/server/handlers/compact-interaction.ts +91 -0
  169. package/src/server/handlers/compact-state.ts +43 -0
  170. package/src/server/handlers/debugger-handlers.ts +28 -9
  171. package/src/server/handlers/diagnostic.ts +11 -4
  172. package/src/server/handlers/input.ts +2 -0
  173. package/src/server/handlers/interact.ts +2 -0
  174. package/src/server/handlers/interaction-diagnostics.ts +50 -0
  175. package/src/server/handlers/journey.ts +15 -17
  176. package/src/server/handlers/observe.ts +2 -1
  177. package/src/server/handlers/payload.ts +2 -1
  178. package/src/server/handlers/tool-handlers.ts +29 -14
  179. package/src/server/schemas/debugger-schema.ts +10 -7
  180. package/src/server/schemas/interaction-diagnostics.ts +10 -0
  181. package/src/server/schemas/system-trace-schema.ts +15 -9
  182. package/src/server/tool-definitions.ts +8 -4
  183. package/src/server-core.ts +5 -2
  184. package/src/server.ts +4 -4
  185. package/src/system/android.ts +18 -11
  186. package/src/system/index.ts +2 -1
  187. package/src/types.ts +2 -0
  188. package/src/utils/android/utils.ts +8 -10
  189. package/src/utils/app-installation.ts +73 -0
  190. package/src/utils/benchmark-metrics.ts +42 -0
  191. package/src/utils/exec.ts +5 -0
  192. package/src/utils/ios/utils.ts +7 -10
  193. package/src/utils/resolve-device.ts +4 -2
  194. package/src/utils/timing.ts +3 -2
  195. package/test/device/automated/debugger/runtime.android.test.ts +5 -0
  196. package/test/device/automated/debugger/runtime.ios.test.ts +227 -0
  197. package/test/device/automated/interact/compact-results.smoke.ts +58 -0
  198. package/test/device/automated/latency/end-to-end-environment.ts +61 -0
  199. package/test/device/automated/latency/end-to-end-report.ts +90 -0
  200. package/test/device/automated/latency/end-to-end-session.ts +98 -0
  201. package/test/device/automated/latency/end-to-end.ts +149 -0
  202. package/test/device/automated/latency/owned-transport.ts +81 -0
  203. package/test/device/automated/latency/snapshot-pair.ts +143 -0
  204. package/test/device/automated/latency-ios/benchmark.ts +126 -0
  205. package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
  206. package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
  207. package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
  208. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  209. package/test/device/automated/observe/system-trace.smoke.ts +2 -2
  210. package/test/fixtures/contracts/tool-definitions.json +243 -47
  211. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  212. package/test/fixtures/ios-debugger-app/README.md +27 -0
  213. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  214. package/test/fixtures/latency-journey-app/build.ts +3 -3
  215. package/test/helpers/ios-trace-shutdown.ts +29 -0
  216. package/test/unit/debugger/handle-guards.test.ts +89 -0
  217. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  218. package/test/unit/debugger/ios.test.ts +104 -0
  219. package/test/unit/debugger/lldb-worker.py +166 -0
  220. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  221. package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
  222. package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
  223. package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
  224. package/test/unit/observe/device-readiness.test.ts +97 -0
  225. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  226. package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
  227. package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
  228. package/test/unit/observe/readiness.test.ts +1 -1
  229. package/test/unit/observe/system-trace-router.test.ts +49 -0
  230. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  231. package/test/unit/server/compact-interaction.test.ts +87 -0
  232. package/test/unit/server/compact-journey-evidence.test.ts +40 -0
  233. package/test/unit/server/compact-review-regressions.test.ts +78 -0
  234. package/test/unit/server/conditional_actions.test.ts +7 -3
  235. package/test/unit/server/correlated-evidence.test.ts +18 -6
  236. package/test/unit/server/end-to-end-report.test.ts +46 -0
  237. package/test/unit/server/input-device-readiness.test.ts +55 -0
  238. package/test/unit/server/interaction-diagnostics.test.ts +45 -0
  239. package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
  240. package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
  241. package/test/unit/server/readiness.test.ts +24 -1
  242. package/test/unit/server/run_journey.test.ts +2 -1
  243. package/test/unit/server/selector_tap_target.test.ts +2 -1
  244. package/test/unit/server/system-trace.test.ts +18 -7
  245. package/test/unit/utils/app-installation.test.ts +78 -0
  246. package/test/unit/utils/benchmark-metrics.test.ts +36 -0
  247. package/test/unit/utils/resolve_device.test.ts +11 -0
@@ -1,9 +1,10 @@
1
- # Android system traces
1
+ # System traces for Android and iOS
2
2
 
3
3
  [Documentation](../README.md) · [Tool reference](TOOLS.md)
4
4
 
5
5
  Use system traces to collect scheduling and system annotations around a slow
6
- interaction. Capture is optional, device-wide and Android-only; it does not
6
+ interaction. Capture is optional; Android uses device-wide Perfetto and iOS simulators use
7
+ app-attached Instruments Time Profiler. It does not
7
8
  automatically diagnose performance problems. No root or Android CLI installation
8
9
  is required. The device must provide Perfetto (Android API 29+), authorised ADB,
9
10
  tracing services, required sched/input categories, readable process identity,
@@ -11,14 +12,14 @@ base64 decoding and writable `/data/misc/perfetto-traces` storage.
11
12
 
12
13
  ## Tools
13
14
 
14
- All inputs reject unknown fields. `platform` is `android` or `ios`; iOS is
15
- unsupported and never starts capture. Optional `deviceId` uses existing device
16
- resolution; supplying one is recommended when multiple devices are connected.
15
+ All inputs reject unknown fields. `platform` is `android` or `ios`. Android's
16
+ optional `deviceId` uses existing device resolution. iOS requires an explicit
17
+ booted simulator UDID, plus `appId` for start; it never records the Mac host.
17
18
 
18
19
  | Tool | Arguments | Result |
19
20
  | --- | --- | --- |
20
- | `get_system_trace_capabilities` | `platform`, `deviceId?` | `supported`, `backend`, resolved `device`, prerequisite booleans, reasons, enabled sources, unavailable categories, limitations. No recording. |
21
- | `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. |
21
+ | `get_system_trace_capabilities` | `platform`, `deviceId?`, `appId?`, `template?` | Common: `supported`, `backend`, resolved `device`, reasons and limitations. Android: prerequisite booleans, `enabled_sources`, `unavailable_categories`. iOS: prerequisites as `available`/`missing`/`unknown`, `discovered_templates`, `templates`, `app`, `xcode_version`, `recovery`. No recording. |
22
+ | `start_system_trace` | `platform`, `deviceId?`, `durationMs?`, `appId?`, `template?` | Owned recording session after native readiness acknowledgement. Duration is an integer 1,000–120,000 ms, default 30,000. |
22
23
  | `get_system_trace_status` | `traceId` | Reconcile process/deadline and finalise ended recording. |
23
24
  | `stop_system_trace` | `traceId` | Gracefully stop and retrieve local artifact; retry transfer/cleanup after failure. |
24
25
  | `cancel_system_trace` | `traceId` | Stop and discard partial files. Already retrieved artifacts remain local. |
@@ -26,7 +27,7 @@ resolution; supplying one is recommended when multiple devices are connected.
26
27
  Trace control IDs are UUIDs and cannot redirect to another device. One capture
27
28
  per device and four pending sessions per server are allowed. Pending failed
28
29
  cleanup reserves its slot until resolved. Terminal records with complete cleanup
29
- expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
30
+ expire after 10 minutes or eviction beyond 100 records per backend (the router retains at most 100 completed IDs). Server restart invalidates
30
31
  IDs. Repeated terminal operations return the stored outcome.
31
32
 
32
33
  ## Capture workflow
@@ -89,12 +90,13 @@ proof of causality; host intervals are not aligned with device trace clocks.
89
90
  ## Session response and failures
90
91
 
91
92
  JSON responses contain `trace_id`, process-scoped `session_id`, `device`,
92
- `backend:"perfetto"`, `state` (starting/recording/stopping/completed/cancelled/failed),
93
+ `backend:"perfetto"` (Android) or `"xctrace"` (iOS), `state` (starting/recording/stopping/completed/cancelled/failed),
93
94
  nullable `reason` and `completion_reason`, host UTC capture bounds, nullable
94
- `artifact`, `cleanup`, `correlation_ids`, `enabled_sources`, and
95
- `unavailable_categories`. Capture bounds include command/acknowledgement latency.
95
+ `artifact`, `cleanup`, and `correlation_ids`. Android additionally returns
96
+ `enabled_sources` and `unavailable_categories`. Capture bounds include command/acknowledgement latency.
96
97
 
97
- `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`.
98
+ Android `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`;
99
+ see the iOS section below for Instruments artifact and session fields.
98
100
  `cleanup` contains `status` (pending/complete/failed) and `remaining_resources`.
99
101
  Errors preserve known session metadata and set MCP `isError`. Argument/unknown-ID
100
102
  errors return `{error:{tool,code,message}}` without a session. Codes include
@@ -105,7 +107,7 @@ Capability failures return ordinary `supported:false` responses with reasons.
105
107
  Read state/cleanup independently: a successful artifact transfer can still have
106
108
  failed device-file cleanup. Retry stop/status, or cancel to discard failed output.
107
109
 
108
- ## Limits and privacy
110
+ ## Android limits and privacy
109
111
 
110
112
  The fixed preset requests sched, freq, idle, am, wm, gfx, view and input categories,
111
113
  a 16 MiB ring buffer and a 32 MiB maximum output. Missing optional categories are
@@ -127,8 +129,73 @@ a uniquely named bounded device file for manual removal; restart performs no
127
129
  broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
128
130
  limit; the server timer bounds wall-clock lifetime while the server is running.
129
131
 
132
+ ## iOS simulator Instruments capture
133
+
134
+ Requires macOS, full Xcode with Instruments and clang, a booted simulator visible
135
+ in both `simctl` and `xctrace list devices`, and an already running development
136
+ app. Journey input also needs a compatible idb companion; capability discovery
137
+ for capture does not establish HID input support. The initial verified target is iPhone 17 Pro simulator / iOS 26.4.1 with
138
+ Xcode 27.2. Physical-device capture is unsupported and unrun. Tool installation,
139
+ OS profiling permissions and pairing/security changes are user actions.
140
+
141
+ ```json
142
+ {"platform":"ios","deviceId":"<simulator-UDID>","appId":"<bundle-id>","template":"Time Profiler","durationMs":60000}
143
+ ```
144
+
145
+ Use this target with capabilities and start, then bracket a `run_journey` on the
146
+ same simulator using the returned `systemTraceId`, and stop/cancel as above.
147
+ Omitting `template` defaults to Time Profiler. `appId` and `template` are rejected
148
+ on Android. Capabilities do not start capture; permissions and symbols remain
149
+ `unknown` until separately established. Installed templates are listed separately
150
+ from support: System Trace scheduling coverage is unverified, Animation Hitches
151
+ reported unsupported on the tested simulator, and Hangs is bundled within Time
152
+ Profiler rather than a selectable standalone template. No silent substitution.
153
+
154
+ The server resolves the bundle to an exact current executable/PID, pins the UDID,
155
+ and waits for a uniquely named Darwin recording-start notification before
156
+ returning `recording`. It compiles a small private listener with Xcode clang in
157
+ the owned temporary directory. A failed/uncertain start cleans up and returns a
158
+ failure, not an automatic retry. Errors such as `APP_NOT_RUNNING`,
159
+ `PROFILING_PERMISSION_REQUIRED`, `READINESS_TIMEOUT` and `TARGET_DISCONNECTED`
160
+ include bounded recovery guidance; raw recorder output is not returned.
161
+
162
+ Only the session-owned recorder is signalled; cleanup never terminates the app.
163
+ iOS readiness has a 35-second budget, stop/finalisation 30 seconds and artifact
164
+ validation 30 seconds. Recording is bounded by native duration and server
165
+ monitoring. Monitoring retains the pinned app identity and tolerates two consecutive
166
+ non-definitive command failures, resetting after a successful poll. The third
167
+ fails capture with its bounded reason (`COMMAND_FAILED`, `ACTION_TIMEOUT`, or
168
+ `TRACE_MONITOR_FAILED` for an unclassified failure). Storage limits, changed app
169
+ identity and a missing running app fail immediately. Expiry and recorder exit
170
+ are still checked after a tolerated poll failure. Shutdown attempts cancellation under an overall ten-second budget.
171
+ Failed export can be retried using stop/status without starting another recording;
172
+ cancel removes owned partial files. Completed artifacts remain for user deletion.
173
+
174
+ The `.trace` result may be a bundle/directory. `artifact` adds
175
+ `format:"instruments"`, `kind`, recursive `size_bytes`, and
176
+ `validation:{readable,schema,sample_rows}`. Validation checks a readable TOC,
177
+ matching target UUID/PID/template, `time-sample` schema and exported sample data.
178
+ Zero rows mean no CPU sample evidence. Warnings are preserved as bounded
179
+ classifications. This establishes readable capture, not successful symbolication,
180
+ working hang detection or root-cause diagnosis. Do not treat a saved failed trace
181
+ as a successful recording.
182
+
183
+ Results also include `app_id`, `pid`, `executable`, `template`,
184
+ `recording_ready_at_ms`, `warnings` and `clock_alignment:"unknown"`. The profiled
185
+ app stays pinned even if the journey navigates elsewhere; linked IDs do not prove
186
+ that other apps are captured. Start/stop during a linked journey reports partial
187
+ coverage. Host capture bounds include startup/finalisation uncertainty.
188
+
189
+ At least 1 GiB free output space is required before recording. The server checks
190
+ a 256 MiB recursive size threshold each second; this is a soft limit and can be
191
+ overshot during polling/finalisation. Oversized output fails and is discarded.
192
+ Final bundles reject symlinks and use restricted file/directory permissions.
193
+ Even app-attached capture may include sensitive system metadata, paths, symbols,
194
+ logs and signposts. Keep artifacts local and delete them when finished; no upload,
195
+ base64 response, permission bypass or restart-wide cleanup sweep occurs.
196
+
130
197
  ## Related guides
131
198
 
132
199
  - [Interaction](interact.md#interaction-execution-controls-rfc-018): run the journey being measured.
133
200
  - [Evidence correlation](correlated-evidence.md): interpret action and observation timing.
134
- - [Android runtime debugger](debugger.md): inspect paused application state.
201
+ - [Runtime debugger](debugger.md): inspect paused application state.
@@ -10,6 +10,12 @@ required platform is available, continue with [device selection](manage.md#list_
10
10
  ## get_system_status
11
11
  A fast, non-throwing healthcheck that inspects key dependencies and connections required for mobile debugging.
12
12
 
13
+ Android installation checks use `MCP_TARGET_PACKAGE` then `MCP_TARGET_APP_ID`.
14
+ `appInstallations` contains device-specific installed/absent/unknown observations
15
+ with app ID, device ID, scope and timestamp. Legacy `appInstalled` is omitted
16
+ unless exactly one device has a conclusive result. Missing context and failed
17
+ queries never establish absence; these fields do not report iOS installation.
18
+
13
19
  Input:
14
20
 
15
21
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "test": "npm run test:unit",
20
20
  "lint": "eslint src test --quiet && npm run check:quality && npm run check:architecture",
21
21
  "lint:fix": "eslint src test --fix && npm run check:quality && npm run check:architecture",
22
+ "package:antigravity": "node scripts/agent-plugin.mjs --export-antigravity",
22
23
  "sync:agent-plugin": "node scripts/agent-plugin.mjs --sync",
23
24
  "check:quality": "node scripts/code-quality.mjs",
24
25
  "check:architecture": "node scripts/check-architecture.mjs",
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict'
2
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import path from 'node:path'
5
5
 
@@ -43,22 +43,53 @@ const cursorManifest = {
43
43
  },
44
44
  },
45
45
  }
46
+ const antigravityManifest = {
47
+ $schema: 'https://antigravity.google/schemas/v1/plugin.json',
48
+ ...sharedMetadata,
49
+ displayName: extensions['com.openai'].interface.displayName,
50
+ logo: 'assets/mobile-debug-tools.svg',
51
+ suggestedPrompts: extensions['com.openai'].interface.defaultPrompt,
52
+ }
53
+ const antigravityMcp = {
54
+ mcpServers: Object.fromEntries(Object.entries(codexMcp.mcpServers).map(([name, server]) => {
55
+ const { type: _transport, ...settings } = server
56
+ return [name, settings]
57
+ })),
58
+ }
59
+ const mcpConfigs = [
60
+ ['.mcp.json', codexMcp],
61
+ ['mcp_config.json', antigravityMcp],
62
+ ]
46
63
  const manifests = [
47
64
  ['.codex-plugin/plugin.json', codexManifest],
48
65
  ['.claude-plugin/plugin.json', claudeManifest],
49
66
  ['.cursor-plugin/plugin.json', cursorManifest],
67
+ ['.antigravity-plugin/plugin.json', antigravityManifest],
50
68
  ]
51
69
 
70
+ const cursorFiles = [
71
+ ['.cursor-plugin/plugin.json', `${JSON.stringify(cursorManifest, null, 2)}\n`],
72
+ ['skills/mobile-debug-mcp/SKILL.md', readFileSync(packagedSkill)],
73
+ ['LICENSE', readFileSync(sourceLicense)],
74
+ ['README.md', '# Mobile Debug Tools for Cursor\n\nGenerated by `npm run sync:agent-plugin`; edit the canonical plugin source.\n\nCopy the contents of this directory into `~/.cursor/plugins/local/mobile-debug-tools`,\nthen run Developer: Reload Window and check Customize for the skill and MCP server.\nDo not copy the portable parent plugin: its `${PLUGIN_DATA}` variable is not expanded by Cursor.\n\nSetup and support: https://github.com/clivejefferies/mobile-debug-tools\n'],
75
+ ]
52
76
  if (process.argv[2] === '--sync') {
53
77
  copyFileSync(sourceLicense, packagedLicense)
54
- writeFileSync(path.join(pluginRoot, '.mcp.json'), `${JSON.stringify(codexMcp, null, 2)}\n`)
78
+ for (const [relativePath, contents] of cursorFiles) {
79
+ const target = path.join(pluginRoot, 'cursor', relativePath)
80
+ mkdirSync(path.dirname(target), { recursive: true })
81
+ writeFileSync(target, contents)
82
+ }
83
+ for (const [relativePath, config] of mcpConfigs) {
84
+ writeFileSync(path.join(pluginRoot, relativePath), `${JSON.stringify(config, null, 2)}\n`)
85
+ }
55
86
  for (const [relativePath, manifest] of manifests) {
56
87
  const manifestPath = path.join(pluginRoot, relativePath)
57
88
  mkdirSync(path.dirname(manifestPath), { recursive: true })
58
89
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
59
90
  }
60
91
  }
61
- else if (process.argv[2] !== '--check') throw new Error('Use --sync or --check')
92
+ else if (!['--check', '--export-antigravity'].includes(process.argv[2])) throw new Error('Use --sync, --check or --export-antigravity <new-directory>')
62
93
 
63
94
  assert.equal(plugin.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json')
64
95
  assert.equal(plugin.name, 'mobile-debug-tools')
@@ -73,7 +104,10 @@ assert.equal(server.command, 'npx')
73
104
  assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
74
105
  assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
75
106
  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')
107
+ for (const [relativePath, config] of mcpConfigs) {
108
+ assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')), config, `${relativePath} must match the portable MCP configuration`)
109
+ }
110
+ assert.ok(readFileSync(path.join(pluginRoot, antigravityManifest.logo)).length > 0, 'Antigravity logo must be bundled')
77
111
  for (const [relativePath, expectedManifest] of manifests) {
78
112
  assert.deepEqual(
79
113
  JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
@@ -81,6 +115,9 @@ for (const [relativePath, expectedManifest] of manifests) {
81
115
  `${relativePath} must match the portable plugin`,
82
116
  )
83
117
  }
118
+ for (const [relativePath, contents] of cursorFiles) {
119
+ assert.deepEqual(readFileSync(path.join(pluginRoot, 'cursor', relativePath)), Buffer.from(contents), `Cursor native ${relativePath} must match its source`)
120
+ }
84
121
  for (const marketplacePath of [
85
122
  '.agents/plugins/marketplace.json',
86
123
  '.claude-plugin/marketplace.json',
@@ -90,9 +127,22 @@ for (const marketplacePath of [
90
127
  assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
91
128
  assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
92
129
  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`)
130
+ assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, marketplacePath.startsWith('.cursor-plugin') ? './agent-plugin/cursor' : './agent-plugin', `${marketplacePath} source must point to the plugin`)
94
131
  }
95
132
  const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
96
133
  assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
97
134
 
98
135
  console.log('Agent plugin package is in sync')
136
+
137
+ if (process.argv[2] === '--export-antigravity') {
138
+ assert.ok(process.argv[3], 'Supply a new output directory for the Antigravity export')
139
+ const target = path.resolve(process.argv[3])
140
+ // Refuse existing directories rather than overwrite source or stale package files.
141
+ mkdirSync(target)
142
+ writeFileSync(path.join(target, 'plugin.json'), `${JSON.stringify(antigravityManifest, null, 2)}\n`)
143
+ writeFileSync(path.join(target, 'mcp_config.json'), `${JSON.stringify(antigravityMcp, null, 2)}\n`)
144
+ copyFileSync(path.join(pluginRoot, '.antigravity-plugin/README.md'), path.join(target, 'README.md'))
145
+ copyFileSync(packagedLicense, path.join(target, 'LICENSE'))
146
+ for (const directory of ['skills', 'assets']) cpSync(path.join(pluginRoot, directory), path.join(target, directory), { recursive: true, filter: file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._') })
147
+ console.log(`Antigravity package exported to ${target}`)
148
+ }
@@ -0,0 +1,8 @@
1
+ export interface DeviceReadiness {
2
+ wakefulness: 'awake' | 'asleep' | 'transitioning' | 'unknown' | 'unsupported'
3
+ lock_state: 'locked' | 'unlocked' | 'unknown' | 'unsupported'
4
+ input_ready: false | null
5
+ blockers: Array<'DEVICE_ASLEEP' | 'DEVICE_LOCKED' | 'DEVICE_TRANSITIONING' | 'DEBUGGER_PAUSED'>
6
+ evidence: Array<{ code: string; source: string; captured_at_ms: number }>
7
+ limitations: string[]
8
+ }
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process'
3
3
  import { promisify } from 'node:util'
4
4
  import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
5
5
  const exec = promisify(execFile)
6
- export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
6
+ export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number | null; executable_path?: string }
7
7
  export interface DebugNative {
8
8
  candidates(device: string, app: string): Promise<DebugTarget[]>
9
9
  identity(device: string, target: DebugTarget): Promise<DebugTarget>
@@ -5,7 +5,7 @@ interface PauseEvidence {
5
5
  captured_at_ms: number
6
6
  target: unknown
7
7
  }
8
- let pauseProvider: (device: string) => PauseEvidence | null = () => null
8
+ let pauseProvider: (device: string, platform?: string) => PauseEvidence | null = () => null
9
9
  export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
10
10
  const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
11
11
  export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
@@ -14,13 +14,13 @@ export function withDebuggerGuard<T>(platform: string, device: string, operation
14
14
  // Synchronous checks and native submission share a JS turn with pause updates.
15
15
  export function checkDebuggerGuard(dispatch = false) {
16
16
  const current = context.getStore()
17
- if (!current || current.platform !== 'android') return
18
- if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
17
+ if (!current) return
18
+ if (pauseProvider(current.device, current.platform)) throw new DebuggerError('DEBUGGER_PAUSED')
19
19
  if (dispatch) current.dispatched = true
20
20
  }
21
21
  export function guardDispatched() { return context.getStore()?.dispatched ?? false }
22
- export function pausedReadiness(device: string) {
23
- const paused = pauseProvider(device)
22
+ export function pausedReadiness(device: string, platform = 'android') {
23
+ const paused = pauseProvider(device, platform)
24
24
  if (!paused) return null
25
25
  return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
26
26
  captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
@@ -0,0 +1,49 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { getXcrunCmd } from '../utils/ios/utils.js'
3
+ import { iosExec } from './ios.js'
4
+ import { DebuggerProcess, type WorkerEvent, type DebugWorker } from './worker.js'
5
+ import { DebuggerError } from './diagnostics.js'
6
+ export const iosCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
7
+ evaluation: false, mutation: false, coroutines: false, native: false, async_execution_inspection: true, async_stepping: true }
8
+ export const iosLimitations = ['IOS_SIMULATOR_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD', 'ASYNC_STEPPING_TOOLCHAIN_DEPENDENT', 'AGGREGATE_VALUES_TYPE_ONLY', 'NO_TASK_ENUMERATION']
9
+ export const iosWorkerSource = fileURLToPath(new URL('../../src/debugger/lldb/worker.py', import.meta.url))
10
+ let prepared: Promise<{ python: string; env: NodeJS.ProcessEnv }> | undefined
11
+ export function prepareIOSWorker() {
12
+ prepared ??= (async () => {
13
+ if (process.platform !== 'darwin') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
14
+ const pythonPath = await iosExec(getXcrunCmd(), ['lldb', '-P'])
15
+ const python = await iosExec(getXcrunCmd(), ['--find', 'python3'])
16
+ // Ignore user Python startup/customisation and LLDB init files.
17
+ const env = { PATH: process.env.PATH, HOME: process.env.HOME, PYTHONPATH: pythonPath, PYTHONNOUSERSITE: '1' }
18
+ const ready = await iosExec(python, ['-s', '-B', '-c', 'import lldb; d=lldb.SBDebugger.Create(False); print("ready" if d.IsValid() else "unavailable"); lldb.SBDebugger.Destroy(d)'], env)
19
+ if (ready !== 'ready') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
20
+ return { python, env }
21
+ })().catch(error => { prepared = undefined; throw error })
22
+ return prepared
23
+ }
24
+ export async function createIOSWorker(onEvent: (event: WorkerEvent) => void) {
25
+ const { python, env } = await prepareIOSWorker()
26
+ let released = false
27
+ const worker = new DebuggerProcess(python, '', event => {
28
+ if (event.event === 'terminated' && ['process_exit', 'detached', 'pause_limit', 'idle_limit', 'session_limit'].includes(String(event.reason))) released = true
29
+ onEvent(event)
30
+ }, { args: ['-u', '-s', '-B', iosWorkerSource], env })
31
+ return new IOSWorker(worker, () => released)
32
+ }
33
+ /** A dead worker is not proof that its owned suspension was released. */
34
+ export class IOSWorker implements DebugWorker {
35
+ private detached = false
36
+ constructor(private worker: DebugWorker, private released: () => boolean) {}
37
+ async call(command: string, args: Record<string, unknown> = {}, timeout = 10000) {
38
+ const result = await this.worker.call(command, args, timeout)
39
+ if (command === 'detach' && result.detached === true) this.detached = true
40
+ return result
41
+ }
42
+ async close() {
43
+ if (!this.detached && !this.released()) {
44
+ try { await this.call('detach', {}, 1000) } catch { /* Report uncertainty after bounded transport shutdown. */ }
45
+ }
46
+ await this.worker.close()
47
+ if (!this.detached && !this.released()) throw new DebuggerError('DEBUGGER_CLEANUP_FAILED')
48
+ }
49
+ }
@@ -0,0 +1,45 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+ import path from 'node:path'
4
+ import { realpath } from 'node:fs/promises'
5
+ import { getXcrunCmd } from '../utils/ios/utils.js'
6
+ import type { DebugNative, DebugTarget } from './android.js'
7
+ import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
8
+ const exec = promisify(execFile)
9
+ export async function iosExec(file: string, args: string[], env?: NodeJS.ProcessEnv) {
10
+ try { return (await exec(file, args, { timeout: debuggerRemaining(5000), signal: debuggerSignal(), maxBuffer: 1024 * 1024, ...(env ? { env } : {}) })).stdout.trim() }
11
+ catch (e) { throw nativeError(e, 'DEVICE_UNAVAILABLE') }
12
+ }
13
+ export class IOSDebugNative implements DebugNative {
14
+ constructor(private run = iosExec) {}
15
+ private sim(args: string[]) { return this.run(getXcrunCmd(), ['simctl', ...args]) }
16
+ async candidates(device: string, app: string): Promise<DebugTarget[]> {
17
+ if (!/^[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}$/.test(device) || !/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(app)) throw new DebuggerError('INVALID_ARGUMENT')
18
+ const inventory = JSON.parse(await this.sim(['list', 'devices', 'booted', '--json'])) as { devices: Record<string, { udid: string; state: string }[]> }
19
+ if (!Object.values(inventory.devices).flat().some(d => d.udid === device && d.state === 'Booted')) throw new DebuggerError('DEBUGGER_UNSUPPORTED')
20
+ const services = await this.sim(['spawn', device, 'launchctl', 'list'])
21
+ const pids = this.processes(services, app)
22
+ if (!pids.length) return []
23
+ const container = await this.sim(['get_app_container', device, app, 'app'])
24
+ const executable = await this.run('/usr/bin/plutil', ['-extract', 'CFBundleExecutable', 'raw', '-o', '-', path.join(container, 'Info.plist')])
25
+ if (path.basename(executable) !== executable || !executable) throw new DebuggerError('PROCESS_NOT_FOUND')
26
+ const executablePath = await realpath(path.join(container, executable))
27
+ return Promise.all(pids.map(pid => this.identity(device, { app_id: app, pid, process_name: executable, executable_path: executablePath, start_identity: '', uid: null })))
28
+ }
29
+ private processes(services: string, app: string) {
30
+ return services.split('\n').flatMap(line => {
31
+ const [pid, , label] = line.trim().split(/\s+/)
32
+ return /^\d+$/.test(pid) && label?.startsWith(`UIKitApplication:${app}[`) ? [Number(pid)] : []
33
+ })
34
+ }
35
+ async identity(device: string, target: DebugTarget): Promise<DebugTarget> {
36
+ const services = await this.sim(['spawn', device, 'launchctl', 'list'])
37
+ if (!this.processes(services, target.app_id).includes(target.pid)) throw new DebuggerError('PROCESS_CHANGED')
38
+ const command = await this.run('/bin/ps', ['-ww', '-p', String(target.pid), '-o', 'comm='])
39
+ const started = await this.run('/bin/ps', ['-p', String(target.pid), '-o', 'lstart='])
40
+ if (command !== target.executable_path || !started || (target.start_identity && started !== target.start_identity)) throw new DebuggerError('PROCESS_CHANGED')
41
+ return { ...target, start_identity: started }
42
+ }
43
+ async forward(): Promise<number> { throw new DebuggerError('DEBUGGER_UNSUPPORTED') }
44
+ async removeForward() { throw new DebuggerError('DEBUGGER_UNSUPPORTED') }
45
+ }
@@ -0,0 +1,51 @@
1
+ """Owned source breakpoints with exact-line checking, including late image loads."""
2
+ import os
3
+ import uuid
4
+ import lldb
5
+
6
+
7
+ class Breakpoints:
8
+ def __init__(self, target):
9
+ self.target = target
10
+ self.owned = {}
11
+
12
+ def describe(self, key):
13
+ bp, source, line = self.owned[key]
14
+ locations = []
15
+ rejected = False
16
+ for loc in bp:
17
+ entry = loc.GetAddress().GetLineEntry()
18
+ actual = str(entry.GetFileSpec())
19
+ exact = entry.IsValid() and os.path.normpath(actual) == source and entry.GetLine() == line
20
+ loc.SetEnabled(exact)
21
+ if exact:
22
+ locations.append({"source_file": actual, "line": line})
23
+ else:
24
+ rejected = True
25
+ known_source = self.target.FindCompileUnits(lldb.SBFileSpec(source)).GetSize() > 0
26
+ status = "resolved" if locations else "rejected" if rejected or known_source else "pending"
27
+ return {"breakpoint_id": key, "status": status, "locations": locations}
28
+
29
+ def set(self, args):
30
+ source = os.path.normpath(args["sourceFile"])
31
+ bp = self.target.BreakpointCreateByLocation(lldb.SBFileSpec(source), args["line"], 0, 0, lldb.SBFileSpecList(), False)
32
+ key = str(uuid.uuid4())
33
+ self.owned[key] = (bp, source, args["line"])
34
+ result = self.describe(key)
35
+ if result["status"] == "rejected":
36
+ self.target.BreakpointDelete(bp.GetID())
37
+ del self.owned[key]
38
+ return result
39
+
40
+ def refresh(self):
41
+ return [self.describe(key) for key in list(self.owned)]
42
+
43
+ def remove(self, key):
44
+ if key in self.owned:
45
+ bp, _, _ = self.owned.pop(key)
46
+ self.target.BreakpointDelete(bp.GetID())
47
+ return {"removed": True}
48
+
49
+ def clear(self):
50
+ for key in list(self.owned):
51
+ self.remove(key)
@@ -0,0 +1,109 @@
1
+ """Bounded, static inspection. Never evaluate expressions or object descriptions."""
2
+ import uuid
3
+ import re
4
+ import lldb
5
+
6
+
7
+ def location(frame):
8
+ entry = frame.GetLineEntry()
9
+ file = entry.GetFileSpec()
10
+ source = str(file)[:512] if file.IsValid() else None
11
+ return {"source_file": source, "line": entry.GetLine() if entry.IsValid() else None,
12
+ "method_name": (frame.GetFunctionName() or "")[:512], "class_name": None}
13
+
14
+
15
+ class Inspection:
16
+ def __init__(self):
17
+ self.invalidate()
18
+
19
+ def invalidate(self):
20
+ self.stop_id = None
21
+ self.threads = {}
22
+ self.frames = {}
23
+
24
+ def stopped(self, process):
25
+ self.invalidate()
26
+ self.stop_id = str(uuid.uuid4())
27
+ selected = None
28
+ chosen = process.GetSelectedThread()
29
+ threads = [chosen] if chosen.IsValid() else []
30
+ for index in range(min(process.GetNumThreads(), 100)):
31
+ thread = process.GetThreadAtIndex(index)
32
+ if not chosen.IsValid() or thread.GetThreadID() != chosen.GetThreadID():
33
+ threads.append(thread)
34
+ for thread in threads[:100]:
35
+ key = str(uuid.uuid4())
36
+ self.threads[key] = thread
37
+ if selected is None and thread.GetStopReason() not in (lldb.eStopReasonNone, lldb.eStopReasonInvalid):
38
+ selected = key
39
+ if selected is None or process.GetState() != lldb.eStateStopped:
40
+ self.invalidate()
41
+ return None
42
+ thread = self.threads.get(selected)
43
+ return {"event": "stopped", "stop_id": self.stop_id, "thread_id": selected,
44
+ "reason": stop_reason(thread),
45
+ "location": location(thread.GetFrameAtIndex(0)) if thread else None,
46
+ "threads": [{"thread_id": key, "name": (t.GetName() or "")[:256]} for key, t in self.threads.items()],
47
+ "threads_truncated": process.GetNumThreads() > 100}
48
+
49
+ def thread(self, args):
50
+ if args.get("stopId") != self.stop_id or args.get("threadId") not in self.threads:
51
+ raise ValueError("DEBUGGER_STALE_REFERENCE")
52
+ return self.threads[args["threadId"]]
53
+
54
+ def stack(self, args):
55
+ thread = self.thread(args)
56
+ offset, limit = args.get("offset", 0), args.get("limit", 20)
57
+ count = min(thread.GetNumFrames(), 1000)
58
+ frames = []
59
+ for index in range(offset, min(count, offset + limit)):
60
+ frame = thread.GetFrameAtIndex(index)
61
+ key = self.stop_id + ":" + args["threadId"] + ":" + str(index)
62
+ if key not in self.frames and len(self.frames) >= 1000:
63
+ break
64
+ self.frames[key] = frame
65
+ frames.append({"frame_id": key, "index": index, **location(frame)})
66
+ return {"frames": frames, "truncated": count > offset + len(frames) or thread.GetNumFrames() > 1000}
67
+
68
+ def locals(self, args):
69
+ if args.get("stopId") != self.stop_id or args.get("frameId") not in self.frames:
70
+ raise ValueError("DEBUGGER_STALE_REFERENCE")
71
+ frame = self.frames[args["frameId"]]
72
+ values = frame.GetVariables(True, True, False, True, lldb.eNoDynamicValues)
73
+ result = []
74
+ for index in range(min(values.GetSize(), 100)):
75
+ value = values.GetValueAtIndex(index).GetNonSyntheticValue()
76
+ name, kind = (value.GetName() or "")[:256], (value.GetTypeName() or "")[:256]
77
+ if not value.GetError().Success():
78
+ result.append({"name": name, "type": kind, "availability": "unavailable"})
79
+ continue
80
+ primitive = value.GetType().GetTypeFlags() & (lldb.eTypeIsScalar | lldb.eTypeIsInteger | lldb.eTypeIsFloat)
81
+ text = value.GetValue() if primitive else None
82
+ # Swift value wrappers are not C scalars. Decode only standard primitive storage.
83
+ if kind in ("Swift.Int", "Swift.Int32", "Swift.Int64", "Swift.UInt", "Swift.Bool", "Swift.Double", "Swift.Float"):
84
+ storage = value.GetChildMemberWithName("_value").GetNonSyntheticValue()
85
+ if storage.IsValid() and storage.GetError().Success():
86
+ text = storage.GetValue()
87
+ if kind == "Swift.Bool" and text in ("0", "1"):
88
+ text = "true" if text == "1" else "false"
89
+ result.append({"name": name, "type": kind, "value": text[:256] if text else None,
90
+ "kind": "primitive" if text is not None else "object",
91
+ "truncated": bool(text and len(text) > 256),
92
+ "object_id": self.object_identity(value, kind) if text is None else None})
93
+ return {"availability": "available" if result else "unknown", "locals": result,
94
+ "reason": None if result else "EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA",
95
+ "truncated": values.GetSize() > 100}
96
+
97
+ def object_identity(self, value, kind):
98
+ pointer = value.GetValue()
99
+ if pointer and re.fullmatch(r"0x[0-9a-fA-F]+", pointer):
100
+ return str(uuid.uuid5(uuid.UUID(self.stop_id), kind + ":" + pointer))
101
+ return None
102
+
103
+
104
+ def stop_reason(thread):
105
+ if thread is None:
106
+ return "unknown"
107
+ return {lldb.eStopReasonBreakpoint: "breakpoint", lldb.eStopReasonPlanComplete: "step",
108
+ lldb.eStopReasonSignal: "signal", lldb.eStopReasonException: "exception",
109
+ lldb.eStopReasonWatchpoint: "watchpoint", lldb.eStopReasonExec: "exec"}.get(thread.GetStopReason(), "unknown")