mobster-cli 0.4.3__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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1099 @@
1
+ // mirroring: drives an iPhone through macOS iPhone Mirroring, for mobile_agent/mirroring.py (an experimental
2
+ // connection: no cable, no Xcode, nothing installed on the phone).
3
+ //
4
+ // It reads one JSON request per line on standard input and prints one JSON line per request:
5
+ // {"op":"state"} where the Mirroring window is, and whether it shows the phone:
6
+ // {"ok":true,"state":"ready"|"blocked"|"no-window"|"not-running",
7
+ // "window":{"x","y","w","h","id"},"said":[...]} ("said": the Mac's own
8
+ // words on an interstitial such as "iPhone in Use"; never the phone's)
9
+ // {"op":"capture","path":P,"ocr":true} the window as a JPEG at P (ScreenCaptureKit), its pixel hash, how much
10
+ // of it is black, and with "ocr" the text Vision reads on it:
11
+ // {"rows":[{"text","confidence","rect":[x,y,w,h]}]}, rects as fractions
12
+ // of the window from its top left
13
+ // {"op":"tap","x":fx,"y":fy} a tap at a point given as fractions of the window
14
+ // {"op":"press","x","y","ms"} a long press
15
+ // {"op":"drag","x1","y1","x2","y2","ms"} a touch drag (iOS sees a swipe)
16
+ // {"op":"scroll","x","y","dy","dx"} a scroll gesture at a point, in window points (dy > 0 reveals what is below)
17
+ // {"op":"key","combo":"cmd+1"} a key or shortcut (cmd+1 Home, cmd+2 App Switcher, cmd+3 Spotlight)
18
+ // {"op":"type","text":T,"mode":"keys"|"paste"} text into the focused field: US key codes, or the Mac pasteboard
19
+ // and cmd+v
20
+ // `mirroring stream FPS MAX_WIDTH QUALITY` (a separate process): the window as JPEG frames, for the live view
21
+ // `mirroring drop FX FY PATH...` / `mirroring drag-out FX FY DEST` (separate processes): files to and from the phone
22
+ // `mirroring audio --seconds N --out FILE [--live]` (a separate process): the phone's sound, for hearing
23
+ // {"op":"restore"} gives the front back to the app that had it before the first gesture
24
+ //
25
+ // A gesture request may carry "input":"foreground" (the person set MOBSTER_MIRRORING_INPUT=foreground); without it
26
+ // the gesture goes in the background and the answer says which way it went: {"ok":true,"input":"background"}.
27
+ //
28
+ // Errors are {"ok":false,"error":"...","code":"..."}: "not_running", "no_window", "blocked", "covered" (foreground
29
+ // input only: another window would take the gesture, so nothing was sent), "background" (the gesture can't be
30
+ // addressed to the window, or Mirroring isn't answering, so nothing was sent), "permission" (Screen Recording or
31
+ // Accessibility is missing), "capture", "unsupported" (macOS before 15), "usage". A gesture is sent at most once and
32
+ // never retried here, and a failed background gesture never turns into a foreground one.
33
+ //
34
+ // The phone image is a video stream: macOS accessibility sees nothing inside it, so input goes in as mouse and
35
+ // keyboard events at the window's position. In the background they are posted to the Mirroring process and addressed
36
+ // to its window: no cursor move, no focus change, and the window can be covered. Foreground input brings the window
37
+ // to the front and posts to the HID event tap. Capture works with the window covered either way.
38
+ //
39
+ // The Mac app carries it as Contents/Resources/mirroring; a source checkout builds it with
40
+ // native_helpers.build("mirroring").
41
+
42
+ import AVFoundation
43
+ import AppKit
44
+ import CoreImage
45
+ import CoreMedia
46
+ import ApplicationServices
47
+ import CoreGraphics
48
+ import Foundation
49
+ import ImageIO
50
+ import ScreenCaptureKit
51
+ import UniformTypeIdentifiers
52
+ import Vision
53
+
54
+ let bundleID = "com.apple.ScreenContinuity"
55
+ let appPath = "/System/Applications/iPhone Mirroring.app"
56
+
57
+ struct HelperError: Error {
58
+ let code: String
59
+ let message: String
60
+ }
61
+
62
+ func emit(_ value: [String: Any]) {
63
+ if let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]),
64
+ let line = String(data: data, encoding: .utf8) {
65
+ FileHandle.standardOutput.write(Data((line + "\n").utf8))
66
+ }
67
+ }
68
+
69
+ // MARK: - the app and its window
70
+
71
+ struct Window {
72
+ let id: CGWindowID
73
+ let frame: CGRect // global screen points, top-left origin (the space CGEvent uses)
74
+ var json: [String: Any] {
75
+ ["x": frame.origin.x, "y": frame.origin.y, "w": frame.width, "h": frame.height, "id": Int(id)]
76
+ }
77
+ }
78
+
79
+ func runningApp() -> NSRunningApplication? {
80
+ NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).first
81
+ }
82
+
83
+ func windowList(onScreenOnly: Bool) -> [[String: Any]] {
84
+ let options: CGWindowListOption = onScreenOnly ? [.optionOnScreenOnly, .excludeDesktopElements] : [.optionAll]
85
+ return CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []
86
+ }
87
+
88
+ // The phone window: the app's normal-layer window whose title is the app's own name (both localized together), else
89
+ // its frontmost normal-layer window. Matched by process, never by the localized owner name.
90
+ func phoneWindow(_ app: NSRunningApplication) -> Window? {
91
+ let pid = app.processIdentifier
92
+ let mine = windowList(onScreenOnly: false).filter {
93
+ ($0[kCGWindowOwnerPID as String] as? pid_t) == pid && ($0[kCGWindowLayer as String] as? Int ?? 1) == 0
94
+ }
95
+ let named = mine.filter {
96
+ let name = $0[kCGWindowName as String] as? String
97
+ return name != nil && name == ($0[kCGWindowOwnerName as String] as? String)
98
+ }
99
+ for info in (named.isEmpty ? mine : named) {
100
+ guard let bounds = info[kCGWindowBounds as String] as? [String: Any],
101
+ let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary),
102
+ let number = info[kCGWindowNumber as String] as? Int,
103
+ rect.width > 40, rect.height > 80 else { continue }
104
+ return Window(id: CGWindowID(number), frame: rect)
105
+ }
106
+ return nil
107
+ }
108
+
109
+ func attribute(_ element: AXUIElement, _ name: String) -> AnyObject? {
110
+ var value: AnyObject?
111
+ return AXUIElementCopyAttributeValue(element, name as CFString, &value) == .success ? value : nil
112
+ }
113
+
114
+ func axFrame(_ element: AXUIElement) -> CGRect? {
115
+ guard let position = attribute(element, kAXPositionAttribute), let size = attribute(element, kAXSizeAttribute)
116
+ else { return nil }
117
+ var point = CGPoint.zero, extent = CGSize.zero
118
+ guard AXValueGetValue(position as! AXValue, .cgPoint, &point), AXValueGetValue(size as! AXValue, .cgSize, &extent)
119
+ else { return nil }
120
+ return CGRect(origin: point, size: extent)
121
+ }
122
+
123
+ func axWindow(_ app: NSRunningApplication, _ window: Window) -> AXUIElement? {
124
+ let element = AXUIElementCreateApplication(app.processIdentifier)
125
+ let windows = attribute(element, kAXWindowsAttribute) as? [AXUIElement] ?? []
126
+ return windows.first { candidate in
127
+ guard let frame = axFrame(candidate) else { return false }
128
+ return abs(frame.minX - window.frame.minX) < 4 && abs(frame.minY - window.frame.minY) < 4
129
+ }
130
+ }
131
+
132
+ // The Mac's own views inside the phone window. A live session has none (the phone is a video stream); "iPhone in
133
+ // Use", "Paused", "Connect" and the Mac login gate are ordinary views with words and buttons.
134
+ func interstitial(_ app: NSRunningApplication, _ window: Window) -> (blocked: Bool, said: [String], login: Bool) {
135
+ guard let element = axWindow(app, window) else { return (false, [], false) }
136
+ var said: [String] = []
137
+ var found = false
138
+ var login = false
139
+ func walk(_ node: AXUIElement, _ depth: Int) {
140
+ if depth > 8 || said.count >= 8 { return }
141
+ let role = attribute(node, kAXRoleAttribute) as? String ?? ""
142
+ if ["AXStaticText", "AXButton", "AXTextField", "AXSecureTextField"].contains(role) {
143
+ found = true
144
+ if role == "AXSecureTextField" || role == "AXTextField" { login = true }
145
+ if role == "AXStaticText", let value = attribute(node, kAXValueAttribute) as? String {
146
+ let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
147
+ if !text.isEmpty { said.append(String(text.prefix(160))) }
148
+ }
149
+ }
150
+ for child in attribute(node, kAXChildrenAttribute) as? [AXUIElement] ?? [] { walk(child, depth + 1) }
151
+ }
152
+ walk(element, 0)
153
+ return (found, said, login)
154
+ }
155
+
156
+ func state() -> [String: Any] {
157
+ guard let app = runningApp() else { return ["ok": true, "state": "not-running"] }
158
+ guard let window = phoneWindow(app) else { return ["ok": true, "state": "no-window"] }
159
+ let seen = interstitial(app, window)
160
+ var out: [String: Any] = ["ok": true, "state": seen.blocked ? "blocked" : "ready", "window": window.json,
161
+ "front": isFront(app)]
162
+ if seen.blocked {
163
+ out["said"] = seen.said
164
+ out["login"] = seen.login
165
+ }
166
+ return out
167
+ }
168
+
169
+ func readyWindow() throws -> (NSRunningApplication, Window) {
170
+ guard let app = runningApp() else {
171
+ throw HelperError(code: "not_running", message: "iPhone Mirroring isn't open")
172
+ }
173
+ guard let window = phoneWindow(app) else {
174
+ throw HelperError(code: "no_window", message: "iPhone Mirroring has no phone window")
175
+ }
176
+ return (app, window)
177
+ }
178
+
179
+ // MARK: - the front
180
+
181
+ var previousFront: pid_t? = nil
182
+
183
+ func frontmostOwner() -> pid_t? {
184
+ for info in windowList(onScreenOnly: true) where (info[kCGWindowLayer as String] as? Int ?? 1) == 0 {
185
+ return info[kCGWindowOwnerPID as String] as? pid_t
186
+ }
187
+ return nil
188
+ }
189
+
190
+ // In front means both: the app has keyboard focus (AXFrontmost) and its window is the frontmost one. Either alone
191
+ // has been seen without the other, and then keystrokes or clicks land in another app.
192
+ func isFront(_ app: NSRunningApplication) -> Bool {
193
+ let element = AXUIElementCreateApplication(app.processIdentifier)
194
+ let front = attribute(element, kAXFrontmostAttribute) as? Bool ?? false
195
+ return front && frontmostOwner() == app.processIdentifier
196
+ }
197
+
198
+ func bringToFront(_ app: NSRunningApplication, _ window: Window) throws {
199
+ if isFront(app) { return }
200
+ if previousFront == nil, let owner = frontmostOwner(), owner != app.processIdentifier { previousFront = owner }
201
+ let element = axWindow(app, window)
202
+ if let element, attribute(element, kAXMinimizedAttribute) as? Bool == true {
203
+ AXUIElementSetAttributeValue(element, kAXMinimizedAttribute as CFString, kCFBooleanFalse)
204
+ }
205
+ var deadline = Date().addingTimeInterval(1.5)
206
+ while Date() < deadline {
207
+ app.activate()
208
+ if let element { AXUIElementPerformAction(element, kAXRaiseAction as CFString) }
209
+ Thread.sleep(forTimeInterval: 0.08)
210
+ if isFront(app) { return }
211
+ }
212
+ // macOS 14's cooperative activation can refuse a background process; Launch Services' open doesn't.
213
+ let process = Process()
214
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/open")
215
+ process.arguments = ["-a", appPath]
216
+ try? process.run()
217
+ process.waitUntilExit()
218
+ deadline = Date().addingTimeInterval(1.5)
219
+ while Date() < deadline {
220
+ Thread.sleep(forTimeInterval: 0.1)
221
+ if isFront(app) { return }
222
+ }
223
+ throw HelperError(code: "covered", message: "iPhone Mirroring couldn't come to the front, so nothing was sent")
224
+ }
225
+
226
+ // The topmost normal window at a global point belongs to iPhone Mirroring: a click there reaches the phone.
227
+ func ownsPoint(_ app: NSRunningApplication, _ point: CGPoint) -> Bool {
228
+ for info in windowList(onScreenOnly: true) where (info[kCGWindowLayer as String] as? Int ?? 1) == 0 {
229
+ guard let bounds = info[kCGWindowBounds as String] as? [String: Any],
230
+ let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary), rect.contains(point) else { continue }
231
+ return (info[kCGWindowOwnerPID as String] as? pid_t) == app.processIdentifier
232
+ }
233
+ return false
234
+ }
235
+
236
+ func restoreFront() -> [String: Any] {
237
+ if let pid = previousFront, let app = NSRunningApplication(processIdentifier: pid) {
238
+ app.activate(options: [])
239
+ }
240
+ previousFront = nil
241
+ return ["ok": true]
242
+ }
243
+
244
+ // MARK: - capture
245
+
246
+ final class Box<T>: @unchecked Sendable { var value: T; init(_ value: T) { self.value = value } }
247
+
248
+ var cachedWindow: SCWindow? = nil
249
+
250
+ func scWindow(_ id: CGWindowID) throws -> SCWindow {
251
+ if let cached = cachedWindow, cached.windowID == id { return cached }
252
+ let done = DispatchSemaphore(value: 0)
253
+ let result = Box<(SCShareableContent?, Error?)>((nil, nil))
254
+ SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: false) { content, error in
255
+ result.value = (content, error)
256
+ done.signal()
257
+ }
258
+ if done.wait(timeout: .now() + 5) == .timedOut {
259
+ throw HelperError(code: "capture", message: "Screen capture didn't answer")
260
+ }
261
+ guard let content = result.value.0 else {
262
+ throw HelperError(code: "permission", message: "Screen Recording permission is needed to read the phone")
263
+ }
264
+ guard let window = content.windows.first(where: { $0.windowID == id }) else {
265
+ throw HelperError(code: "no_window", message: "The Mirroring window isn't capturable")
266
+ }
267
+ cachedWindow = window
268
+ return window
269
+ }
270
+
271
+ @available(macOS 14.0, *)
272
+ func captureImage(_ window: Window) throws -> CGImage {
273
+ let target = try scWindow(window.id)
274
+ let filter = SCContentFilter(desktopIndependentWindow: target)
275
+ let config = SCStreamConfiguration()
276
+ let scale = CGFloat(filter.pointPixelScale)
277
+ config.width = Int(window.frame.width * scale)
278
+ config.height = Int(window.frame.height * scale)
279
+ config.showsCursor = false
280
+ config.ignoreShadowsSingleWindow = true
281
+ let done = DispatchSemaphore(value: 0)
282
+ let result = Box<(CGImage?, Error?)>((nil, nil))
283
+ SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) { image, error in
284
+ result.value = (image, error)
285
+ done.signal()
286
+ }
287
+ if done.wait(timeout: .now() + 5) == .timedOut {
288
+ throw HelperError(code: "capture", message: "The window capture didn't finish")
289
+ }
290
+ guard let image = result.value.0 else {
291
+ cachedWindow = nil
292
+ throw HelperError(code: "capture", message: "The window capture failed")
293
+ }
294
+ return image
295
+ }
296
+
297
+ // A small grey picture of the frame: its hash says whether the screen changed; its share of near-black cells says
298
+ // whether protected video (DRM) is drawn black.
299
+ func signature(_ image: CGImage) -> (hash: String, dark: Double) {
300
+ let width = 24, height = 52
301
+ var pixels = [UInt8](repeating: 0, count: width * height)
302
+ let context = CGContext(data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width,
303
+ space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue)
304
+ context?.interpolationQuality = .low
305
+ context?.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
306
+ var hash: UInt64 = 1469598103934665603
307
+ var dark = 0
308
+ for value in pixels {
309
+ hash = (hash ^ UInt64(value >> 3)) &* 1099511628211
310
+ if value < 10 { dark += 1 }
311
+ }
312
+ return (String(format: "%016llx", hash), Double(dark) / Double(pixels.count))
313
+ }
314
+
315
+ func writeJPEG(_ image: CGImage, _ path: String) throws {
316
+ let url = URL(fileURLWithPath: path)
317
+ guard let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.jpeg.identifier as CFString, 1, nil)
318
+ else { throw HelperError(code: "capture", message: "Couldn't write the frame") }
319
+ CGImageDestinationAddImage(destination, image, [kCGImageDestinationLossyCompressionQuality: 0.8] as CFDictionary)
320
+ if !CGImageDestinationFinalize(destination) {
321
+ throw HelperError(code: "capture", message: "Couldn't write the frame")
322
+ }
323
+ }
324
+
325
+ func recognize(_ image: CGImage) throws -> [[String: Any]] {
326
+ let request = VNRecognizeTextRequest()
327
+ request.recognitionLevel = .accurate
328
+ request.usesLanguageCorrection = false
329
+ try VNImageRequestHandler(cgImage: image, options: [:]).perform([request])
330
+ return (request.results ?? []).compactMap { observation in
331
+ guard let candidate = observation.topCandidates(1).first else { return nil }
332
+ let box = observation.boundingBox
333
+ return ["text": candidate.string, "confidence": Double(candidate.confidence),
334
+ "rect": [box.minX, 1 - box.maxY, box.width, box.height]]
335
+ }
336
+ }
337
+
338
+ func capture(_ request: [String: Any]) throws -> [String: Any] {
339
+ let (app, window) = try readyWindow()
340
+ let started = Date()
341
+ guard #available(macOS 14.0, *) else { throw HelperError(code: "unsupported", message: "This needs macOS 15") }
342
+ let image = try captureImage(window)
343
+ let captured = Date()
344
+ let sig = signature(image)
345
+ var out: [String: Any] = ["ok": true, "window": window.json, "hash": sig.hash, "dark": sig.dark,
346
+ "pixels": [image.width, image.height]]
347
+ if let path = request["path"] as? String { try writeJPEG(image, path) }
348
+ if request["ocr"] as? Bool == true {
349
+ out["rows"] = try recognize(image)
350
+ }
351
+ let seen = interstitial(app, window)
352
+ if seen.blocked {
353
+ out["blocked"] = true
354
+ out["said"] = seen.said
355
+ out["login"] = seen.login
356
+ }
357
+ out["ms"] = ["capture": Int(captured.timeIntervalSince(started) * 1000),
358
+ "total": Int(Date().timeIntervalSince(started) * 1000)]
359
+ return out
360
+ }
361
+
362
+ // MARK: - input
363
+
364
+ // Two ways in. The default posts each event to the iPhone Mirroring process, addressed to its window: the Mac's
365
+ // cursor, keyboard focus and front app stay where the person left them, and the window can be behind other windows.
366
+ // The other (`"input":"foreground"`, from MOBSTER_MIRRORING_INPUT=foreground) is the earlier path: it brings the
367
+ // window to the front, moves the real cursor and posts to the HID event tap. It is used only when asked for.
368
+ //
369
+ // Background delivery rests on two things Apple doesn't document, both read off macOS 26:
370
+ // CGEvent field 51 holds the number of the window an event is for, and CGEventSetWindowLocation (exported by
371
+ // CoreGraphics, absent from its headers) sets where in that window it lands. Without both, AppKit hands
372
+ // the app an event with no window and drops it. When either is missing, or the app doesn't answer, nothing is sent
373
+ // and the error says so: a later macOS can break this, and it must not fall back to the front on its own.
374
+
375
+ var foreground = false // set from each request
376
+
377
+ struct Target {
378
+ let pid: pid_t
379
+ let window: Window
380
+ }
381
+
382
+ typealias SetWindowLocation = @convention(c) (CGEvent, CGPoint) -> Void
383
+ let setWindowLocation: SetWindowLocation? = {
384
+ guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "CGEventSetWindowLocation") else { return nil }
385
+ return unsafeBitCast(symbol, to: SetWindowLocation.self)
386
+ }()
387
+ let windowField = CGEventField(rawValue: 51)!
388
+ // A private source starts with no modifier flags and never merges the person's own held keys into what we post.
389
+ let source = CGEventSource(stateID: .privateState)
390
+
391
+ func background(_ message: String) -> HelperError { HelperError(code: "background", message: message) }
392
+
393
+ let foregroundHint = "Set MOBSTER_MIRRORING_INPUT=foreground to let Mobster bring the window forward and use your cursor and keyboard for each gesture."
394
+
395
+ // Ready for background input: the window we address is the live one, and its app answers. A frozen Mirroring would
396
+ // take a gesture late, and Mobster couldn't say when it landed, so it is refused.
397
+ func confirmBackground(_ app: NSRunningApplication, _ window: Window) throws {
398
+ guard setWindowLocation != nil else {
399
+ throw background("This macOS doesn't let Mobster address iPhone Mirroring's window, so nothing was sent. " + foregroundHint)
400
+ }
401
+ guard !app.isTerminated, axWindow(app, window) != nil else {
402
+ throw background("iPhone Mirroring isn't answering, so nothing was sent. Wait for it, or quit and reopen it, then try again. " + foregroundHint)
403
+ }
404
+ }
405
+
406
+ func number(_ request: [String: Any], _ key: String) throws -> Double {
407
+ guard let value = request[key] as? Double ?? (request[key] as? Int).map(Double.init), value.isFinite else {
408
+ throw HelperError(code: "usage", message: "\(key) is missing")
409
+ }
410
+ return value
411
+ }
412
+
413
+ // A point given as fractions of the window, in window points from its top left; refused outside the window.
414
+ func localPoint(_ window: Window, _ fx: Double, _ fy: Double) throws -> CGPoint {
415
+ guard (0...1).contains(fx), (0...1).contains(fy) else {
416
+ throw HelperError(code: "usage", message: "The point is outside the phone's screen")
417
+ }
418
+ return CGPoint(x: window.frame.width * fx, y: window.frame.height * fy)
419
+ }
420
+
421
+ func globalPoint(_ target: Target, _ local: CGPoint) -> CGPoint {
422
+ CGPoint(x: target.window.frame.minX + local.x, y: target.window.frame.minY + local.y)
423
+ }
424
+
425
+ // Ready for a gesture: the session shows the phone and Mirroring can take it. In the foreground path the window is
426
+ // also in front and owns every point the gesture touches. Otherwise nothing is sent.
427
+ func readyForInput(_ fractions: [(Double, Double)] = []) throws -> (target: Target, points: [CGPoint]) {
428
+ let (app, found) = try readyWindow()
429
+ let seen = interstitial(app, found)
430
+ if seen.blocked {
431
+ throw HelperError(code: "blocked", message: seen.said.first ?? "iPhone Mirroring isn't showing the phone")
432
+ }
433
+ guard AXIsProcessTrusted() else {
434
+ throw HelperError(code: "permission", message: "Accessibility permission is needed to tap the phone")
435
+ }
436
+ let target = Target(pid: app.processIdentifier, window: found)
437
+ let locals = try fractions.map { try localPoint(found, $0.0, $0.1) }
438
+ if foreground {
439
+ try bringToFront(app, found)
440
+ for local in locals where !ownsPoint(app, globalPoint(target, local)) {
441
+ throw HelperError(code: "covered", message: "Another window covers the phone, so nothing was sent")
442
+ }
443
+ } else {
444
+ try confirmBackground(app, found)
445
+ }
446
+ return (target, locals)
447
+ }
448
+
449
+ func withCursorKept(_ body: () throws -> Void) rethrows {
450
+ guard foreground else { return try body() }
451
+ let saved = CGEvent(source: nil)?.location
452
+ defer { if let saved { CGWarpMouseCursorPosition(saved) } }
453
+ try body()
454
+ }
455
+
456
+ // One mouse event at a point in the window.
457
+ func post(_ type: CGEventType, _ local: CGPoint, _ target: Target) throws {
458
+ let point = globalPoint(target, local)
459
+ if foreground { return postHID(type, point) }
460
+ guard let setLocation = setWindowLocation,
461
+ let event = CGEvent(mouseEventSource: source, mouseType: type, mouseCursorPosition: point, mouseButton: .left)
462
+ else { throw background("The Mac wouldn't make the mouse event, so nothing was sent. " + foregroundHint) }
463
+ event.setIntegerValueField(windowField, value: Int64(target.window.id))
464
+ setLocation(event, local)
465
+ if type != .mouseMoved { event.setIntegerValueField(.mouseEventClickState, value: 1) }
466
+ event.postToPid(target.pid)
467
+ }
468
+
469
+ // One mouse event at a global point, through the Mac's pointer. A file drag uses it: the drag runs between two apps,
470
+ // so it can't be sent to one window.
471
+ func postHID(_ type: CGEventType, _ point: CGPoint) {
472
+ CGEvent(mouseEventSource: nil, mouseType: type, mouseCursorPosition: point, mouseButton: .left)?
473
+ .post(tap: .cghidEventTap)
474
+ }
475
+
476
+ func tap(_ request: [String: Any], holdMs: Double = 50) throws -> [String: Any] {
477
+ let (target, points) = try readyForInput([(try number(request, "x"), try number(request, "y"))])
478
+ let point = points[0]
479
+ try withCursorKept {
480
+ try post(.mouseMoved, point, target)
481
+ usleep(foreground ? 40_000 : 10_000)
482
+ try post(.leftMouseDown, point, target)
483
+ usleep(useconds_t(max(holdMs, 20) * 1000))
484
+ try post(.leftMouseUp, point, target)
485
+ }
486
+ return ["ok": true, "input": foreground ? "foreground" : "background"]
487
+ }
488
+
489
+ func drag(_ request: [String: Any]) throws -> [String: Any] {
490
+ let (target, points) = try readyForInput([(try number(request, "x1"), try number(request, "y1")),
491
+ (try number(request, "x2"), try number(request, "y2"))])
492
+ let start = points[0], end = points[1]
493
+ let ms = max(120, (request["ms"] as? Double) ?? 350)
494
+ let steps = 16
495
+ try withCursorKept {
496
+ try post(.mouseMoved, start, target)
497
+ usleep(foreground ? 40_000 : 10_000)
498
+ try post(.leftMouseDown, start, target)
499
+ usleep(30_000)
500
+ for step in 1...steps {
501
+ let t = Double(step) / Double(steps)
502
+ try post(.leftMouseDragged, CGPoint(x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t), target)
503
+ usleep(useconds_t(ms / Double(steps) * 1000))
504
+ }
505
+ // Held still a moment before lifting, so the lift carries no speed and the content doesn't fling on.
506
+ usleep(60_000)
507
+ try post(.leftMouseUp, end, target)
508
+ }
509
+ return ["ok": true, "input": foreground ? "foreground" : "background"]
510
+ }
511
+
512
+ func scroll(_ request: [String: Any]) throws -> [String: Any] {
513
+ let (target, points) = try readyForInput([(try number(request, "x"), try number(request, "y"))])
514
+ let local = points[0]
515
+ let dy = (request["dy"] as? Double) ?? 0, dx = (request["dx"] as? Double) ?? 0
516
+ let steps = 8
517
+ try withCursorKept {
518
+ try post(.mouseMoved, local, target)
519
+ usleep(foreground ? 40_000 : 10_000)
520
+ for _ in 0..<steps {
521
+ let event = CGEvent(scrollWheelEvent2Source: foreground ? nil : source, units: .pixel, wheelCount: 2,
522
+ wheel1: Int32(dy / Double(steps)), wheel2: Int32(dx / Double(steps)), wheel3: 0)
523
+ event?.location = globalPoint(target, local)
524
+ if foreground {
525
+ event?.post(tap: .cghidEventTap)
526
+ } else {
527
+ guard let event, let setLocation = setWindowLocation else {
528
+ throw background("The Mac wouldn't make the scroll event, so nothing was sent. " + foregroundHint)
529
+ }
530
+ event.setIntegerValueField(windowField, value: Int64(target.window.id))
531
+ setLocation(event, local)
532
+ event.postToPid(target.pid)
533
+ }
534
+ usleep(25_000)
535
+ }
536
+ }
537
+ return ["ok": true, "input": foreground ? "foreground" : "background"]
538
+ }
539
+
540
+ // US key codes. iPhone Mirroring forwards raw key codes to the phone and drops both the event's flags and any
541
+ // Unicode payload, so a modifier has to be a key held down, and text has to be key codes (or a paste).
542
+ let keyCodes: [String: CGKeyCode] = [
543
+ "a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7, "c": 8, "v": 9, "b": 11, "q": 12, "w": 13,
544
+ "e": 14, "r": 15, "y": 16, "t": 17, "1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23, "=": 24, "9": 25,
545
+ "7": 26, "-": 27, "8": 28, "0": 29, "]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "return": 36, "l": 37,
546
+ "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42, ",": 43, "/": 44, "n": 45, "m": 46, ".": 47, "tab": 48,
547
+ "space": 49, " ": 49, "`": 50, "delete": 51, "escape": 53, "left": 123, "right": 124, "down": 125, "up": 126,
548
+ ]
549
+ let shifted: [Character: String] = [
550
+ "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", "_": "-",
551
+ "+": "=", ":": ";", "\"": "'", "<": ",", ">": ".", "?": "/", "~": "`", "{": "[", "}": "]", "|": "\\",
552
+ ]
553
+ let modifierKeys: [String: (CGKeyCode, CGEventFlags)] = [
554
+ "cmd": (55, .maskCommand), "shift": (56, .maskShift), "alt": (58, .maskAlternate), "ctrl": (59, .maskControl),
555
+ ]
556
+
557
+ func key(_ code: CGKeyCode, _ down: Bool, _ flags: CGEventFlags, _ target: Target) throws {
558
+ if foreground {
559
+ let event = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down)
560
+ if !flags.isEmpty { event?.flags = flags }
561
+ event?.post(tap: .cghidEventTap)
562
+ return
563
+ }
564
+ guard let event = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: down) else {
565
+ throw background("The Mac wouldn't make the key event, so nothing was sent. " + foregroundHint)
566
+ }
567
+ event.flags = flags
568
+ event.setIntegerValueField(windowField, value: Int64(target.window.id))
569
+ event.postToPid(target.pid)
570
+ }
571
+
572
+ func press(_ code: CGKeyCode, holding modifiers: [String], _ target: Target) throws {
573
+ var flags: CGEventFlags = []
574
+ var held: [(CGKeyCode, CGEventFlags)] = []
575
+ // Always released, last first, each without its own flag: a modifier left down corrupts every later key.
576
+ func release() {
577
+ for (modCode, mask) in held.reversed() {
578
+ flags.remove(mask)
579
+ try? key(modCode, false, flags, target)
580
+ usleep(10_000)
581
+ }
582
+ }
583
+ do {
584
+ for name in modifiers {
585
+ guard let (modCode, mask) = modifierKeys[name] else { continue }
586
+ flags.insert(mask)
587
+ held.append((modCode, mask))
588
+ try key(modCode, true, flags, target)
589
+ usleep(10_000)
590
+ }
591
+ try key(code, true, flags, target)
592
+ usleep(25_000)
593
+ try key(code, false, flags, target)
594
+ usleep(10_000)
595
+ } catch {
596
+ release()
597
+ throw error
598
+ }
599
+ release()
600
+ }
601
+
602
+ func combo(_ request: [String: Any]) throws -> [String: Any] {
603
+ guard let text = request["combo"] as? String else { throw HelperError(code: "usage", message: "combo is missing") }
604
+ let parts = text.lowercased().split(separator: "+").map(String.init)
605
+ guard let last = parts.last, let code = keyCodes[last], parts.dropLast().allSatisfy({ modifierKeys[$0] != nil })
606
+ else { throw HelperError(code: "usage", message: "Unknown key \(text)") }
607
+ let (target, _) = try readyForInput()
608
+ try press(code, holding: Array(parts.dropLast()), target)
609
+ return ["ok": true, "input": foreground ? "foreground" : "background"]
610
+ }
611
+
612
+ func typeText(_ request: [String: Any]) throws -> [String: Any] {
613
+ guard let text = request["text"] as? String, !text.isEmpty else {
614
+ throw HelperError(code: "usage", message: "text is missing")
615
+ }
616
+ let paste = request["mode"] as? String == "paste"
617
+ var strokes: [(CGKeyCode, Bool)] = []
618
+ if !paste {
619
+ for character in text {
620
+ if character == "\n" { strokes.append((keyCodes["return"]!, false)); continue }
621
+ let lower = String(character).lowercased()
622
+ if let base = shifted[character], let code = keyCodes[base] {
623
+ strokes.append((code, true))
624
+ } else if let code = keyCodes[lower], lower.count == 1 {
625
+ strokes.append((code, character.isUppercase))
626
+ } else {
627
+ throw HelperError(code: "usage", message: "This text needs a paste: it has characters with no key")
628
+ }
629
+ }
630
+ }
631
+ let (target, _) = try readyForInput()
632
+ if paste {
633
+ // The text stays on the Mac's pasteboard: putting the earlier contents back races the phone's read of it,
634
+ // and the phone could paste those instead.
635
+ let board = NSPasteboard.general
636
+ board.clearContents()
637
+ board.setString(text, forType: .string)
638
+ guard board.string(forType: .string) == text else {
639
+ throw HelperError(code: "usage", message: "The Mac's pasteboard didn't take the text; nothing was typed")
640
+ }
641
+ usleep(200_000)
642
+ try press(keyCodes["v"]!, holding: ["cmd"], target)
643
+ } else {
644
+ for (code, shift) in strokes {
645
+ try press(code, holding: shift ? ["shift"] : [], target)
646
+ usleep(15_000)
647
+ }
648
+ }
649
+ return ["ok": true, "input": foreground ? "foreground" : "background"]
650
+ }
651
+
652
+ // MARK: - the loop
653
+
654
+ func handle(_ request: [String: Any]) throws -> [String: Any] {
655
+ // iPhone Mirroring itself came with macOS 15; the Mac app still runs on 13.
656
+ guard #available(macOS 15.0, *) else {
657
+ throw HelperError(code: "unsupported", message: "iPhone Mirroring needs macOS 15 or later")
658
+ }
659
+ foreground = request["input"] as? String == "foreground"
660
+ switch request["op"] as? String ?? "" {
661
+ case "state": return state()
662
+ case "capture": return try capture(request)
663
+ case "tap": return try tap(request)
664
+ case "press": return try tap(request, holdMs: (request["ms"] as? Double) ?? 1500)
665
+ case "drag": return try drag(request)
666
+ case "scroll": return try scroll(request)
667
+ case "key": return try combo(request)
668
+ case "type": return try typeText(request)
669
+ case "restore": return restoreFront()
670
+ default: throw HelperError(code: "usage", message: "Unknown op")
671
+ }
672
+ }
673
+
674
+ // MARK: - the live stream
675
+
676
+ // `mirroring stream FPS MAX_WIDTH QUALITY`: the Mirroring window as a stream of JPEG frames on standard output, each
677
+ // a 4-byte big-endian length and the JPEG, for the live view (mirroring.MirroringVideo). ScreenCaptureKit captures the
678
+ // window itself, so it streams while the window is behind other windows. A frame that shows no change is not sent.
679
+ // It ends when standard input closes (its parent went away) or the window does.
680
+ @available(macOS 14.0, *)
681
+ final class FrameStream: NSObject, SCStreamOutput, SCStreamDelegate {
682
+ let quality: Double
683
+ let out = FileHandle.standardOutput
684
+ let context = CIContext(options: [.useSoftwareRenderer: false])
685
+ var stream: SCStream?
686
+ init(quality: Double) { self.quality = quality }
687
+
688
+ func start(window: Window, fps: Double, maxWidth: Double) throws {
689
+ let target = try scWindow(window.id)
690
+ let filter = SCContentFilter(desktopIndependentWindow: target)
691
+ let config = SCStreamConfiguration()
692
+ let scale = min(Double(filter.pointPixelScale), maxWidth / max(window.frame.width, 1))
693
+ config.width = Int(window.frame.width * scale)
694
+ config.height = Int(window.frame.height * scale)
695
+ config.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(max(1, min(fps, 60))))
696
+ config.showsCursor = false
697
+ config.queueDepth = 3
698
+ config.ignoreShadowsSingleWindow = true
699
+ let stream = SCStream(filter: filter, configuration: config, delegate: self)
700
+ try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: DispatchQueue(label: "frames"))
701
+ self.stream = stream
702
+ stream.startCapture { error in
703
+ if error != nil { exit(3) }
704
+ }
705
+ }
706
+
707
+ func stream(_ stream: SCStream, didOutputSampleBuffer buffer: CMSampleBuffer, of type: SCStreamOutputType) {
708
+ guard type == .screen, buffer.isValid,
709
+ let attachments = CMSampleBufferGetSampleAttachmentsArray(buffer, createIfNecessary: false)
710
+ as? [[SCStreamFrameInfo: Any]],
711
+ let raw = attachments.first?[.status] as? Int, SCFrameStatus(rawValue: raw) == .complete,
712
+ let pixels = CMSampleBufferGetImageBuffer(buffer) else { return }
713
+ let image = CIImage(cvPixelBuffer: pixels)
714
+ guard let cg = context.createCGImage(image, from: image.extent)
715
+ else { return }
716
+ let data = NSMutableData()
717
+ guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)
718
+ else { return }
719
+ CGImageDestinationAddImage(destination, cg, [kCGImageDestinationLossyCompressionQuality: quality] as CFDictionary)
720
+ guard CGImageDestinationFinalize(destination) else { return }
721
+ var length = UInt32(data.length).bigEndian
722
+ out.write(Data(bytes: &length, count: 4))
723
+ out.write(data as Data)
724
+ }
725
+
726
+ func stream(_ stream: SCStream, didStopWithError error: Error) { exit(4) }
727
+ }
728
+
729
+ if CommandLine.arguments.count >= 2 && CommandLine.arguments[1] == "stream" {
730
+ let args = CommandLine.arguments
731
+ let fps = args.count > 2 ? Double(args[2]) ?? 15 : 15
732
+ let maxWidth = args.count > 3 ? Double(args[3]) ?? 640 : 640
733
+ let quality = args.count > 4 ? Double(args[4]) ?? 0.6 : 0.6
734
+ guard #available(macOS 15.0, *) else { exit(2) }
735
+ guard let app = runningApp(), let window = phoneWindow(app) else { exit(5) }
736
+ let frames = FrameStream(quality: quality)
737
+ do { try frames.start(window: window, fps: fps, maxWidth: maxWidth) } catch { exit(6) }
738
+ Thread.detachNewThread {
739
+ while readLine() != nil {}
740
+ exit(0)
741
+ }
742
+ dispatchMain()
743
+ }
744
+
745
+ // MARK: - files, by drag and drop (macOS 15.1+)
746
+
747
+ // `mirroring drop FX FY PATH...`: drags the files from a small Mobster window onto the phone at (FX, FY), fractions of
748
+ // the Mirroring window; the app in front on the phone receives them as a drop. `mirroring drag-out FX FY DEST`:
749
+ // long-presses the item at (FX, FY) on the phone, drags it onto a small Mobster window and saves what iOS hands over
750
+ // in DEST. Each prints one JSON line ({"ok":true,"sent":[...]} / {"ok":true,"received":[...]}, or {"ok":false,
751
+ // "code","error"}) and exits. The pointer is driven with HID events, so the Mirroring window must be in front.
752
+
753
+ final class DragSource: NSView, NSDraggingSource {
754
+ var urls: [URL] = []
755
+ var ended: ((NSDragOperation) -> Void)?
756
+ var started = false
757
+ override func mouseDown(with event: NSEvent) {}
758
+ override func mouseDragged(with event: NSEvent) {
759
+ if started { return }
760
+ started = true
761
+ let items = urls.map { url -> NSDraggingItem in
762
+ let item = NSDraggingItem(pasteboardWriter: url as NSURL)
763
+ item.setDraggingFrame(NSRect(x: 0, y: 0, width: 32, height: 32), contents: NSWorkspace.shared.icon(forFile: url.path))
764
+ return item
765
+ }
766
+ beginDraggingSession(with: items, event: event, source: self)
767
+ }
768
+ func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
769
+ context == .outsideApplication ? [.copy, .generic] : []
770
+ }
771
+ func draggingSession(_ session: NSDraggingSession, endedAt point: NSPoint, operation: NSDragOperation) {
772
+ ended?(operation)
773
+ }
774
+ }
775
+
776
+ final class DropTarget: NSView {
777
+ var destination = URL(fileURLWithPath: NSTemporaryDirectory())
778
+ var finished: (([String], String?) -> Void)?
779
+ override init(frame: NSRect) {
780
+ super.init(frame: frame)
781
+ registerForDraggedTypes(NSFilePromiseReceiver.readableDraggedTypes.map { NSPasteboard.PasteboardType($0) }
782
+ + [.fileURL, .URL, .string, .png, .tiff])
783
+ }
784
+ required init?(coder: NSCoder) { nil }
785
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { .copy }
786
+ override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { .copy }
787
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
788
+ let board = sender.draggingPasteboard
789
+ if let promises = board.readObjects(forClasses: [NSFilePromiseReceiver.self]) as? [NSFilePromiseReceiver],
790
+ !promises.isEmpty {
791
+ var received: [String] = []
792
+ let group = DispatchGroup()
793
+ let queue = OperationQueue()
794
+ for promise in promises {
795
+ group.enter()
796
+ promise.receivePromisedFiles(atDestination: destination, options: [:], operationQueue: queue) { url, error in
797
+ if error == nil { received.append(url.path) }
798
+ group.leave()
799
+ }
800
+ }
801
+ group.notify(queue: .main) { self.finished?(received, received.isEmpty ? "refused" : nil) }
802
+ return true
803
+ }
804
+ if let urls = board.readObjects(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) as? [URL],
805
+ !urls.isEmpty {
806
+ var copied: [String] = []
807
+ for url in urls {
808
+ let target = destination.appendingPathComponent(url.lastPathComponent)
809
+ if (try? FileManager.default.copyItem(at: url, to: target)) != nil { copied.append(target.path) }
810
+ }
811
+ finished?(copied, copied.isEmpty ? "refused" : nil)
812
+ return true
813
+ }
814
+ if let image = NSImage(pasteboard: board), let tiff = image.tiffRepresentation,
815
+ let png = NSBitmapImageRep(data: tiff)?.representation(using: .png, properties: [:]) {
816
+ let target = destination.appendingPathComponent("from-iphone-\(Int(Date().timeIntervalSince1970)).png")
817
+ if (try? png.write(to: target)) != nil {
818
+ finished?([target.path], nil)
819
+ return true
820
+ }
821
+ }
822
+ if let text = board.string(forType: .string) {
823
+ let target = destination.appendingPathComponent("from-iphone-\(Int(Date().timeIntervalSince1970)).txt")
824
+ if (try? text.write(to: target, atomically: true, encoding: .utf8)) != nil {
825
+ finished?([target.path], nil)
826
+ return true
827
+ }
828
+ }
829
+ finished?([], "refused")
830
+ return false
831
+ }
832
+ }
833
+
834
+ func finishTransfer(_ value: [String: Any]) -> Never {
835
+ emit(value)
836
+ exit(0)
837
+ }
838
+
839
+ // A small floating window of Mobster's at a global top-left point (CG coordinates), holding ``view``.
840
+ func transferWindow(at point: CGPoint, view: NSView) -> NSWindow {
841
+ let screenHeight = NSScreen.screens.first?.frame.height ?? 900
842
+ let size: CGFloat = 72
843
+ let frame = NSRect(x: point.x - size / 2, y: screenHeight - point.y - size / 2, width: size, height: size)
844
+ let window = NSWindow(contentRect: frame, styleMask: [.borderless], backing: .buffered, defer: false)
845
+ window.level = .floating
846
+ window.isOpaque = false
847
+ window.backgroundColor = NSColor.systemPink.withAlphaComponent(0.25)
848
+ view.frame = NSRect(x: 0, y: 0, width: size, height: size)
849
+ window.contentView = view
850
+ window.orderFrontRegardless()
851
+ return window
852
+ }
853
+
854
+ func glide(_ from: CGPoint, _ to: CGPoint, steps: Int = 24, seconds: Double = 0.6) {
855
+ for step in 1...steps {
856
+ let t = Double(step) / Double(steps)
857
+ postHID(.leftMouseDragged, CGPoint(x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t))
858
+ usleep(useconds_t(seconds / Double(steps) * 1_000_000))
859
+ }
860
+ }
861
+
862
+ func runTransfer(_ args: [String]) -> Never {
863
+ guard #available(macOS 15.1, *) else {
864
+ finishTransfer(["ok": false, "code": "unsupported", "error": "Dragging files to and from iPhone Mirroring needs macOS 15.1 or later"])
865
+ }
866
+ let mode = args[1]
867
+ guard args.count >= 5, let fx = Double(args[2]), let fy = Double(args[3]), (0...1).contains(fx), (0...1).contains(fy)
868
+ else { finishTransfer(["ok": false, "code": "usage", "error": "usage: drop FX FY PATH... | drag-out FX FY DEST"]) }
869
+ let app = NSApplication.shared
870
+ app.setActivationPolicy(.accessory)
871
+ var mirror: Window
872
+ do {
873
+ foreground = true // the drag needs the window in front and the Mac's pointer
874
+ mirror = try readyForInput().target.window
875
+ } catch let error as HelperError {
876
+ finishTransfer(["ok": false, "code": error.code, "error": error.message])
877
+ } catch {
878
+ finishTransfer(["ok": false, "code": "capture", "error": "iPhone Mirroring isn't ready"])
879
+ }
880
+ let phonePoint = CGPoint(x: mirror.frame.minX + mirror.frame.width * fx, y: mirror.frame.minY + mirror.frame.height * fy)
881
+ // Mobster's window sits just right of the Mirroring window (left of it when there's no room).
882
+ let screenWidth = NSScreen.screens.first?.frame.width ?? 1440
883
+ let side = mirror.frame.maxX + 60 < screenWidth ? mirror.frame.maxX + 50 : mirror.frame.minX - 50
884
+ let macPoint = CGPoint(x: side, y: mirror.frame.midY)
885
+ let deadline = DispatchTime.now() + 60
886
+ DispatchQueue.main.asyncAfter(deadline: deadline) {
887
+ finishTransfer(["ok": false, "code": "refused", "error": "Nothing came through in 60 s"])
888
+ }
889
+ if mode == "drop" {
890
+ let paths = Array(args[4...])
891
+ let urls = paths.map { URL(fileURLWithPath: $0) }
892
+ for url in urls where !FileManager.default.fileExists(atPath: url.path) {
893
+ finishTransfer(["ok": false, "code": "usage", "error": "No file at \(url.lastPathComponent)"])
894
+ }
895
+ let source = DragSource()
896
+ source.urls = urls
897
+ source.ended = { operation in
898
+ finishTransfer(operation.isEmpty
899
+ ? ["ok": false, "code": "refused", "error": "The app on the phone didn't take the drop"]
900
+ : ["ok": true, "sent": urls.map { $0.lastPathComponent }])
901
+ }
902
+ let window = transferWindow(at: macPoint, view: source)
903
+ _ = window
904
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
905
+ postHID(.mouseMoved, macPoint)
906
+ usleep(80_000)
907
+ postHID(.leftMouseDown, macPoint)
908
+ usleep(80_000)
909
+ glide(macPoint, CGPoint(x: macPoint.x + 6, y: macPoint.y + 6), steps: 4, seconds: 0.1)
910
+ glide(CGPoint(x: macPoint.x + 6, y: macPoint.y + 6), phonePoint)
911
+ usleep(700_000) // hover: the phone shows where the drop goes
912
+ postHID(.leftMouseUp, phonePoint)
913
+ }
914
+ } else if mode == "drag-out" {
915
+ let destination = URL(fileURLWithPath: args[4], isDirectory: true)
916
+ try? FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
917
+ let target = DropTarget(frame: .zero)
918
+ target.destination = destination
919
+ target.finished = { received, failure in
920
+ finishTransfer(failure == nil ? ["ok": true, "received": received]
921
+ : ["ok": false, "code": failure!, "error": "Nothing came off the phone"])
922
+ }
923
+ let window = transferWindow(at: macPoint, view: target)
924
+ _ = window
925
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
926
+ postHID(.mouseMoved, phonePoint)
927
+ usleep(80_000)
928
+ postHID(.leftMouseDown, phonePoint)
929
+ usleep(900_000) // a long press lifts the item on iOS
930
+ glide(phonePoint, CGPoint(x: phonePoint.x + 8, y: phonePoint.y + 8), steps: 6, seconds: 0.3)
931
+ glide(CGPoint(x: phonePoint.x + 8, y: phonePoint.y + 8), macPoint, steps: 30, seconds: 0.9)
932
+ usleep(600_000)
933
+ postHID(.leftMouseUp, macPoint)
934
+ }
935
+ } else {
936
+ finishTransfer(["ok": false, "code": "usage", "error": "Unknown transfer"])
937
+ }
938
+ app.run()
939
+ exit(0)
940
+ }
941
+
942
+ if CommandLine.arguments.count >= 2 && ["drop", "drag-out"].contains(CommandLine.arguments[1]) {
943
+ runTransfer(CommandLine.arguments)
944
+ }
945
+
946
+ // MARK: - the phone's sound (iPhone Mirroring plays it on the Mac)
947
+
948
+ // `mirroring audio --seconds N --out FILE [--live]`: records the iPhone Mirroring app's audio (ScreenCaptureKit) into
949
+ // FILE for N seconds (1 to 120), then prints {"ok":true,"recordedSeconds","peakDb","file"}. With --live it prints
950
+ // {"event":"recording"} once audio flows and {"event":"level","t","db"} every half second, and ends early when standard
951
+ // input reads "stop" or closes: phone-audio's live protocol, so mobile_agent/listen.py reads both the same way.
952
+ // Speech and sounds are told apart afterwards by phone-audio's transcribe and classify, on this Mac.
953
+ @available(macOS 14.0, *)
954
+ final class AudioRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
955
+ let url: URL
956
+ let live: Bool
957
+ var file: AVAudioFile?
958
+ var stream: SCStream?
959
+ var started: Date?
960
+ var peak: Float = -160
961
+ var levelSum: Float = 0
962
+ var levelCount = 0
963
+ var lastLevel = Date.distantPast
964
+ let lock = NSLock()
965
+ init(url: URL, live: Bool) { self.url = url; self.live = live }
966
+
967
+ func start(app: SCRunningApplication, display: SCDisplay) throws {
968
+ let filter = SCContentFilter(display: display, including: [app], exceptingWindows: [])
969
+ let config = SCStreamConfiguration()
970
+ config.capturesAudio = true
971
+ config.excludesCurrentProcessAudio = true
972
+ config.sampleRate = 48000
973
+ config.channelCount = 1
974
+ config.width = 2
975
+ config.height = 2
976
+ config.minimumFrameInterval = CMTime(value: 1, timescale: 1)
977
+ let stream = SCStream(filter: filter, configuration: config, delegate: self)
978
+ try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: DispatchQueue(label: "audio"))
979
+ self.stream = stream
980
+ stream.startCapture { error in
981
+ if let error { emit(["ok": false, "error": "capture_failed", "message": "Mobster couldn't capture iPhone Mirroring's sound (\(type(of: error)))."]); exit(0) }
982
+ }
983
+ }
984
+
985
+ func stream(_ stream: SCStream, didOutputSampleBuffer buffer: CMSampleBuffer, of type: SCStreamOutputType) {
986
+ guard type == .audio, buffer.isValid, let description = buffer.formatDescription else { return }
987
+ let format = AVAudioFormat(cmAudioFormatDescription: description)
988
+ let frames = AVAudioFrameCount(CMSampleBufferGetNumSamples(buffer))
989
+ guard frames > 0, let pcm = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frames) else { return }
990
+ pcm.frameLength = frames
991
+ guard CMSampleBufferCopyPCMDataIntoAudioBufferList(buffer, at: 0, frameCount: Int32(frames),
992
+ into: pcm.mutableAudioBufferList) == noErr else { return }
993
+ lock.lock()
994
+ defer { lock.unlock() }
995
+ if file == nil {
996
+ file = try? AVAudioFile(forWriting: url, settings: format.settings)
997
+ started = Date()
998
+ if live { emit(["event": "recording"]) }
999
+ }
1000
+ try? file?.write(from: pcm)
1001
+ if let data = pcm.floatChannelData?[0] {
1002
+ var sum: Float = 0
1003
+ for i in 0..<Int(frames) { sum += data[i] * data[i] }
1004
+ let rms = sqrt(sum / Float(frames))
1005
+ let db = rms > 0 ? 20 * log10(rms) : -160
1006
+ peak = max(peak, db)
1007
+ levelSum += db
1008
+ levelCount += 1
1009
+ }
1010
+ if live, Date().timeIntervalSince(lastLevel) >= 0.5, levelCount > 0, let started {
1011
+ emit(["event": "level", "t": Date().timeIntervalSince(started), "db": Double(levelSum / Float(levelCount))])
1012
+ levelSum = 0
1013
+ levelCount = 0
1014
+ lastLevel = Date()
1015
+ }
1016
+ }
1017
+
1018
+ func finish() -> Never {
1019
+ let done = DispatchSemaphore(value: 0)
1020
+ stream?.stopCapture { _ in done.signal() }
1021
+ _ = done.wait(timeout: .now() + 3)
1022
+ lock.lock()
1023
+ let seconds = started.map { Date().timeIntervalSince($0) } ?? 0
1024
+ file = nil // closes the file
1025
+ lock.unlock()
1026
+ if seconds == 0 {
1027
+ emit(["ok": false, "error": "capture_failed", "message": "No sound came from iPhone Mirroring. Check that it's connected and the media is playing."])
1028
+ } else {
1029
+ emit(["ok": true, "recordedSeconds": seconds, "peakDb": Double(peak), "file": url.path])
1030
+ }
1031
+ exit(0)
1032
+ }
1033
+
1034
+ func stream(_ stream: SCStream, didStopWithError error: Error) { finish() }
1035
+ }
1036
+
1037
+ func runAudio(_ args: [String]) -> Never {
1038
+ func option(_ name: String) -> String? {
1039
+ guard let index = args.firstIndex(of: name), index + 1 < args.count else { return nil }
1040
+ return args[index + 1]
1041
+ }
1042
+ guard #available(macOS 15.0, *) else {
1043
+ emit(["ok": false, "error": "capture_failed", "message": "iPhone Mirroring needs macOS 15 or later."]); exit(0)
1044
+ }
1045
+ guard let out = option("--out") else {
1046
+ emit(["ok": false, "error": "usage", "message": "audio --seconds N --out FILE [--live]"]); exit(2)
1047
+ }
1048
+ let seconds = min(120, max(1, Double(option("--seconds") ?? "10") ?? 10))
1049
+ guard let running = runningApp() else {
1050
+ emit(["ok": false, "error": "no_device", "message": "iPhone Mirroring isn't open. Open it and connect your iPhone, then try again."]); exit(0)
1051
+ }
1052
+ let done = DispatchSemaphore(value: 0)
1053
+ let found = Box<(SCShareableContent?, Error?)>((nil, nil))
1054
+ SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: false) { content, error in
1055
+ found.value = (content, error)
1056
+ done.signal()
1057
+ }
1058
+ _ = done.wait(timeout: .now() + 5)
1059
+ guard let content = found.value.0,
1060
+ let app = content.applications.first(where: { $0.processID == running.processIdentifier }),
1061
+ let display = content.displays.first else {
1062
+ emit(["ok": false, "error": "permission", "message": "Mobster needs Screen Recording to hear iPhone Mirroring. Allow it in System Settings › Privacy & Security › Screen & System Audio Recording."]); exit(0)
1063
+ }
1064
+ let recorder = AudioRecorder(url: URL(fileURLWithPath: out), live: args.contains("--live"))
1065
+ do { try recorder.start(app: app, display: display) } catch {
1066
+ emit(["ok": false, "error": "capture_failed", "message": "Mobster couldn't capture iPhone Mirroring's sound."]); exit(0)
1067
+ }
1068
+ Thread.detachNewThread {
1069
+ while let line = readLine() { if line.trimmingCharacters(in: .whitespaces) == "stop" { break } }
1070
+ if args.contains("--live") { recorder.finish() }
1071
+ }
1072
+ DispatchQueue.global().asyncAfter(deadline: .now() + seconds) { recorder.finish() }
1073
+ dispatchMain()
1074
+ }
1075
+
1076
+ if CommandLine.arguments.count >= 2 && CommandLine.arguments[1] == "audio" {
1077
+ runAudio(CommandLine.arguments)
1078
+ }
1079
+
1080
+ while let line = readLine() {
1081
+ guard let data = line.data(using: .utf8),
1082
+ let request = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
1083
+ emit(["ok": false, "code": "usage", "error": "Each line must be a JSON object"])
1084
+ continue
1085
+ }
1086
+ do {
1087
+ var out = try handle(request)
1088
+ if let id = request["id"] { out["id"] = id }
1089
+ emit(out)
1090
+ } catch let error as HelperError {
1091
+ var out: [String: Any] = ["ok": false, "code": error.code, "error": error.message]
1092
+ if let id = request["id"] { out["id"] = id }
1093
+ emit(out)
1094
+ } catch {
1095
+ var out: [String: Any] = ["ok": false, "code": "capture", "error": "\(type(of: error))"]
1096
+ if let id = request["id"] { out["id"] = id }
1097
+ emit(out)
1098
+ }
1099
+ }