sim-mirror 1.2.0__tar.gz → 2.0.0__tar.gz

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 (350) hide show
  1. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/PKG-INFO +25 -11
  2. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/README.md +24 -10
  3. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/THIRD_PARTY_LICENSES.md +43 -0
  4. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/docs/README.md +6 -0
  5. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/docs/media/README.md +6 -5
  6. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/claude-code-cli/README.md +2 -2
  7. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/codex/README.md +1 -1
  8. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/cursor/README.md +1 -1
  9. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/fastapi-embed/README.md +1 -1
  10. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Package.swift +9 -1
  11. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Arguments.swift +26 -1
  12. sim_mirror-2.0.0/helper/Sources/HelperCore/Capture.swift +202 -0
  13. sim_mirror-2.0.0/helper/Sources/HelperCore/Recording.swift +247 -0
  14. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Requests.swift +7 -2
  15. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Server.swift +10 -2
  16. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Video.swift +25 -0
  17. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Wire.swift +1 -1
  18. sim_mirror-2.0.0/helper/Sources/HelperPlatform/CaptureDevice.swift +187 -0
  19. sim_mirror-2.0.0/helper/Sources/HelperPlatform/Disclaimed.swift +46 -0
  20. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Framebuffer.swift +18 -40
  21. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/H264Stream.swift +36 -29
  22. sim_mirror-2.0.0/helper/Sources/HelperPlatform/PixelSource.swift +91 -0
  23. sim_mirror-2.0.0/helper/Sources/HelperPlatform/RecordingRenderer.swift +362 -0
  24. sim_mirror-2.0.0/helper/Sources/HelperPlatform/ScreenFeed.swift +60 -0
  25. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/SimulatorDevice.swift +24 -48
  26. sim_mirror-2.0.0/helper/Sources/HelperPlatform/UsbCapture.swift +245 -0
  27. sim_mirror-2.0.0/helper/Sources/HelperPlatform/Warming.swift +29 -0
  28. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/sim-mirror-helper/main.swift +74 -31
  29. sim_mirror-2.0.0/helper/Support/Info.plist +12 -0
  30. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/ArgumentsTests.swift +5 -1
  31. sim_mirror-2.0.0/helper/Tests/HelperCoreTests/CaptureTests.swift +108 -0
  32. sim_mirror-2.0.0/helper/Tests/HelperCoreTests/RecordingTests.swift +139 -0
  33. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/ServerTests.swift +5 -2
  34. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/WireTests.swift +1 -0
  35. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/README.md +2 -2
  36. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/common.schema.json +40 -3
  37. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/http.schema.json +56 -0
  38. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/status.schema.json +23 -4
  39. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/pyproject.toml +1 -0
  40. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/scripts/hatch_build.py +1 -1
  41. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/_version.py +1 -1
  42. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/api.py +1 -1
  43. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/destination.py +4 -4
  44. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/provider.py +15 -5
  45. sim_mirror-2.0.0/src/sim_mirror/build/wda.py +147 -0
  46. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/xcodebuild.py +62 -11
  47. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/context.py +3 -0
  48. sim_mirror-2.0.0/src/sim_mirror/cli/device_command.py +58 -0
  49. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/devices.py +10 -6
  50. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/main.py +5 -2
  51. sim_mirror-2.0.0/src/sim_mirror/cli/record.py +45 -0
  52. sim_mirror-2.0.0/src/sim_mirror/cli/wda.py +176 -0
  53. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/model.py +23 -0
  54. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/schema.py +114 -0
  55. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/base.py +38 -2
  56. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/helper_process.py +35 -10
  57. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/__init__.py +2 -0
  58. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/cable.py +39 -0
  59. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/capture.py +140 -0
  60. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/connector.py +276 -0
  61. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/screen.py +78 -0
  62. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda.py +208 -0
  63. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_client.py +190 -0
  64. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_roles.py +316 -0
  65. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_setup.py +178 -0
  66. sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_source.py +246 -0
  67. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/client.py +3 -0
  68. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/connector.py +3 -18
  69. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/helper.py +115 -1
  70. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/registry.py +56 -8
  71. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/capture.py +2 -15
  72. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/connector.py +4 -0
  73. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/actions.py +24 -4
  74. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/availability.py +4 -2
  75. sim_mirror-2.0.0/src/sim_mirror/core/backends.py +204 -0
  76. sim_mirror-2.0.0/src/sim_mirror/core/control.py +317 -0
  77. sim_mirror-2.0.0/src/sim_mirror/core/device_changes.py +217 -0
  78. sim_mirror-2.0.0/src/sim_mirror/core/device_logs.py +138 -0
  79. sim_mirror-2.0.0/src/sim_mirror/core/device_settings.py +111 -0
  80. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/devices.py +4 -0
  81. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/instance.py +35 -3
  82. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/manager.py +207 -60
  83. sim_mirror-2.0.0/src/sim_mirror/core/recording_sources.py +168 -0
  84. sim_mirror-2.0.0/src/sim_mirror/core/recordings.py +509 -0
  85. sim_mirror-2.0.0/src/sim_mirror/core/render.py +79 -0
  86. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/runtime.py +77 -6
  87. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/screen_input.py +15 -8
  88. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/screen_relay.py +6 -4
  89. sim_mirror-2.0.0/src/sim_mirror/core/signing.py +92 -0
  90. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/status.py +16 -11
  91. sim_mirror-2.0.0/src/sim_mirror/core/touch.py +99 -0
  92. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/checks.py +5 -1
  93. sim_mirror-2.0.0/src/sim_mirror/doctor/real_devices.py +132 -0
  94. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/tap.py +3 -3
  95. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/host_copy.py +111 -0
  96. sim_mirror-2.0.0/src/sim_mirror/platform/devicectl.py +399 -0
  97. sim_mirror-2.0.0/src/sim_mirror/platform/errors.py +15 -0
  98. sim_mirror-2.0.0/src/sim_mirror/platform/identifiers.py +41 -0
  99. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/images.py +13 -3
  100. sim_mirror-2.0.0/src/sim_mirror/platform/keychain.py +79 -0
  101. sim_mirror-2.0.0/src/sim_mirror/platform/lockdown.py +192 -0
  102. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/simctl.py +109 -10
  103. sim_mirror-2.0.0/src/sim_mirror/platform/usbmux.py +171 -0
  104. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/_generated.py +146 -3
  105. sim_mirror-2.0.0/src/sim_mirror/server/http_routes.py +200 -0
  106. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/settings_routes.py +1 -1
  107. sim_mirror-2.0.0/src/sim_mirror/server/static/viewer/start.js +32 -0
  108. sim_mirror-2.0.0/src/sim_mirror/testing/daemon.py +90 -0
  109. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fakes.py +196 -6
  110. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/development-certificate.pem +24 -0
  111. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-appearance.json +46 -0
  112. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-apps.json +59 -0
  113. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-devices.json +1065 -0
  114. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-displays.json +63 -0
  115. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-launch-missing.json +58 -0
  116. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-launch.json +53 -0
  117. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-lock-state.json +27 -0
  118. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-processes.json +39 -0
  119. sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-screenshot.json +31 -0
  120. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/guards.py +9 -0
  121. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/native.py +10 -1
  122. sim_mirror-2.0.0/src/sim_mirror/testing/recording.py +64 -0
  123. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/rig.py +29 -16
  124. sim_mirror-2.0.0/src/sim_mirror/testing/usbmux.py +214 -0
  125. sim_mirror-2.0.0/src/sim_mirror/testing/wda.py +252 -0
  126. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/app.py +22 -19
  127. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/context.py +16 -5
  128. sim_mirror-2.0.0/src/sim_mirror/tools/device.py +128 -0
  129. sim_mirror-2.0.0/src/sim_mirror/tools/record.py +88 -0
  130. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/registry.py +4 -4
  131. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/schemas.py +55 -5
  132. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/tests/unit/protocol/test_messages.py +4 -0
  133. sim_mirror-1.2.0/src/sim_mirror/server/http_routes.py +0 -93
  134. sim_mirror-1.2.0/src/sim_mirror/server/static/viewer/start.js +0 -26
  135. sim_mirror-1.2.0/src/sim_mirror/tools/device.py +0 -51
  136. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/.gitignore +0 -0
  137. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/LICENSE +0 -0
  138. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/NOTICE +0 -0
  139. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/README.md +0 -0
  140. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/custom-connector/README.md +0 -0
  141. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/embed-host/README.md +0 -0
  142. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/iframe/README.md +0 -0
  143. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/token-budget/README.md +0 -0
  144. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/web-component/README.md +0 -0
  145. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Accessibility.swift +0 -0
  146. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/FrameScheduler.swift +0 -0
  147. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Input.swift +0 -0
  148. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/JSON.swift +0 -0
  149. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Screen.swift +0 -0
  150. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/AccessibilityReader.swift +0 -0
  151. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Frameworks.swift +0 -0
  152. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/HIDTransports.swift +0 -0
  153. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Pictures.swift +0 -0
  154. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/SimDeviceHandle.swift +0 -0
  155. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/UnixSocket.swift +0 -0
  156. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/ObjCGuard/ObjCGuard.m +0 -0
  157. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/ObjCGuard/include/ObjCGuard.h +0 -0
  158. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/Fixtures/wire-vectors.json +0 -0
  159. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/AccessibilityTests.swift +0 -0
  160. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/InputTests.swift +0 -0
  161. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/PictureTests.swift +0 -0
  162. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/error-inactive.json +0 -0
  163. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/error-unauthorized.json +0 -0
  164. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-alert.json +0 -0
  165. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-keyboard.json +0 -0
  166. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-swiftui.json +0 -0
  167. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-tagged.json +0 -0
  168. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-uikit.json +0 -0
  169. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/listing.json +0 -0
  170. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/hierarchy.schema.json +0 -0
  171. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/listing.schema.json +0 -0
  172. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/agent-event.schema.json +0 -0
  173. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/client-input.schema.json +0 -0
  174. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/close-codes.json +0 -0
  175. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/constants.json +0 -0
  176. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/hello.schema.json +0 -0
  177. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/screen-text.schema.json +0 -0
  178. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/settings.schema.json +0 -0
  179. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/stream.schema.json +0 -0
  180. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/__init__.py +0 -0
  181. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/__main__.py +0 -0
  182. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/__init__.py +0 -0
  183. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/xcresult.py +0 -0
  184. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/__init__.py +0 -0
  185. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/app_command.py +0 -0
  186. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/config_command.py +0 -0
  187. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/doctor.py +0 -0
  188. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/helper.py +0 -0
  189. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/mcp.py +0 -0
  190. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/open_viewer.py +0 -0
  191. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/serve.py +0 -0
  192. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/settings_command.py +0 -0
  193. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/tokens.py +0 -0
  194. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/tools.py +0 -0
  195. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/version.py +0 -0
  196. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/xcode.py +0 -0
  197. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/__init__.py +0 -0
  198. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/discovery.py +0 -0
  199. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/env.py +0 -0
  200. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/provenance.py +0 -0
  201. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/settings_store.py +0 -0
  202. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/toml_source.py +0 -0
  203. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/writer.py +0 -0
  204. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/__init__.py +0 -0
  205. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/__init__.py +0 -0
  206. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/client.py +0 -0
  207. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/discovery.py +0 -0
  208. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/document.py +0 -0
  209. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/errors.py +0 -0
  210. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/merge.py +0 -0
  211. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/reader.py +0 -0
  212. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/wire.py +0 -0
  213. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/__init__.py +0 -0
  214. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/companion.py +0 -0
  215. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/connector.py +0 -0
  216. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/engine.py +0 -0
  217. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/frameworks.py +0 -0
  218. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/__init__.py +0 -0
  219. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb.proto +0 -0
  220. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb_grpc.py +0 -0
  221. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb_pb2.py +0 -0
  222. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/__init__.py +0 -0
  223. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/client.py +0 -0
  224. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/connector.py +0 -0
  225. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/hierarchy.py +0 -0
  226. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/merge.py +0 -0
  227. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/reader.py +0 -0
  228. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/__init__.py +0 -0
  229. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/wire.py +0 -0
  230. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/__init__.py +0 -0
  231. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/__init__.py +0 -0
  232. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/events.py +0 -0
  233. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/frames.py +0 -0
  234. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/gestures.py +0 -0
  235. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/reaper.py +0 -0
  236. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/text_entry.py +0 -0
  237. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/text_overlay.py +0 -0
  238. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/tickets.py +0 -0
  239. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/__init__.py +0 -0
  240. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/app.py +0 -0
  241. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/auth.py +0 -0
  242. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/confirmations.py +0 -0
  243. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/health.py +0 -0
  244. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/lease.py +0 -0
  245. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/lifecycle.py +0 -0
  246. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/passes.py +0 -0
  247. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/policy.py +0 -0
  248. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/tokens.py +0 -0
  249. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/__init__.py +0 -0
  250. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/macos.py +0 -0
  251. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/report.py +0 -0
  252. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/hosting.py +0 -0
  253. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/__init__.py +0 -0
  254. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/launcher.py +0 -0
  255. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/relay.py +0 -0
  256. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/__init__.py +0 -0
  257. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/budget.py +0 -0
  258. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/imagehash.py +0 -0
  259. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/model.py +0 -0
  260. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/ocr.py +0 -0
  261. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/readers.py +0 -0
  262. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/settle.py +0 -0
  263. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/snapshot.py +0 -0
  264. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/__init__.py +0 -0
  265. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/helper.py +0 -0
  266. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/probe.py +0 -0
  267. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/text_recognizer.swift +0 -0
  268. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/wait.py +0 -0
  269. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/__init__.py +0 -0
  270. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/developer_dir.py +0 -0
  271. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/device_data.py +0 -0
  272. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/json_lines.py +0 -0
  273. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/keyboard.py +0 -0
  274. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/process.py +0 -0
  275. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/swift.py +0 -0
  276. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/swiftpm.py +0 -0
  277. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/xcode.py +0 -0
  278. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/xcrun.py +0 -0
  279. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/__init__.py +0 -0
  280. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/messages.py +0 -0
  281. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/py.typed +0 -0
  282. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/scope.py +0 -0
  283. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/seams.py +0 -0
  284. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/__init__.py +0 -0
  285. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/agent_routes.py +0 -0
  286. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/envelope.py +0 -0
  287. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/log_redaction.py +0 -0
  288. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/pages.py +0 -0
  289. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/security.py +0 -0
  290. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/settings_view.py +0 -0
  291. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/socket_routes.py +0 -0
  292. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/static/viewer/embed.html +0 -0
  293. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/static/viewer/index.html +0 -0
  294. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/__init__.py +0 -0
  295. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/app_support.py +0 -0
  296. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/claims.py +0 -0
  297. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/private.py +0 -0
  298. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/__init__.py +0 -0
  299. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/app_sdk.py +0 -0
  300. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/asgi.py +0 -0
  301. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/contract.py +0 -0
  302. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-alert-ios27.json +0 -0
  303. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-alert.json +0 -0
  304. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-keyboard.json +0 -0
  305. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-swiftui.json +0 -0
  306. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-tagged.json +0 -0
  307. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-uikit.json +0 -0
  308. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-home-complete.json +0 -0
  309. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-home-ios27.json +0 -0
  310. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-keyboard-ios27.json +0 -0
  311. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-probe-ios27.json +0 -0
  312. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-safari-ios27.json +0 -0
  313. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-safari-typed.json +0 -0
  314. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-settings-interactable.json +0 -0
  315. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-settings-ios27.json +0 -0
  316. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-answers.json +0 -0
  317. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-alert.txt +0 -0
  318. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-home.txt +0 -0
  319. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-keyboard.txt +0 -0
  320. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-probe.txt +0 -0
  321. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-safari.txt +0 -0
  322. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-settings.txt +0 -0
  323. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/simctl-devices.json +0 -0
  324. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/simctl-runtimes.json +0 -0
  325. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-list-names.json +0 -0
  326. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-list.json +0 -0
  327. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-settings.json +0 -0
  328. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-test-plans-none.json +0 -0
  329. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-test-plans.json +0 -0
  330. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-broken.json +0 -0
  331. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-ok.json +0 -0
  332. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-results-unbuilt.json +0 -0
  333. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-retries.json +0 -0
  334. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing-xcode27.json +0 -0
  335. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing.json +0 -0
  336. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-unbuilt.json +0 -0
  337. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary.json +0 -0
  338. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-retries.json +0 -0
  339. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing-xcode27.json +0 -0
  340. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing.json +0 -0
  341. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests.json +0 -0
  342. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/pictures.py +0 -0
  343. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/vision.py +0 -0
  344. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/__init__.py +0 -0
  345. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/act.py +0 -0
  346. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/results.py +0 -0
  347. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/screenshot.py +0 -0
  348. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/snapshot.py +0 -0
  349. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/validation.py +0 -0
  350. {sim_mirror-1.2.0 → sim_mirror-2.0.0}/viewer/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sim-mirror
