sim-mirror 0.2.0__py3-none-any.whl

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 (186) hide show
  1. sim_mirror/__init__.py +6 -0
  2. sim_mirror/__main__.py +7 -0
  3. sim_mirror/_version.py +4 -0
  4. sim_mirror/api.py +98 -0
  5. sim_mirror/build/__init__.py +2 -0
  6. sim_mirror/build/destination.py +82 -0
  7. sim_mirror/build/provider.py +176 -0
  8. sim_mirror/build/xcodebuild.py +709 -0
  9. sim_mirror/build/xcresult.py +365 -0
  10. sim_mirror/cli/__init__.py +2 -0
  11. sim_mirror/cli/config_command.py +100 -0
  12. sim_mirror/cli/context.py +128 -0
  13. sim_mirror/cli/devices.py +43 -0
  14. sim_mirror/cli/doctor.py +75 -0
  15. sim_mirror/cli/main.py +61 -0
  16. sim_mirror/cli/mcp.py +34 -0
  17. sim_mirror/cli/open_viewer.py +36 -0
  18. sim_mirror/cli/serve.py +73 -0
  19. sim_mirror/cli/settings_command.py +38 -0
  20. sim_mirror/cli/tokens.py +65 -0
  21. sim_mirror/cli/tools.py +32 -0
  22. sim_mirror/cli/version.py +31 -0
  23. sim_mirror/cli/xcode.py +90 -0
  24. sim_mirror/config/__init__.py +3 -0
  25. sim_mirror/config/discovery.py +19 -0
  26. sim_mirror/config/env.py +34 -0
  27. sim_mirror/config/model.py +79 -0
  28. sim_mirror/config/provenance.py +39 -0
  29. sim_mirror/config/schema.py +482 -0
  30. sim_mirror/config/settings_store.py +34 -0
  31. sim_mirror/config/toml_source.py +139 -0
  32. sim_mirror/config/writer.py +218 -0
  33. sim_mirror/connectors/__init__.py +7 -0
  34. sim_mirror/connectors/base.py +228 -0
  35. sim_mirror/connectors/idb/__init__.py +2 -0
  36. sim_mirror/connectors/idb/companion.py +351 -0
  37. sim_mirror/connectors/idb/connector.py +91 -0
  38. sim_mirror/connectors/idb/engine.py +149 -0
  39. sim_mirror/connectors/idb/frameworks.py +51 -0
  40. sim_mirror/connectors/idb/proto/__init__.py +8 -0
  41. sim_mirror/connectors/idb/proto/idb.proto +1301 -0
  42. sim_mirror/connectors/idb/proto/idb_grpc.py +776 -0
  43. sim_mirror/connectors/idb/proto/idb_pb2.py +452 -0
  44. sim_mirror/connectors/mcpbridge/__init__.py +3 -0
  45. sim_mirror/connectors/mcpbridge/client.py +264 -0
  46. sim_mirror/connectors/mcpbridge/connector.py +84 -0
  47. sim_mirror/connectors/mcpbridge/hierarchy.py +225 -0
  48. sim_mirror/connectors/mcpbridge/merge.py +68 -0
  49. sim_mirror/connectors/mcpbridge/reader.py +220 -0
  50. sim_mirror/connectors/registry.py +136 -0
  51. sim_mirror/connectors/simctl/__init__.py +2 -0
  52. sim_mirror/connectors/simctl/capture.py +79 -0
  53. sim_mirror/connectors/simctl/connector.py +74 -0
  54. sim_mirror/core/__init__.py +3 -0
  55. sim_mirror/core/actions.py +541 -0
  56. sim_mirror/core/availability.py +61 -0
  57. sim_mirror/core/devices.py +163 -0
  58. sim_mirror/core/events.py +46 -0
  59. sim_mirror/core/frames.py +308 -0
  60. sim_mirror/core/gestures.py +164 -0
  61. sim_mirror/core/instance.py +126 -0
  62. sim_mirror/core/manager.py +541 -0
  63. sim_mirror/core/reaper.py +64 -0
  64. sim_mirror/core/runtime.py +214 -0
  65. sim_mirror/core/screen_input.py +170 -0
  66. sim_mirror/core/screen_relay.py +275 -0
  67. sim_mirror/core/status.py +58 -0
  68. sim_mirror/core/text_entry.py +64 -0
  69. sim_mirror/core/tickets.py +43 -0
  70. sim_mirror/daemon/__init__.py +2 -0
  71. sim_mirror/daemon/app.py +332 -0
  72. sim_mirror/daemon/auth.py +164 -0
  73. sim_mirror/daemon/confirmations.py +110 -0
  74. sim_mirror/daemon/health.py +47 -0
  75. sim_mirror/daemon/lease.py +41 -0
  76. sim_mirror/daemon/lifecycle.py +77 -0
  77. sim_mirror/daemon/passes.py +83 -0
  78. sim_mirror/daemon/policy.py +37 -0
  79. sim_mirror/daemon/tokens.py +302 -0
  80. sim_mirror/doctor/__init__.py +2 -0
  81. sim_mirror/doctor/checks.py +303 -0
  82. sim_mirror/doctor/macos.py +67 -0
  83. sim_mirror/doctor/report.py +59 -0
  84. sim_mirror/doctor/tap.py +139 -0
  85. sim_mirror/host_copy.py +136 -0
  86. sim_mirror/hosting.py +190 -0
  87. sim_mirror/mcp/__init__.py +2 -0
  88. sim_mirror/mcp/launcher.py +252 -0
  89. sim_mirror/mcp/relay.py +316 -0
  90. sim_mirror/perception/__init__.py +9 -0
  91. sim_mirror/perception/budget.py +66 -0
  92. sim_mirror/perception/model.py +65 -0
  93. sim_mirror/perception/readers.py +204 -0
  94. sim_mirror/perception/settle.py +93 -0
  95. sim_mirror/perception/snapshot.py +252 -0
  96. sim_mirror/perception/wait.py +73 -0
  97. sim_mirror/platform/__init__.py +3 -0
  98. sim_mirror/platform/developer_dir.py +72 -0
  99. sim_mirror/platform/images.py +51 -0
  100. sim_mirror/platform/keyboard.py +29 -0
  101. sim_mirror/platform/process.py +105 -0
  102. sim_mirror/platform/simctl.py +275 -0
  103. sim_mirror/platform/xcode.py +20 -0
  104. sim_mirror/platform/xcrun.py +129 -0
  105. sim_mirror/protocol/__init__.py +39 -0
  106. sim_mirror/protocol/_generated.py +605 -0
  107. sim_mirror/protocol/messages.py +168 -0
  108. sim_mirror/py.typed +0 -0
  109. sim_mirror/scope.py +67 -0
  110. sim_mirror/seams.py +236 -0
  111. sim_mirror/server/__init__.py +2 -0
  112. sim_mirror/server/agent_routes.py +54 -0
  113. sim_mirror/server/envelope.py +28 -0
  114. sim_mirror/server/http_routes.py +93 -0
  115. sim_mirror/server/log_redaction.py +45 -0
  116. sim_mirror/server/pages.py +59 -0
  117. sim_mirror/server/security.py +161 -0
  118. sim_mirror/server/settings_routes.py +188 -0
  119. sim_mirror/server/settings_view.py +82 -0
  120. sim_mirror/server/socket_routes.py +61 -0
  121. sim_mirror/server/static/viewer/embed.html +19 -0
  122. sim_mirror/server/static/viewer/index.html +19 -0
  123. sim_mirror/server/static/viewer/start.js +25 -0
  124. sim_mirror/storage/__init__.py +3 -0
  125. sim_mirror/storage/app_support.py +100 -0
  126. sim_mirror/storage/claims.py +146 -0
  127. sim_mirror/storage/private.py +135 -0
  128. sim_mirror/testing/__init__.py +2 -0
  129. sim_mirror/testing/asgi.py +96 -0
  130. sim_mirror/testing/contract.py +73 -0
  131. sim_mirror/testing/fakes.py +732 -0
  132. sim_mirror/testing/fixtures/ax-alert-ios27.json +162 -0
  133. sim_mirror/testing/fixtures/ax-home-complete.json +1 -0
  134. sim_mirror/testing/fixtures/ax-home-ios27.json +447 -0
  135. sim_mirror/testing/fixtures/ax-keyboard-ios27.json +1261 -0
  136. sim_mirror/testing/fixtures/ax-probe-ios27.json +444 -0
  137. sim_mirror/testing/fixtures/ax-safari-ios27.json +188 -0
  138. sim_mirror/testing/fixtures/ax-safari-typed.json +1 -0
  139. sim_mirror/testing/fixtures/ax-settings-interactable.json +1 -0
  140. sim_mirror/testing/fixtures/ax-settings-ios27.json +425 -0
  141. sim_mirror/testing/fixtures/mcpbridge-answers.json +183 -0
  142. sim_mirror/testing/fixtures/mcpbridge-hierarchy-alert.txt +46 -0
  143. sim_mirror/testing/fixtures/mcpbridge-hierarchy-home.txt +257 -0
  144. sim_mirror/testing/fixtures/mcpbridge-hierarchy-keyboard.txt +92 -0
  145. sim_mirror/testing/fixtures/mcpbridge-hierarchy-probe.txt +39 -0
  146. sim_mirror/testing/fixtures/mcpbridge-hierarchy-safari.txt +77 -0
  147. sim_mirror/testing/fixtures/mcpbridge-hierarchy-settings.txt +169 -0
  148. sim_mirror/testing/fixtures/simctl-devices.json +73 -0
  149. sim_mirror/testing/fixtures/simctl-runtimes.json +114 -0
  150. sim_mirror/testing/fixtures/xcodebuild-list-names.json +20 -0
  151. sim_mirror/testing/fixtures/xcodebuild-list.json +17 -0
  152. sim_mirror/testing/fixtures/xcodebuild-settings.json +13 -0
  153. sim_mirror/testing/fixtures/xcodebuild-test-plans-none.json +3 -0
  154. sim_mirror/testing/fixtures/xcodebuild-test-plans.json +10 -0
  155. sim_mirror/testing/fixtures/xcresult-build-broken.json +32 -0
  156. sim_mirror/testing/fixtures/xcresult-build-ok.json +27 -0
  157. sim_mirror/testing/fixtures/xcresult-build-results-unbuilt.json +36 -0
  158. sim_mirror/testing/fixtures/xcresult-test-summary-retries.json +56 -0
  159. sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing-xcode27.json +63 -0
  160. sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing.json +60 -0
  161. sim_mirror/testing/fixtures/xcresult-test-summary-unbuilt.json +24 -0
  162. sim_mirror/testing/fixtures/xcresult-test-summary.json +49 -0
  163. sim_mirror/testing/fixtures/xcresult-test-tests-retries.json +165 -0
  164. sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing-xcode27.json +135 -0
  165. sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing.json +125 -0
  166. sim_mirror/testing/fixtures/xcresult-test-tests.json +93 -0
  167. sim_mirror/testing/guards.py +129 -0
  168. sim_mirror/testing/rig.py +163 -0
  169. sim_mirror/tools/__init__.py +2 -0
  170. sim_mirror/tools/act.py +31 -0
  171. sim_mirror/tools/app.py +127 -0
  172. sim_mirror/tools/context.py +120 -0
  173. sim_mirror/tools/device.py +51 -0
  174. sim_mirror/tools/registry.py +82 -0
  175. sim_mirror/tools/results.py +30 -0
  176. sim_mirror/tools/schemas.py +170 -0
  177. sim_mirror/tools/screenshot.py +28 -0
  178. sim_mirror/tools/snapshot.py +23 -0
  179. sim_mirror/validation.py +46 -0
  180. sim_mirror-0.2.0.dist-info/METADATA +210 -0
  181. sim_mirror-0.2.0.dist-info/RECORD +186 -0
  182. sim_mirror-0.2.0.dist-info/WHEEL +4 -0
  183. sim_mirror-0.2.0.dist-info/entry_points.txt +2 -0
  184. sim_mirror-0.2.0.dist-info/licenses/LICENSE +202 -0
  185. sim_mirror-0.2.0.dist-info/licenses/NOTICE +14 -0
  186. sim_mirror-0.2.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +61 -0