3
- Version: 1.2.0
3
+ Version: 2.0.0
4
4
  Summary: Mirror and drive the iOS Simulator from Claude Code, CLI agents and the browser.
5
5
  Project-URL: Homepage, https://github.com/AndrewKochulab/sim-mirror
6
6
  Project-URL: Documentation, https://github.com/AndrewKochulab/sim-mirror/tree/main/docs
@@ -45,16 +45,21 @@ Description-Content-Type: text/markdown
45
45
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
46
46
  [![Release](https://img.shields.io/github/v/release/AndrewKochulab/sim-mirror?include_prereleases&sort=semver)](https://github.com/AndrewKochulab/sim-mirror/releases)
47
47
 
48
- ![A Claude Code session tapping through Settings on an iPhone 17 Pro simulator in the viewer, its cursor named
49
- and drawn before each tap](docs/media/hero.gif)
48
+ <img src="docs/media/hero.gif" width="100%"
49
+ alt="A Claude Code session driving a calendar app on an iPhone 17 simulator in the viewer: it opens an event, scrolls
50
+ through its notes, then fills in and saves a new one, the agent's cursor gliding to each spot before it taps">
51
+
52
+ *A real session, sped up four times. [The whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4)
53
+ runs 1m 39s.*
50
54
 
51
55
  A live iOS Simulator in any browser tab or web page, an animated cursor that shows exactly what your AI agent is about
52
56
  to tap, and token-efficient UI snapshots so agents read the screen as compact text instead of screenshots. Works with
53
57
  Claude Code, Codex, Cursor and any other MCP client.
54
58
 
55
- > **Status: 1.2.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
56
- > 1.x release to [what SimMirror promises not to break](docs/stability.md). 1.2 drives simulators with SimMirror's own
57
- > helper, reads a screen from its pixels when accessibility says nothing, and lets an app share its own views.
59
+ > **Status: 2.0.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
60
+ > release to [what SimMirror promises not to break](docs/stability.md). 2.0 brings real iPhones and iPads beside the
61
+ > simulators -- live over the cable, driven through WebDriverAgent -- and records any screen as an MP4 or a GIF. It
62
+ > removes nothing 1.x promised.
58
63
 
59
64
  ## Why SimMirror
60
65
 
@@ -79,7 +84,7 @@ Claude Code, Codex, Cursor and any other MCP client.
79
84
  **Any MCP client**, as a stdio server:
80
85
 
81
86
  ```sh
82
- claude mcp add sim-mirror -- uvx --from sim-mirror==1.2.0 sim-mirror mcp
87
+ claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
83
88
  ```
84
89
 
85
90
  Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.md) and
@@ -88,7 +93,7 @@ Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.
88
93
  **The command**, for `sim-mirror open`, `doctor` and the rest:
89
94
 
90
95
  ```sh
91
- uv tool install sim-mirror==1.2.0
96
+ uv tool install sim-mirror==2.0.0
92
97
  # or
93
98
  brew install andrewkochulab/tap/sim-mirror
94
99
  ```
@@ -138,6 +143,14 @@ in a Debug build on the simulator it shares its own UIKit and SwiftUI views, so
138
143
  gesture or a hand-drawn control reads by name, and `.simMirror("Settings")` names a SwiftUI view. Optional, and nothing
139
144
  of it is in a Release build. See [The app SDK](docs/app-sdk.md).
140
145
 
146
+ **Real devices.** A cabled iPhone or iPad shows up beside the simulators: its live screen over the cable, its apps,
147
+ logs, look and location through Xcode's `devicectl`, and touching, typing and its element tree through a WebDriverAgent
148
+ you build with your own team. What SimMirror changes on it is put back when it is let go. See
149
+ [Real devices](docs/real-devices.md).
150
+
151
+ **Recording.** The viewer's Record button, `sim_record` or `sim-mirror record` keep a device's screen as an MP4, a GIF
152
+ or both, with each touch drawn where it landed and a 9:41 status bar. See [Recording](docs/recording.md).
153
+
141
154
  **Connectors.** SimMirror's own native helper gives full control with nothing to install; idb_companion still can,
142
155
  and the simctl connector mirrors the screen view-only when neither is there, and says why. `auto` uses the fastest that
143
156
  works and falls back when one fails. More connectors plug in through an entry point. See
@@ -185,6 +198,7 @@ an image at about 750 pixels a token).
185
198
  | macOS | 26 (15 with Xcode 16.4, view-only) |
186
199
  | Xcode | 26 and 27 |
187
200
  | Connectors | native helper (full control), idb_companion 1.5 (full control), simctl (view-only) |
201
+ | Real devices | an iPhone or iPad that Xcode's `devicectl` reaches, by cable (view-only without WebDriverAgent) |
188
202
  | App SDK | Debug builds on the iOS Simulator, iOS 16 or later |
189
203
  | Clients | Claude Code, Codex, Cursor, any stdio MCP client |
190
204
  | Browsers | Chrome, Safari, Firefox, Edge |
@@ -192,7 +206,7 @@ an image at about 750 pixels a token).
192
206
  Rows checked on real hardware are marked verified, with the date, in [Compatibility](docs/compatibility.md).
193
207
 
194
208
  From 1.0 the tools, the protocol, the embedding API, the viewer package, settings and the command line are held to
195
- [what SimMirror promises not to break](docs/stability.md): a 1.x release adds, and never breaks. Connectors are the
209
+ [what SimMirror promises not to break](docs/stability.md): a release adds, and never breaks; 2.0 broke nothing. Connectors are the
196
210
  exception, and pin a minor version.
197
211
 
198
212
  ## Documentation
@@ -208,8 +222,8 @@ the [command line](docs/reference/cli.md) and the [protocol](docs/reference/prot
208
222
  | **v0.1** (preview) | Live viewer, agent cursor, person and agent control, MCP tools, `sim-mirror doctor`, idb and simctl connectors, build and test as a preview |
209
223
  | **v0.2** | Build, run and test tools, a settings panel, Xcode 27's UI hierarchy through `mcpbridge`, one shared daemon for many hosts |
210
224
  | **v1.0** | Stable protocol and embedding API, held by a check; PyPI, npm and the MCP Registry; a cursor that stays while the agent works |
211
- | **v1.2** (now) | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
212
- | **v2.0** | Real iPhones: view, install and launch without signing; full control through WebDriverAgent |
225
+ | **v1.2** | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
226
+ | **v2.0** (now) | Real iPhones and iPads: the live screen over the cable, apps, logs, look and location without signing; full control through WebDriverAgent; recording a screen as an MP4 or GIF |
213
227
  | **Android** | Android emulator support with the same viewer, cursor and tools |
214
228
  | **Website & launch** | Landing page, video tutorials and guides |
215
229
 
@@ -8,16 +8,21 @@
8
8
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
9
9
  [![Release](https://img.shields.io/github/v/release/AndrewKochulab/sim-mirror?include_prereleases&sort=semver)](https://github.com/AndrewKochulab/sim-mirror/releases)
10
10
 
11
- ![A Claude Code session tapping through Settings on an iPhone 17 Pro simulator in the viewer, its cursor named
12
- and drawn before each tap](docs/media/hero.gif)
11
+ <img src="docs/media/hero.gif" width="100%"
12
+ alt="A Claude Code session driving a calendar app on an iPhone 17 simulator in the viewer: it opens an event, scrolls
13
+ through its notes, then fills in and saves a new one, the agent's cursor gliding to each spot before it taps">
14
+
15
+ *A real session, sped up four times. [The whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4)
16
+ runs 1m 39s.*
13
17
 
14
18
  A live iOS Simulator in any browser tab or web page, an animated cursor that shows exactly what your AI agent is about
15
19
  to tap, and token-efficient UI snapshots so agents read the screen as compact text instead of screenshots. Works with
16
20
  Claude Code, Codex, Cursor and any other MCP client.
17
21
 
18
- > **Status: 1.2.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
19
- > 1.x release to [what SimMirror promises not to break](docs/stability.md). 1.2 drives simulators with SimMirror's own
20
- > helper, reads a screen from its pixels when accessibility says nothing, and lets an app share its own views.
22
+ > **Status: 2.0.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
23
+ > release to [what SimMirror promises not to break](docs/stability.md). 2.0 brings real iPhones and iPads beside the
24
+ > simulators -- live over the cable, driven through WebDriverAgent -- and records any screen as an MP4 or a GIF. It
25
+ > removes nothing 1.x promised.
21
26
 
22
27
  ## Why SimMirror
23
28
 
@@ -42,7 +47,7 @@ Claude Code, Codex, Cursor and any other MCP client.
42
47
  **Any MCP client**, as a stdio server:
43
48
 
44
49
  ```sh
45
- claude mcp add sim-mirror -- uvx --from sim-mirror==1.2.0 sim-mirror mcp
50
+ claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
46
51
  ```
47
52
 
48
53
  Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.md) and
@@ -51,7 +56,7 @@ Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.
51
56
  **The command**, for `sim-mirror open`, `doctor` and the rest:
52
57
 
53
58
  ```sh
54
- uv tool install sim-mirror==1.2.0
59
+ uv tool install sim-mirror==2.0.0
55
60
  # or
56
61
  brew install andrewkochulab/tap/sim-mirror
57
62
  ```
@@ -101,6 +106,14 @@ in a Debug build on the simulator it shares its own UIKit and SwiftUI views, so
101
106
  gesture or a hand-drawn control reads by name, and `.simMirror("Settings")` names a SwiftUI view. Optional, and nothing
102
107
  of it is in a Release build. See [The app SDK](docs/app-sdk.md).
103
108
 
109
+ **Real devices.** A cabled iPhone or iPad shows up beside the simulators: its live screen over the cable, its apps,
110
+ logs, look and location through Xcode's `devicectl`, and touching, typing and its element tree through a WebDriverAgent
111
+ you build with your own team. What SimMirror changes on it is put back when it is let go. See
112
+ [Real devices](docs/real-devices.md).
113
+
114
+ **Recording.** The viewer's Record button, `sim_record` or `sim-mirror record` keep a device's screen as an MP4, a GIF
115
+ or both, with each touch drawn where it landed and a 9:41 status bar. See [Recording](docs/recording.md).
116
+
104
117
  **Connectors.** SimMirror's own native helper gives full control with nothing to install; idb_companion still can,
105
118
  and the simctl connector mirrors the screen view-only when neither is there, and says why. `auto` uses the fastest that
106
119
  works and falls back when one fails. More connectors plug in through an entry point. See
@@ -148,6 +161,7 @@ an image at about 750 pixels a token).
148
161
  | macOS | 26 (15 with Xcode 16.4, view-only) |
149
162
  | Xcode | 26 and 27 |
150
163
  | Connectors | native helper (full control), idb_companion 1.5 (full control), simctl (view-only) |
164
+ | Real devices | an iPhone or iPad that Xcode's `devicectl` reaches, by cable (view-only without WebDriverAgent) |
151
165
  | App SDK | Debug builds on the iOS Simulator, iOS 16 or later |
152
166
  | Clients | Claude Code, Codex, Cursor, any stdio MCP client |
153
167
  | Browsers | Chrome, Safari, Firefox, Edge |
@@ -155,7 +169,7 @@ an image at about 750 pixels a token).
155
169
  Rows checked on real hardware are marked verified, with the date, in [Compatibility](docs/compatibility.md).