@@ -0,0 +1,82 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Which tools an agent is offered, and running a call to one.
3
+
4
+ Tools come from providers: the device tools always, the build tools while ``build.tools`` is on -- and a host or a
5
+ later version adds a provider rather than editing this. A manifest leaves out a tool the scope's connector cannot
6
+ serve (a view-only mirror offers no `sim_act`), and a call to one anyway is refused with why (`context.ready_device`).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from collections.abc import Collection, Sequence
13
+ from typing import Any, Protocol
14
+
15
+ from sim_mirror.build.provider import BuildToolProvider
16
+ from sim_mirror.build.xcodebuild import BuildRefused
17
+ from sim_mirror.config.model import SimConfig
18
+ from sim_mirror.connectors.base import Capability
19
+ from sim_mirror.core.actions import ActionError
20
+ from sim_mirror.platform.simctl import SimctlError
21
+ from sim_mirror.tools import act, app, device, screenshot, snapshot
22
+ from sim_mirror.tools.context import Tool, ToolContext
23
+ from sim_mirror.tools.results import Result, ToolRefused, text
24
+ from sim_mirror.tools.schemas import LOOK_AND_ACT
25
+
26
+
27
+ class ToolProvider(Protocol):
28
+ @property
29
+ def tools(self) -> tuple[Tool, ...]:
30
+ """Every tool this provider has, whether offered now or not."""
31
+ ...
32
+
33
+ def offered(self, config: SimConfig) -> bool:
34
+ """Whether a scope with these settings is offered the tools."""
35
+ ...
36
+
37
+ def instructions(self, config: SimConfig) -> str:
38
+ """What an agent is told, for these settings."""
39
+ ...
40
+
41
+
42
+ class DeviceToolProvider:
43
+ """Looking at and touching the device, and its apps: always offered."""
44
+
45
+ tools: tuple[Tool, ...] = (device.TOOL, snapshot.TOOL, screenshot.TOOL, act.TOOL, app.TOOL)
46
+
47
+ def offered(self, config: SimConfig) -> bool:
48
+ return True
49
+
50
+ def instructions(self, config: SimConfig) -> str:
51
+ return LOOK_AND_ACT
52
+
53
+
54
+ class ToolRegistry:
55
+ def __init__(self, providers: Sequence[ToolProvider] = (DeviceToolProvider(), BuildToolProvider())) -> None:
56
+ self._providers = tuple(providers)
57
+ self._tools = {tool.name: tool for provider in self._providers for tool in provider.tools}
58
+
59
+ def names(self) -> list[str]:
60
+ return list(self._tools)
61
+
62
+ def manifest(self, config: SimConfig, capabilities: Collection[Capability]) -> dict[str, Any]:
63
+ """The tools a scope with these settings is offered on a connector with these capabilities, and how to use
64
+ them."""
65
+ can = frozenset(capabilities)
66
+ offered = [provider for provider in self._providers if provider.offered(config)]
67
+ return {
68
+ "tools": [tool.listing() for provider in offered for tool in provider.tools if tool.needs <= can],
69
+ "instructions": "".join(provider.instructions(config) for provider in self._providers),
70
+ }
71
+
72
+ async def call(self, name: object, arguments: object, ctx: ToolContext) -> Result:
73
+ """Run one tool call, answering with its result -- or with why not, as an error result."""
74
+ tool = self._tools.get(name) if isinstance(name, str) else None
75
+ if tool is None:
76
+ return text(f"there is no tool named {name!r}", error=True)
77
+ if not isinstance(arguments, dict):
78
+ return text("arguments must be an object", error=True)
79
+ try:
80
+ return await tool.handler(arguments, dataclasses.replace(ctx, tool=tool))
81
+ except (ToolRefused, ActionError, SimctlError, BuildRefused) as exc:
82
+ return text(str(exc), error=True)
@@ -0,0 +1,30 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """A tool's answer, shaped as MCP's: content blocks, and whether it is an error.
3
+
4
+ Text first: an image is sent only when one was asked for, because an image costs an agent far more than the lines that
5
+ say what is on the screen (`perception.snapshot`).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ from collections.abc import Sequence
12
+ from typing import Any
13
+
14
+ Result = dict[str, Any]
15
+
16
+
17
+ class ToolRefused(Exception):
18
+ """A call a tool will not make, said so the agent knows what to do instead."""
19
+
20
+
21
+ def text(value: str, *, error: bool = False) -> Result:
22
+ return {"content": [{"type": "text", "text": value}], "isError": error}
23
+
24
+
25
+ def images(caption: str, jpegs: Sequence[bytes]) -> Result:
26
+ """A caption, then each JPEG."""
27
+ blocks = [
28
+ {"type": "image", "data": base64.b64encode(jpeg).decode("ascii"), "mimeType": "image/jpeg"} for jpeg in jpegs
29
+ ]
30
+ return {"content": [{"type": "text", "text": caption}, *blocks], "isError": False}
@@ -0,0 +1,170 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Every agent tool's description and input schema, and the instructions an MCP client is given -- the one catalogue.
3
+
4
+ The tool names are part of SimMirror's public surface: an agent's prompts, a plugin's skill and a host's permissions
5
+ name them. `scripts/gen_docs.py` writes `docs/reference/tools.md` from here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from sim_mirror.build.destination import KEYS as DESTINATION_KEYS
13
+ from sim_mirror.build.destination import TEXT_MAX as DESTINATION_TEXT_MAX
14
+ from sim_mirror.build.xcodebuild import TEST_ID_MAX, TESTS_MAX
15
+ from sim_mirror.validation import XCODE_NAME_MAX
16
+
17
+ LAUNCH_ARGS_MAX = 20
18
+ LOG_SINCE_S = (1, 300)
19
+ LOG_LINES = (1, 200)
20
+ #: How long a build call waits before answering with its build id; within a relay's own limit for a call.
21
+ BUILD_WAIT_S = 120
22
+ BUILD_WAIT_BOUNDS = (0, 600)
23
+ #: How many extra goes a failing test may be given. Bounded low: each one runs the test again, and a test that needs
24
+ #: four attempts has told you what you needed to know by the second.
25
+ TEST_RETRIES_BOUNDS = (0, 3)
26
+
27
+
28
+ def schema(properties: dict[str, Any], required: tuple[str, ...] = ()) -> dict[str, Any]:
29
+ return {"type": "object", "properties": properties, "required": list(required), "additionalProperties": False}
30
+
31
+
32
+ _NAME: dict[str, Any] = {"type": "string", "maxLength": XCODE_NAME_MAX}
33
+ _BUILD_PROPERTIES: dict[str, Any] = {
34
+ "scheme": {**_NAME, "description": "needed only when the project has several"},
35
+ "project": {"type": "string", "description": "a .xcodeproj in your folder, when it has several"},
36
+ "workspace": {"type": "string", "description": "a .xcworkspace in your folder, when it has several"},
37
+ "configuration": {**_NAME, "description": "Debug unless the settings chose another"},
38
+ "warnings": {"type": "boolean", "description": "list warnings too, not only errors"},
39
+ "wait_s": {"type": "integer", "minimum": BUILD_WAIT_BOUNDS[0], "maximum": BUILD_WAIT_BOUNDS[1]},
40
+ "build_id": {"type": "string", "description": "a run that answered still running: wait for it again"},
41
+ }
42
+ _TEST_IDS = {
43
+ "type": "array",
44
+ "items": {"type": "string", "maxLength": TEST_ID_MAX},
45
+ "maxItems": TESTS_MAX,
46
+ "description": "tests as a failure names them: AppTests, AppTests/LoginTests or AppTests/LoginTests/testLogin",
47
+ }
48
+ _DESTINATION_TEXT: dict[str, Any] = {"type": "string", "maxLength": DESTINATION_TEXT_MAX}
49
+ _DESTINATION = {
50
+ "type": "object",
51
+ "properties": {key: _DESTINATION_TEXT for key in sorted(DESTINATION_KEYS)},
52
+ "additionalProperties": False,
53
+ "description": 'another simulator on this Mac: {"name": "iPhone 17"} or {"udid": ...}, with "runtime": '
54
+ '"iOS 26.5" to choose between simulators of one name',
55
+ }
56
+
57
+ #: Each tool's description and input schema, by name, in the order a manifest lists them.
58
+ SCHEMAS: dict[str, tuple[str, dict[str, Any]]] = {
59
+ "sim_device": (
60
+ "Your iOS Simulator. info: which device, its state and the xcodebuild destination (does not start it). boot: "
61
+ "start it and wait until it is ready. restart: shut it down and start it again -- for when its apps stop "
62
+ "answering sim_snapshot, as they can after UI tests. appearance: light or dark.",
63
+ schema(
64
+ {
65
+ "action": {"type": "string", "enum": ["info", "boot", "restart", "appearance"]},
66
+ "mode": {"type": "string", "enum": ["light", "dark"]},
67
+ }
68
+ ),
69
+ ),
70
+ "sim_snapshot": (
71
+ "What is on screen as short lines -- one per button, field, text or heading, each with a ref (e4) and where a "
72
+ "tap lands. Cheap: prefer it to a screenshot. mode diff (default) gives only what changed since you last "
73
+ "looked; full gives the whole screen.",
74
+ schema({"mode": {"type": "string", "enum": ["diff", "full"]}}),
75
+ ),
76
+ "sim_screenshot": (
77
+ "A JPEG of the screen, or of a region (a ref, or {x, y, w, h} in points). frames > 1 takes that many, "
78
+ "interval_ms apart, to see an animation. Costs far more than sim_snapshot: use it to check how something "
79
+ "looks.",
80
+ schema(
81
+ {
82
+ "width": {"type": "integer", "minimum": 160, "maximum": 1200},
83
+ "region": {"oneOf": [{"type": "string"}, {"type": "object"}]},
84
+ "frames": {"type": "integer", "minimum": 1, "maximum": 6},
85
+ "interval_ms": {"type": "integer", "minimum": 50, "maximum": 2000},
86
+ }
87
+ ),
88
+ ),
89
+ "sim_act": (
90
+ "Touch the device: a batch of steps in one call, played in order, each drawn live for anyone watching. "
91
+ 'Steps: {"tap": T}, {"long_press": T, "ms": 800}, {"swipe": {"from": T, "direction": "up"}} or {"swipe": '
92
+ '{"from": T, "to": [x, y]}}, {"drag": {"path": [[x, y], ...]}}, {"type": "Groceries", "into": T, "clear": '
93
+ 'true, "submit": true} (the words to type are the value of "type"; typing adds to what a field already '
94
+ 'holds, "clear": true replaces it), {"press": "home" | "lock" | "return" | "delete" | ...}, {"pause": ms}. A '
95
+ 'key a step does not take is refused. T is a ref like "e4" or a point [x, y]. wait after the steps: {"for": '
96
+ 'text}, {"gone": text} or {"settle_ms": 400} for an animation. Answers ok/error per step, then what changed '
97
+ "on screen.",
98
+ schema(
99
+ {
100
+ "steps": {"type": "array", "minItems": 1, "maxItems": 20, "items": {"type": "object"}},
101
+ "wait": {"type": "object"},
102
+ "snapshot": {"type": "string", "enum": ["diff", "full", "none"]},
103
+ },
104
+ required=("steps",),
105
+ ),
106
+ ),
107
+ "sim_app": (
108
+ "Apps on the device. launch or terminate a bundle_id -- launching an app that is already running only brings "
109
+ "it to the front, so give relaunch: true to start it fresh; install a built .app (a path in a folder you may "
110
+ "install from, or DerivedData); open_url; logs: the device log for the last since_s seconds, for a bundle_id "
111
+ "(else errors and faults), optionally filtered.",
112
+ schema(
113
+ {
114
+ "action": {"type": "string", "enum": ["launch", "terminate", "install", "open_url", "logs"]},
115
+ "bundle_id": {"type": "string"},
116
+ "relaunch": {"type": "boolean"},
117
+ "path": {"type": "string"},
118
+ "url": {"type": "string"},
119
+ "args": {"type": "array", "items": {"type": "string"}, "maxItems": LAUNCH_ARGS_MAX},
120
+ "since_s": {"type": "integer", "minimum": LOG_SINCE_S[0], "maximum": LOG_SINCE_S[1]},
121
+ "filter": {"type": "string"},
122
+ "lines": {"type": "integer", "minimum": LOG_LINES[0], "maximum": LOG_LINES[1]},
123
+ },
124
+ required=("action",),
125
+ ),
126
+ ),
127
+ "sim_build_run": (
128
+ "Build the app in your folder with xcodebuild for your simulator, then install and launch it. Answers ok, or "
129
+ "each error as file:line and message. A build longer than wait_s answers with its build_id: call again with "
130
+ "that build_id to keep waiting.",
131
+ schema(_BUILD_PROPERTIES),
132
+ ),
133
+ "sim_test": (
134
+ "Run the scheme's tests (unit and UI) on your simulator, or on another one with destination. Answers with the "
135
+ "counts and each failure where it happened, named as only_testing takes it back -- or, when the tests do not "
136
+ "build, with the compile errors. While they run your device takes no sim_act steps. A test that fails, is "
137
+ "retried and then passes is reported as flaky: worth knowing before you treat a green run as a fix.",
138
+ schema(
139
+ {
140
+ **_BUILD_PROPERTIES,
141
+ "only_testing": _TEST_IDS,
142
+ "skip_testing": _TEST_IDS,
143
+ "test_plan": {
144
+ **_NAME,
145
+ "description": "one of the scheme's test plans; leave out to run what the scheme runs by default",
146
+ },
147
+ "retries": {
148
+ "type": "integer",
149
+ "minimum": TEST_RETRIES_BOUNDS[0],
150
+ "maximum": TEST_RETRIES_BOUNDS[1],
151
+ "description": "more goes for a failing test",
152
+ },
153
+ "destination": _DESTINATION,
154
+ }
155
+ ),
156
+ ),
157
+ }
158
+
159
+ LOOK_AND_ACT = (
160
+ "These tools drive your iOS Simulator while anyone watching sees every gesture live. Look with sim_snapshot "
161
+ "(cheap text) before acting, then act with its refs in one sim_act batch; take sim_screenshot only to check how "
162
+ "something looks or moves. The device boots on first use. "
163
+ )
164
+ SHELL_INSTRUCTIONS = (
165
+ "Build from your shell with xcodebuild and the destination sim_device info gives, then sim_app install and launch."
166
+ )
167
+ BUILD_INSTRUCTIONS = (
168
+ "Build, install and launch the app with sim_build_run, and run its tests with sim_test: both answer with only what "
169
+ "failed and where."
170
+ )
@@ -0,0 +1,28 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`sim_screenshot`: a JPEG of the screen or a region of it, or several to see an animation."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from sim_mirror.connectors.base import Capability
9
+ from sim_mirror.tools.context import ToolContext, make_tool, ready_device
10
+ from sim_mirror.tools.results import Result, images
11
+
12
+
13
+ async def run(args: dict[str, Any], ctx: ToolContext) -> Result:
14
+ instance = await ready_device(ctx)
15
+ shots = await ctx.actions.screenshots(
16
+ instance,
17
+ ctx.caller,
18
+ width=args.get("width", ctx.config.screenshot_width),
19
+ region=args.get("region"),
20
+ frames=args.get("frames", 1),
21
+ interval_ms=args.get("interval_ms"),
22
+ max_elements=ctx.config.snapshot_max_elements,
23
+ )
24
+ caption = f"{len(shots)} frame{'s' if len(shots) != 1 else ''} · {shots[0].width}x{shots[0].height}px"
25
+ return images(caption, [shot.jpeg for shot in shots])
26
+
27
+
28
+ TOOL = make_tool("sim_screenshot", (Capability.SCREENSHOT,), run)
@@ -0,0 +1,23 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`sim_snapshot`: the screen as short lines with refs, or what changed since the agent last looked."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from sim_mirror.connectors.base import Capability
9
+ from sim_mirror.tools.context import ToolContext, make_tool, ready_device
10
+ from sim_mirror.tools.results import Result, ToolRefused, text
11
+
12
+
13
+ async def run(args: dict[str, Any], ctx: ToolContext) -> Result:
14
+ mode = args.get("mode", "diff")
15
+ if mode not in ("diff", "full"):
16
+ raise ToolRefused("mode is diff or full")
17
+ instance = await ready_device(ctx)
18
+ return text(
19
+ await ctx.actions.snapshot(instance, ctx.caller, mode=mode, max_elements=ctx.config.snapshot_max_elements)
20
+ )
21
+
22
+
23
+ TOOL = make_tool("sim_snapshot", (Capability.ELEMENT_TREE,), run)
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Checking the values an agent's call names, with messages that say what would do."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import TypeGuard
7
+
8
+ #: The longest scheme, build configuration or test plan name a call or a setting may give.
9
+ XCODE_NAME_MAX = 128
10
+
11
+
12
+ class Invalid(ValueError):
13
+ """A value a call named that cannot be used, said so the caller knows what to give instead."""
14
+
15
+
16
+ def is_number(value: object) -> bool:
17
+ """A real number: an int or a float, never a bool."""
18
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
19
+
20
+
21
+ def is_xcode_name(value: object) -> TypeGuard[str]:
22
+ """A name Xcode lets a person give a scheme, a build configuration or a test plan -- `App (Staging)` as much as
23
+ `Debug` -- that reaches xcodebuild as nothing but that name: one printable line, trimmed, and not an option.
24
+
25
+ xcodebuild is run with an argument vector, never a shell, so punctuation means nothing to it; a leading `-` would,
26
+ as the option after `-scheme`. Anything narrower refuses names Xcode made, and an agent told "it has App
27
+ (Staging)" and then refused for using it can only try again.
28
+ """
29
+ return (
30
+ isinstance(value, str)
31
+ and 0 < len(value) <= XCODE_NAME_MAX
32
+ and value.isprintable()
33
+ and value == value.strip()
34
+ and not value.startswith("-")
35
+ )
36
+
37
+
38
+ def whole(value: object, default: int, bounds: tuple[int, int], name: str, unit: str | None = "milliseconds") -> int:
39
+ """`value` as a whole number within `bounds`, `default` when it is None. Raises `Invalid` otherwise."""
40
+ if value is None:
41
+ return default
42
+ low, high = bounds
43
+ if not isinstance(value, int) or isinstance(value, bool) or not low <= value <= high:
44
+ of_unit = f" of {unit}" if unit else ""
45
+ raise Invalid(f"{name} must be a whole number{of_unit} from {low} to {high}")
46
+ return value
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.5
2
+ Name: sim-mirror
3
+ Version: 0.2.0
4
+ Summary: Mirror and drive the iOS Simulator from Claude Code, CLI agents and the browser.
5
+ Project-URL: Homepage, https://github.com/AndrewKochulab/sim-mirror
6
+ Project-URL: Documentation, https://github.com/AndrewKochulab/sim-mirror/tree/main/docs
7
+ Project-URL: Issues, https://github.com/AndrewKochulab/sim-mirror/issues
8
+ Project-URL: Changelog, https://github.com/AndrewKochulab/sim-mirror/blob/main/CHANGELOG.md
9
+ Author: Andrew Kochulab
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ License-File: THIRD_PARTY_LICENSES.md
14
+ Keywords: ai-agents,claude-code,ios-simulator,mcp,screen-mirroring,simulator,ui-automation
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Environment :: MacOS X
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Software Development :: Testing
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: fastapi<1,>=0.115
30
+ Requires-Dist: grpclib<0.5,>=0.4.9
31
+ Requires-Dist: protobuf<8,>=7.36
32
+ Requires-Dist: tomli>=2; python_version < '3.11'
33
+ Requires-Dist: tomlkit>=0.13
34
+ Requires-Dist: uvicorn[standard]>=0.35
35
+ Description-Content-Type: text/markdown
36
+
37
+ <!-- mcp-name: io.github.AndrewKochulab/sim-mirror -->
38
+
39
+ # SimMirror
40
+
41
+ **Mirror and drive the iOS Simulator from Claude Code, CLI agents and the browser.**
42
+
43
+ [![CI](https://github.com/AndrewKochulab/sim-mirror/actions/workflows/ci.yml/badge.svg)](https://github.com/AndrewKochulab/sim-mirror/actions/workflows/ci.yml)
44
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
45
+ [![Release](https://img.shields.io/github/v/release/AndrewKochulab/sim-mirror?include_prereleases&sort=semver)](https://github.com/AndrewKochulab/sim-mirror/releases)
46
+
47
+ ![A Claude Code session tapping through Settings on an iPhone 17 Pro simulator in the viewer, its cursor named
48
+ and drawn before each tap](docs/media/hero.gif)
49
+
50
+ A live iOS Simulator in any browser tab or web page, an animated cursor that shows exactly what your AI agent is about
51
+ to tap, and token-efficient UI snapshots so agents read the screen as compact text instead of screenshots. Works with
52
+ Claude Code, Codex, Cursor and any other MCP client.
53
+
54
+ > **Status: first public preview (v0.1).** The tools, the viewer and the protocol work today; the embedding API and
55
+ > protocol become stable with v1.0.
56
+
57
+ ## Why SimMirror
58
+
59
+ - **See the simulator anywhere.** A live H.264 or JPEG stream in a Chrome tab, an iframe or a `<sim-mirror>` web
60
+ component, with touch, scrolling, typing, hardware buttons and light and dark mode.
61
+ - **Watch what the agent does.** Every agent gesture is announced before it lands, and the viewer's cursor glides to
62
+ the spot first -- so you can follow along, and step in: an agent waits for your hand to be still.
63
+ - **Spend fewer tokens.** Agents read accessibility snapshots with stable element refs and diffs between screens, act
64
+ in batches with waits, and reach for a screenshot only when a question is visual.
65
+
66
+ ## Install
67
+
68
+ **Claude Code**, with the plugin (tools, a skill that teaches snapshot-first use, and `/sim-mirror:open`):
69
+
70
+ ```
71
+ /plugin marketplace add AndrewKochulab/sim-mirror
72
+ /plugin install sim-mirror@sim-mirror
73
+ ```
74
+
75
+ **Any MCP client**, as a stdio server:
76
+
77
+ ```sh
78
+ claude mcp add sim-mirror -- uvx --from git+https://github.com/AndrewKochulab/sim-mirror@v0.2.0 sim-mirror mcp
79
+ ```
80
+
81
+ Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.md) and
82
+ [other clients](docs/clients/other-mcp.md).
83
+
84
+ **The command**, for `sim-mirror open`, `doctor` and the rest:
85
+
86
+ ```sh
87
+ uv tool install git+https://github.com/AndrewKochulab/sim-mirror@v0.2.0
88
+ ```
89
+
90
+ **The viewer library**, for your own pages: the npm package `@andrewkochulab/sim-mirror`, attached to each
91
+ [release](https://github.com/AndrewKochulab/sim-mirror/releases). Homebrew and PyPI come with v1.0.
92
+
93
+ **Requirements:** an Apple Silicon Mac, Xcode 26 or later, [uv](https://docs.astral.sh/uv/), and for touching the
94
+ screen, idb_companion (`brew install facebook/fb/idb-companion`). Details in [Installation](docs/installation.md).
95
+
96
+ ## Quickstart
97
+
98
+ ```sh
99
+ sim-mirror doctor # check this Mac, ending with a real tap on a simulator
100
+ sim-mirror open # this project's simulator, live in a browser tab
101
+ ```
102
+
103
+ Then ask your agent:
104
+
105
+ > Open Settings → General → About and tell me which iOS version this simulator runs. Use sim_snapshot rather than
106
+ > screenshots.
107
+
108
+ [Getting started](docs/getting-started.md) explains what happens along the way.
109
+
110
+ ## Features
111
+
112
+ **Live viewer.** H.264 through WebCodecs where the page can decode it, JPEG otherwise, negotiated per connection.
113
+ Touch, drag, scroll, typing and paste, Home and Lock, light and dark, and a device picker. It reconnects by itself
114
+ after a blip and says plainly when the simulator is off.
115
+
116
+ **Agent cursor.** Every agent gesture -- tap, long press, swipe, drag, typing -- reaches the viewer before it lands, and
117
+ the viewer's own pointer glides there first (`agent.cursor_lead_ms`). Never the Mac's pointer.
118
+
119
+ **Screen understanding.** A snapshot is a few lines -- `e2 button "General" (201,319)` -- with refs that stay with their
120
+ elements across scrolls, a digest, and diffs. `sim_act` plays up to 20 steps in one call and waits for text, its
121
+ absence, or the screen to settle. See [Screen understanding](docs/screen-understanding.md).
122
+
123
+ **Connectors.** idb_companion gives full control; without it the simctl connector still mirrors the screen, view-only,
124
+ and says why. More connectors plug in through an entry point. See [Connectors](docs/connectors.md).
125
+
126
+ **Embedding.** An [iframe](docs/embedding/iframe.md) with a one-shot ticket, the
127
+ [`<sim-mirror>` element](docs/embedding/web-component.md) themed with CSS custom properties, or SimMirror
128
+ [inside a Python application](docs/embedding/python-fastapi.md) with its own sign-in.
129
+
130
+ **Settings panel.** `sim-mirror open --settings` puts every setting a click away in the viewer, a tab per section,
131
+ saved for one project or all of them and applied before it says so. Settings that decide what runs or who may reach
132
+ the daemon wait for a code from `sim-mirror settings confirm`. See [The settings panel](docs/settings.md).
133
+
134
+ **Doctor.** Checks Xcode, its Simulator frameworks, runtimes, idb_companion, Device Hub and the desktop session, then
135
+ proves a tap reaches a simulator. See [the doctor](docs/doctor.md).
136
+
137
+ **Security.** Loopback only, a Host allowlist, exact Origin checks, CORS and framing only for origins you list, hashed
138
+ scoped tokens, and one-shot codes in URL fragments. See [Security](docs/security.md).
139
+
140
+ **Build and test.** `sim_build_run` and `sim_test` build with xcodebuild for the agent's simulator, install and
141
+ launch, and answer with only what failed and where -- each failing test named so it can be run again alone, and on
142
+ another simulator when asked. Off by default, since they run commands: `sim-mirror config set build.tools true`.
143
+
144
+ ## Measure the token cost
145
+
146
+ `benchmarks/tool_budget.py` calls each read-only tool through the same MCP relay a client uses and prints each answer's
147
+ bytes, estimated tokens and latency on your own screens; see [the token budget example](examples/token-budget/). On
148
+ an iPhone 17 Pro simulator (iOS 26.5, idb connector) showing Settings → General, 5 calls each (2026-09-16):
149
+
150
+ | Call | Tool | Bytes | Tokens (est.) | p50 ms | p95 ms |
151
+ |---|---|---:|---:|---:|---:|
152
+ | device info | `sim_device` | 265 | 46 | 2.9 | 3.6 |
153
+ | snapshot, full | `sim_snapshot` | 561 | 112 | 64.8 | 76.4 |
154
+ | snapshot, diff of an unchanged screen | `sim_snapshot` | 83 | 5 | 63.2 | 64.8 |
155
+ | screenshot, 400 px wide | `sim_screenshot` | 62222 | 469 | 8.2 | 8.9 |
156
+ | screenshot, 1200 px wide | `sim_screenshot` | 276472 | 4181 | 20.4 | 21.9 |
157
+
158
+ Bytes are the whole answer, an image's base64 included; tokens are estimates (text at about 4 characters a token,
159
+ an image at about 750 pixels a token).
160
+
161
+ ## Compatibility
162
+
163
+ | | Expected to work |
164
+ |---|---|
165
+ | macOS | 26 (15 with Xcode 16.4, view-only) |
166
+ | Xcode | 26 and 27 |
167
+ | Connectors | idb_companion 1.5 (full control), simctl (view-only) |
168
+ | Clients | Claude Code, Codex, Cursor, any stdio MCP client |
169
+ | Browsers | Chrome, Safari, Firefox, Edge |
170
+
171
+ Rows checked on real hardware are marked verified, with the date, in [Compatibility](docs/compatibility.md).
172
+
173
+ While SimMirror is `0.x`, the tools, the protocol and the embedding API can still change in a minor release, and the
174
+ changelog says when they do; from 1.0 they are held to [what SimMirror promises not to break](docs/stability.md). Pin
175
+ an exact version either way.
176
+
177
+ ## Documentation
178
+
179
+ [Everything](docs/README.md): getting started, clients, embedding, screen understanding, connectors, security,
180
+ architecture, and the reference for [tools](docs/reference/tools.md), [configuration](docs/reference/configuration.md),
181
+ the [command line](docs/reference/cli.md) and the [protocol](docs/reference/protocol.md).
182
+
183
+ ## Roadmap
184
+
185
+ | Milestone | What it brings |
186
+ |---|---|
187
+ | **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 |
188
+ | **v1.0** | Stable protocol and embedding API, PyPI and npm packages, the MCP registry |
189
+ | **v1.1** | Build, run and test tools become stable |
190
+ | **v1.2** | Richer screen understanding, a native Swift helper, Xcode 27 `mcpbridge` connector, one shared daemon for many hosts |
191
+ | **v2.0** | Real iPhones: view, install and launch without signing; full control through WebDriverAgent |
192
+ | **Android** | Android emulator support with the same viewer, cursor and tools |
193
+ | **Homebrew** | `brew install andrewkochulab/tap/sim-mirror`, right after v1.0 |
194
+ | **Website & launch** | Landing page, video tutorials and guides |
195
+
196
+ See [ROADMAP.md](ROADMAP.md) and the [milestones](https://github.com/AndrewKochulab/sim-mirror/milestones). How
197
+ SimMirror relates to other tools is in [Comparison](docs/comparison.md).
198
+
199
+ ## Contributing
200
+
201
+ Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md); AI
202
+ coding agents read [AGENTS.md](AGENTS.md). Report security issues privately as described in [SECURITY.md](SECURITY.md).
203
+ Questions go to [Discussions](https://github.com/AndrewKochulab/sim-mirror/discussions).
204
+
205
+ ## License
206
+
207
+ [Apache-2.0](LICENSE). See [NOTICE](NOTICE) and [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).
208
+
209
+ SimMirror is an independent project, not affiliated with Apple or Anthropic. iOS, iPhone, Xcode and Simulator are
210
+ trademarks of Apple Inc.; Claude and Claude Code are trademarks of Anthropic PBC.