156
170
 
157
171
  From 1.0 the tools, the protocol, the embedding API, the viewer package, settings and the command line are held to
158
- [what SimMirror promises not to break](docs/stability.md): a 1.x release adds, and never breaks. Connectors are the
172
+ [what SimMirror promises not to break](docs/stability.md): a release adds, and never breaks; 2.0 broke nothing. Connectors are the
159
173
  exception, and pin a minor version.
160
174
 
161
175
  ## Documentation
@@ -171,8 +185,8 @@ the [command line](docs/reference/cli.md) and the [protocol](docs/reference/prot
171
185
  | **v0.1** (preview) | Live viewer, agent cursor, person and agent control, MCP tools, `sim-mirror doctor`, idb and simctl connectors, build and test as a preview |
172
186
  | **v0.2** | Build, run and test tools, a settings panel, Xcode 27's UI hierarchy through `mcpbridge`, one shared daemon for many hosts |
173
187
  | **v1.0** | Stable protocol and embedding API, held by a check; PyPI, npm and the MCP Registry; a cursor that stays while the agent works |
174
- | **v1.2** (now) | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
175
- | **v2.0** | Real iPhones: view, install and launch without signing; full control through WebDriverAgent |
188
+ | **v1.2** | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
189
+ | **v2.0** (now) | Real iPhones and iPads: the live screen over the cable, apps, logs, look and location without signing; full control through WebDriverAgent; recording a screen as an MP4 or GIF |
176
190
  | **Android** | Android emulator support with the same viewer, cursor and tools |
177
191
  | **Website & launch** | Landing page, video tutorials and guides |
178
192
 
@@ -84,5 +84,48 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
84
84
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
85
85
  ```
86
86
 
87
+ ## WebDriverAgent
88
+
89
+ - **What:** the XCTest runner SimMirror uses to touch, type on and read a real device
90
+ ([Real devices](docs/real-devices.md#tier-2-webdriveragent)). It is **not** shipped: `sim-mirror wda setup` fetches
91
+ release 16.12.10 (commit `00c38220c3e84906c965b996ffc4c12d09fef62f`), checks its SHA-256, makes two changes -- its
92
+ screen stream is bound to the device's loopback, as its API is, and a point of a touch is used where it is given
93
+ rather than looked up against the app -- and builds it on your Mac with your team.
94
+ - **Source:** [appium/WebDriverAgent](https://github.com/appium/WebDriverAgent), first written by Facebook.
95
+ - **License:** BSD-3-Clause
96
+
97
+ ```
98
+ BSD License
99
+
100
+ For WebDriverAgent software
101
+
102
+ Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
103
+
104
+ Redistribution and use in source and binary forms, with or without modification,
105
+ are permitted provided that the following conditions are met:
106
+
107
+ * Redistributions of source code must retain the above copyright notice, this
108
+ list of conditions and the following disclaimer.
109
+
110
+ * Redistributions in binary form must reproduce the above copyright notice,
111
+ this list of conditions and the following disclaimer in the documentation
112
+ and/or other materials provided with the distribution.
113
+
114
+ * Neither the name Facebook nor the names of its contributors may be used to
115
+ endorse or promote products derived from this software without specific
116
+ prior written permission.
117
+
118
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
119
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
120
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
121
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
122
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
123
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
124
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
125
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
126
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
127
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
128
+ ```
129
+
87
130
  Runtime dependencies installed from PyPI and npm (FastAPI, Uvicorn, grpclib, protobuf, tomlkit, Pillow and others) are
88
131
  not redistributed here; each carries its own license in its package.
@@ -13,6 +13,12 @@
13
13
  - [Cursor](clients/cursor.md)
14
14
  - [Any other MCP client](clients/other-mcp.md)
15
15
 
16
+ ## Use it with a real device, and record
17
+
18
+ - [Real devices](real-devices.md) -- a cabled iPhone or iPad: its live screen, apps and logs, and WebDriverAgent for
19
+ touch, typing and the element tree
20
+ - [Recording](recording.md) -- a screen as an MP4 or a GIF, with each touch drawn, and a demo in a pull request
21
+
16
22
  ## Change its settings
17
23
 
18
24
  - [The settings panel](settings.md) -- every setting from the viewer, and a code at the terminal for the ones a page
@@ -4,17 +4,18 @@ Pictures the README and the docs show. Every clone carries every picture ever co
4
4
  keeps them small: a GIF at most 3 MB, a still image at most 400 KB, the folder at most 25 MB. Videos are attached to a
5
5
  GitHub Release instead.
6
6
 
7
- Captured from real runs, never mocked up. All of these were taken on 2026-09-16 from one machine -- macOS 26.6.2,
8
- Xcode 26.6, an iPhone 17 Pro simulator on iOS 26.5 -- with SimMirror 0.1.0:
7
+ Captured from real runs, never mocked up. Most were taken on 2026-09-16 from one machine -- macOS 26.6.2, Xcode 26.6,
8
+ an iPhone 17 Pro simulator on iOS 26.5 -- with SimMirror 0.1.0; a row says so when its picture came from a later run:
9
9
 
10
10
  | File | Shows | How it was made |
11
11
  |---|---|---|
12
- | `hero.gif` | A Claude Code session tapping through Settings, its cursor drawn before each tap | Frames of the viewer while a real Claude Code session drove the device over MCP -- which is why the cursor's chip reads `claude-code` and the device says it is in use |
13
- | `cursor.gif` | The agent cursor gliding to a row and the screen following | The same run, the moment around one tap |
12
+ | `hero.gif` | A Claude Code session reading and writing calendar events, its cursor drawn before each tap | A screen recording made later, on 2026-09-17 with SimMirror 1.2.0, Xcode 27.0 and an iPhone 17 simulator on iOS 27.0, of a real Claude Code session driving a calendar app over MCP -- which is why the cursor's chip reads `claude-code` and the device says it is in use. Sped up four times and cut to 700 px wide to stay under the GIF limit; [the whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4) is attached to the 1.2.0 release |
13
+ | `cursor.gif` | The agent cursor gliding to a row and the screen following | The 2026-09-16 Settings run, the moment around one tap |
14
14
  | `viewer.png` | The viewer in a browser tab | A screenshot of the tab, live |
15
15
  | `snapshot-vs-screenshot.png` | A snapshot beside a screenshot of the same screen | The real answers of `sim_screenshot` and `sim_snapshot`, taken a moment apart, set side by side with their measured token estimates |
16
16
  | `iframe.png` | The viewer framed in another page | The `examples/iframe` page on its own origin, with a one-shot embed ticket |
17
17
  | `view-only.png` | The view-only mirror without idb_companion | The same viewer with `connectors.idb.companion_path` pointed at nothing, so the simctl connector serves it |
18
18
  | `doctor.png` | `sim-mirror doctor` on a Mac | The real output of `sim-mirror doctor --no-tap`, typeset for the docs rather than photographed from a terminal |
19
- | `social-preview.png` | GitHub's social preview, 1280×640 | The project's name and claims beside a frame of `hero.gif` |
19
+ | `social-preview.png` | GitHub's social preview, 1280×640 | The project's name and claims beside a frame of the Settings run `hero.gif` showed until 1.2.0 |
20
+ | `recording-demo.gif` | A recording an agent made: Settings › Appearance switched to dark and back, its taps drawn | `sim_record` itself, on 2026-09-29 with Xcode 27.0 and an iPhone 18 Pro simulator on iOS 27.0: an agent started a GIF recording with the demo status bar, tapped through Settings with `sim_act`, and stopped it; the file is as `sim_record stop` kept it, 600 px wide |
20
21
  | `settings-panel.png` | The settings panel's Server tab | A screenshot of the panel in Chrome, taken later on 2026-09-16 with Xcode 27.0 and an iOS 27.0 simulator, from a daemon started with `SIM_MIRROR_SERVER_PORT=7491` -- which is why the port shows as locked |
@@ -16,7 +16,7 @@ Its tools are named `mcp__plugin_sim-mirror_sim-mirror__sim_snapshot` and so on.
16
16
  ## Without the plugin
17
17
 
18
18
  ```sh
19
- claude mcp add sim-mirror -- uvx --from sim-mirror==1.2.0 sim-mirror mcp
19
+ claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
20
20
  ```
21
21
 
22
22
  Add `--scope project` before the name to write it to the project's `.mcp.json` for everyone working on it. Its tools
@@ -30,7 +30,7 @@ simulator (its scope) and this folder as the only place builds and installs may
30
30
  ## Watch it
31
31
 
32
32
  ```sh
33
- uvx --from sim-mirror==1.2.0 sim-mirror open
33
+ uvx --from sim-mirror==2.0.0 sim-mirror open
34
34
  ```
35
35
 
36
36
  opens this folder's viewer in your browser. Every gesture the agent makes is drawn there as a cursor just before it
@@ -6,7 +6,7 @@ Add [config.toml](config.toml)'s table to `~/.codex/config.toml`, then start Cod
6
6
  and installs may reach. The commented `args` pin a project by name and path instead.
7
7
  - `tool_timeout_sec` is raised because `sim_build_run` and `sim_test` can take minutes (they are there only when build
8
8
  tools are switched on).
9
- - Watch and take over in a browser: `uvx --from sim-mirror==1.2.0 sim-mirror open`
9
+ - Watch and take over in a browser: `uvx --from sim-mirror==2.0.0 sim-mirror open`
10
10
  in the same folder.
11
11
 
12
12
  The tools are the same for every client; see [claude-code-cli](../claude-code-cli/) for a first prompt to try.
@@ -6,7 +6,7 @@ one), and enable the server in Cursor's MCP settings.
6
6
  - `sim-mirror mcp` gives the agent the simulator of the folder it starts in, and that folder as the only place builds
7
7
  and installs may reach. If the tools act on the wrong project, append `"--scope", "my-app", "--root",
8
8
  "/absolute/path/to/my-app"` to `args`.
9
- - Watch and take over in a browser: `uvx --from sim-mirror==1.2.0 sim-mirror open`
9
+ - Watch and take over in a browser: `uvx --from sim-mirror==2.0.0 sim-mirror open`
10
10
  in the same folder.
11
11
 
12
12
  The tools are the same for every client; see [claude-code-cli](../claude-code-cli/) for a first prompt to try.
@@ -5,7 +5,7 @@ A Python application with SimMirror inside it: [app.py](app.py). No daemon runs;
5
5
 
6
6
  ```sh
7
7
  cd examples/fastapi-embed
8
- EXAMPLE_KEY=change-me uv run --with "sim-mirror==1.2.0" \
8
+ EXAMPLE_KEY=change-me uv run --with "sim-mirror==2.0.0" \
9
9
  uvicorn --factory app:create_app --port 7484
10
10
  ```
11
11
 
@@ -28,7 +28,15 @@ let package = Package(
28
28
  .executableTarget(
29
29
  name: "sim-mirror-helper",
30
30
  dependencies: ["HelperCore", "HelperPlatform"],
31
- swiftSettings: [.swiftLanguageMode(.v5)]
31
+ swiftSettings: [.swiftLanguageMode(.v5)],
32
+ // The Info.plist macOS reads to ask for the Camera, which reading a cabled device's screen needs, embedded
33
+ // in the program itself.
34
+ linkerSettings: [
35
+ .unsafeFlags([
36
+ "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist",
37
+ "-Xlinker", "\(Context.packageDirectory)/Support/Info.plist",
38
+ ]),
39
+ ]
32
40
  ),
33
41
  .testTarget(name: "HelperCoreTests", dependencies: ["HelperCore"]),
34
42
  ]
@@ -10,12 +10,21 @@ public enum Command: Equatable, Sendable {
10
10
  /// Reach a device the way serving would -- its screen, a picture, input and the element tree -- say what worked as
11
11
  /// JSON, and exit 0 only if all of it did.
12
12
  case selfCheck(DeviceOptions)
13
+ /// Render a recording as the job at a path asks -- an MP4, a GIF, its touches drawn in -- say what it wrote as JSON,
14
+ /// and exit.
15
+ case render(job: String)
16
+ /// Serve a cabled real device's screen, read through AVFoundation, on a unix socket until SimMirror lets go of it.
17
+ case capture(CaptureOptions)
13
18
 
14
19
  public static let usage = """
15
20
  usage: sim-mirror-helper version
16
21
  sim-mirror-helper serve --udid UDID --socket PATH [--parent-pid PID] [--hid auto|dtuhid|indigo]
17
22
  [--idle-key-frames on|off] [--log-level debug|info|warning|error]
18
23
  sim-mirror-helper self-check --udid UDID [--hid auto|dtuhid|indigo]
24
+ sim-mirror-helper render --job PATH
25
+ sim-mirror-helper capture --udid UDID --name NAME --socket PATH --width-px N --height-px N --scale N
26
+ [--capture-id ID] [--reference PATH] [--parent-pid PID] [--wait SECONDS] [--linger SECONDS]
27
+ [--idle-key-frames on|off] [--log-level debug|info|warning|error]
19
28
  """
20
29
 
21
30
  public static func parse(_ arguments: [String]) throws -> Command {
@@ -39,6 +48,16 @@ public enum Command: Equatable, Sendable {
39
48
  let device = try DeviceOptions.take(&options)
40
49
  try refuseLeftovers(options)
41
50
  return .selfCheck(device)
51
+ case "render":
52
+ guard let job = options.removeValue(forKey: "job"), job.hasPrefix("/") else {
53
+ throw HelperFailure("render needs --job and the absolute path of its job\n\(usage)", status: 400)
54
+ }
55
+ try refuseLeftovers(options)
56
+ return .render(job: job)
57
+ case "capture":
58
+ let capture = try CaptureOptions.take(&options)
59
+ try refuseLeftovers(options)
60
+ return .capture(capture)
42
61
  default:
43
62
  throw HelperFailure("not a command: \(verb)\n\(usage)", status: 400)
44
63
  }
@@ -122,18 +141,23 @@ public struct ServeOptions: Equatable, Sendable {
122
141
 
123
142
  /// What `sim-mirror-helper version` prints.
124
143
  public struct VersionReport: Equatable, Sendable, Encodable {
144
+ /// What this helper can do besides serving a simulator, so SimMirror asks only a helper that can.
145
+ public static let features = ["render", "capture"]
146
+
125
147
  public let version: String
126
148
  public let wire: Int
127
149
  public let coreSimulator: String?
150
+ public let features: [String]
128
151
 
129
152
  public init(coreSimulator: String?) {
130
153
  version = HelperVersion.current
131
154
  wire = Wire.version
132
155
  self.coreSimulator = coreSimulator
156
+ features = VersionReport.features
133
157
  }
134
158
 
135
159
  enum CodingKeys: String, CodingKey {
136
- case version, wire, coreSimulator = "core_simulator"
160
+ case version, wire, coreSimulator = "core_simulator", features
137
161
  }
138
162
 
139
163
  public func encode(to encoder: Encoder) throws {
@@ -141,6 +165,7 @@ public struct VersionReport: Equatable, Sendable, Encodable {
141
165
  try container.encode(version, forKey: .version)
142
166
  try container.encode(wire, forKey: .wire)
143
167
  try container.encode(coreSimulator, forKey: .coreSimulator)
168
+ try container.encode(features, forKey: .features)
144
169
  }
145
170
  }
146
171
 
@@ -0,0 +1,202 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import Foundation
3
+
4
+ /// What `sim-mirror-helper capture` is asked to serve: a cabled real device's screen, read the way QuickTime reads it.
5
+ public struct CaptureOptions: Equatable, Sendable {
6
+ /// The device's hardware UDID, as SimMirror knows it.
7
+ public let udid: String
8
+ /// The device's name, which is what its screen is called among the Mac's capture devices.
9
+ public let name: String
10
+ public let socket: String
11
+ /// The screen as devicectl measures it: its pixels and scale. The capture's own pixels may differ by one.
12
+ public let screen: ScreenGeometry
13
+ /// The capture device SimMirror found this device to be before, tried first.
14
+ public let captureId: String?
15
+ /// A picture of the screen taken another way, to tell apart devices that share a name.
16
+ public let reference: String?
17
+ public let parentPid: Int32?
18
+ public let idleKeyFrames: Bool
19
+ /// How long a picture of the screen is waited for: the first capture after the cable is plugged in switches it
20
+ /// into another mode, measured at 6.4 seconds on an iPhone 14 Pro (2026-09-29).
21
+ public let waitS: Double
22
+ /// How long the capture keeps running after the last picture was asked for, so the next one is quick.
23
+ public let lingerS: Double
24
+ public let logLevel: Log.Level
25
+
26
+ public init(
27
+ udid: String, name: String, socket: String, screen: ScreenGeometry, captureId: String? = nil, reference: String? = nil,
28
+ parentPid: Int32? = nil, idleKeyFrames: Bool = true, waitS: Double = CaptureOptions.defaultWaitS,
29
+ lingerS: Double = CaptureOptions.defaultLingerS, logLevel: Log.Level = .info
30
+ ) {
31
+ self.udid = udid
32
+ self.name = name
33
+ self.socket = socket
34
+ self.screen = screen
35
+ self.captureId = captureId
36
+ self.reference = reference
37
+ self.parentPid = parentPid
38
+ self.idleKeyFrames = idleKeyFrames
39
+ self.waitS = waitS
40
+ self.lingerS = lingerS
41
+ self.logLevel = logLevel
42
+ }
43
+
44
+ public static let defaultWaitS = 15.0
45
+ public static let waitRange = 1.0...120.0
46
+ public static let defaultLingerS = 30.0
47
+ public static let lingerRange = 0.0...600.0
48
+
49
+ static func take(_ options: inout [String: String]) throws -> CaptureOptions {
50
+ func required(_ key: String) throws -> String {
51
+ guard let value = options.removeValue(forKey: key), !value.isEmpty else {
52
+ throw HelperFailure("capture needs --\(key)\n\(Command.usage)", status: 400)
53
+ }
54
+ return value
55
+ }
56
+ func number(_ key: String) throws -> Double {
57
+ let text = try required(key)
58
+ guard let value = Double(text), value.isFinite else { throw HelperFailure("--\(key) is a number, not \(text)", status: 400) }
59
+ return value
60
+ }
61
+ func seconds(_ key: String, _ range: ClosedRange<Double>, otherwise: Double) throws -> Double {
62
+ guard let text = options.removeValue(forKey: key) else { return otherwise }
63
+ guard let value = Double(text), range.contains(value) else {
64
+ throw HelperFailure("--\(key) is \(Int(range.lowerBound)) to \(Int(range.upperBound)) seconds, not \(text)", status: 400)
65
+ }
66
+ return value
67
+ }
68
+ let udid = try required("udid")
69
+ let name = try required("name")
70
+ let socket = try required("socket")
71
+ let screen = try ScreenGeometry.of(pixelWidth: number("width-px"), pixelHeight: number("height-px"), scale: number("scale"))
72
+ let reference = options.removeValue(forKey: "reference")
73
+ if let reference, !reference.hasPrefix("/") {
74
+ throw HelperFailure("--reference is the absolute path of a picture, not \(reference)", status: 400)
75
+ }
76
+ return CaptureOptions(
77
+ udid: udid, name: name, socket: socket, screen: screen, captureId: options.removeValue(forKey: "capture-id"),
78
+ reference: reference, parentPid: try options.removeValue(forKey: "parent-pid").map(Command.pid),
79
+ idleKeyFrames: try options.removeValue(forKey: "idle-key-frames").map(Command.onOff) ?? true,
80
+ waitS: try seconds("wait", waitRange, otherwise: defaultWaitS),
81
+ lingerS: try seconds("linger", lingerRange, otherwise: defaultLingerS),
82
+ logLevel: try options.removeValue(forKey: "log-level").map(Command.logLevel) ?? .info
83
+ )
84
+ }
85
+ }
86
+
87
+
88
+ /// One of the Mac's capture devices that shows a cabled device's screen.
89
+ public struct CaptureCandidate: Equatable, Sendable {
90
+ public let id: String
91
+ public let name: String
92
+
93
+ public init(id: String, name: String) {
94
+ self.id = id
95
+ self.name = name
96
+ }
97
+ }
98
+
99
+ /// Which capture device is the one asked for.
100
+ ///
101
+ /// A capture device is named for the device it shows, but its id is its own -- not the device's UDID -- so it is found
102
+ /// by the id it had before, else by its name, else, when several devices share the name, by which of them shows the
103
+ /// picture of the screen taken another way (`Likeness`).
104
+ public enum CaptureChoice: Equatable, Sendable {
105
+ case use(String)
106
+ /// Several are called that: the one whose screen looks like the reference.
107
+ case compare([String])
108
+ case none
109
+
110
+ public static func pick(_ candidates: [CaptureCandidate], name: String, captureId: String?) -> CaptureChoice {
111
+ if let captureId, candidates.contains(where: { $0.id == captureId }) { return .use(captureId) }
112
+ let named = candidates.filter { $0.name == name }.map(\.id)
113
+ switch named.count {
114
+ case 0: return .none
115
+ case 1: return .use(named[0])
116
+ default: return .compare(named)
117
+ }
118
+ }
119
+ }
120
+
121
+ /// How alike two pictures of a screen are, each shrunk to the same few grey pixels: the mean difference, 0 to 255.
122
+ ///
123
+ /// A screen captured by cable and one taken by devicectl a moment apart differ in the clock and little else, so the
124
+ /// device whose picture is nearest the reference is the one the reference was taken of.
125
+ public enum Likeness {
126
+ /// The size pictures are shrunk to before they are compared.
127
+ public static let width = 12
128
+ public static let height = 26
129
+
130
+ public static func distance(_ one: [UInt8], _ other: [UInt8]) -> Double? {
131
+ guard one.count == other.count, !one.isEmpty else { return nil }
132
+ let total = zip(one, other).reduce(0) { $0 + abs(Int($1.0) - Int($1.1)) }
133
+ return Double(total) / Double(one.count)
134
+ }
135
+
136
+ /// The index of the picture nearest the reference, or nil when none can be compared with it.
137
+ public static func nearest(_ reference: [UInt8], among pictures: [[UInt8]?]) -> Int? {
138
+ let scored = pictures.enumerated().compactMap { index, picture in
139
+ picture.flatMap { distance(reference, $0) }.map { (index, $0) }
140
+ }
141
+ return scored.min { $0.1 < $1.1 }?.0
142
+ }
143
+ }
144
+
145
+ /// Whether a captured picture is turned from the portrait SimMirror serves: a device held on its side sends its
146
+ /// screen wider than tall, and it is turned back a quarter so points map onto it as they do in portrait.
147
+ public enum CaptureTurn: Equatable, Sendable {
148
+ case none
149
+ case quarter
150
+
151
+ public static func of(width: Int, height: Int) -> CaptureTurn {
152
+ width > height ? .quarter : .none
153
+ }
154
+ }
155
+
156
+ /// When the capture runs: while anything holds it -- a stream -- and for a while after the last picture was asked for,
157
+ /// so a device's status bar reads 9:41 and its cable carries a picture only while someone is looking.
158
+ public struct CaptureDemand: Equatable, Sendable {
159
+ public let lingerS: Double
160
+ public private(set) var holders = 0
161
+ public private(set) var lastWanted: Double?
162
+
163
+ public init(lingerS: Double) {
164
+ self.lingerS = lingerS
165
+ }
166
+
167
+ /// Something needs the capture from now until it lets go.
168
+ public mutating func hold(at time: Double) {
169
+ holders += 1
170
+ lastWanted = time
171
+ }
172
+
173
+ public mutating func release(at time: Double) {
174
+ holders = max(0, holders - 1)
175
+ lastWanted = time
176
+ }
177
+
178
+ /// Whether the capture should be running at `time`.
179
+ public func running(at time: Double) -> Bool {
180
+ guard let lastWanted else { return false }
181
+ return holders > 0 || time - lastWanted < lingerS
182
+ }
183
+
184
+ /// When nothing will need the capture any more if nothing asks again, or nil while something holds it.
185
+ public func idleAt() -> Double? {
186
+ guard holders == 0, let lastWanted else { return nil }
187
+ return lastWanted + lingerS
188
+ }
189
+ }
190
+
191
+ /// Whether this process is the one that asks macOS for the Camera, or has to start one that is.
192
+ ///
193
+ /// macOS asks permission of a process's responsible process: under a terminal or an agent, that is the terminal or the
194
+ /// agent, which has not asked for the Camera and never will, so the capture quietly gets no pictures. Started again
195
+ /// with its responsibility disclaimed, the helper is its own responsible process and asks for itself.
196
+ public enum Disclaim {
197
+ public static let variable = "SIM_MIRROR_HELPER_DISCLAIMED"
198
+
199
+ public static func needed(_ environment: [String: String]) -> Bool {
200
+ environment[variable] != "1"
201
+ }
202
+ }