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,895 @@
1
+ """Transfers: photos, videos and files between this Mac and an iPhone, both ways, over every connection Mobster has,
2
+ checked by count and checksum, with progress. `mobster phone photos`/`file`, MCP's send_media and pull_media, the Mac
3
+ app (POST /api/transfers) and the content queue all use it.
4
+
5
+ Routes, in the order ``auto`` tries them:
6
+
7
+ | Direction | What | Route (``via``) | How | Checked by |
8
+ |---|---|---|---|---|
9
+ | to the iPhone | photos, videos | ``cable`` / ``wifi`` | Mobster's runner (photos.send) on a USB or Wi-Fi phone | SHA-256 on the phone, the library's count |
10
+ | | | ``airdrop`` | AirDrop to the phone by its name (handoff.airdrop) | the count, and SHA-256 on the phone where it can read it |
11
+ | | | ``mirroring`` | a drop on the iPhone Mirroring window, Photos in front | the same |
12
+ | | | ``icloud`` | this Mac's Photos library, then iCloud Photos (icloud.py); only when chosen | the same, once it arrives |
13
+ | | files | ``cable`` | an app's Documents folder, or a debug build's own folder (files.py) | size, then SHA-256 read back |
14
+ | to this Mac | photos, videos | ``cable`` | the camera roll's files (photos.CameraRoll, AFC) | size against the phone's listing |
15
+ | | | ``wifi`` | the runner reads the originals (photos.export) | SHA-256 on the phone against this Mac's |
16
+ | | | ``tunnel`` | Apple's own tunnel, no cable (tunnel.py, pymobiledevice3 if installed) | nothing is compared; the SHA-256 of what arrived is kept |
17
+ | | | ``icloud`` | this Mac's Photos library (icloud.py); only when chosen | not checked |
18
+ | | files | ``cable`` | files.py get | size against the phone's listing |
19
+
20
+ Rules every route follows:
21
+
22
+ - **One route per transfer.** ``auto`` moves to the next route only when the one before refused before anything was
23
+ sent (not set up, no cable, a helper missing). Once a route sent anything, it is never tried again on another route:
24
+ an outcome that is unknown stays unknown ("Look in Photos before you send it again"), so nothing arrives twice.
25
+ - **Checked.** Each item ends ``verified`` (SHA-256 matches on both sides), ``counted`` (the library grew by the
26
+ number sent, or a pulled file's size matched the phone's listing, but no checksum was compared), ``sent`` (the route
27
+ said it went; nothing on the phone could be read or compared) or ``failed``. The transfer keeps the library's count
28
+ before and after.
29
+ - **Each transfer names one phone.** Several phones run their transfers at the same time; one phone takes one at a
30
+ time (``Transfers``' per-phone lock).
31
+ """ # noqa: E501
32
+
33
+ import contextlib
34
+ import os
35
+ import threading
36
+ import time
37
+ import uuid
38
+ from dataclasses import dataclass, field
39
+ from pathlib import Path
40
+ from typing import Any, Callable, Optional
41
+
42
+ from . import PhoneIOError, checked_request
43
+
44
+ DIRECTIONS = ("to_phone", "to_mac")
45
+ WHAT = ("photos", "files")
46
+ VIA = ("auto", "cable", "wifi", "airdrop", "mirroring", "icloud", "tunnel")
47
+ ROUTES = {
48
+ ("to_phone", "photos"): ("cable", "wifi", "airdrop", "mirroring", "icloud"),
49
+ ("to_phone", "files"): ("cable",),
50
+ ("to_mac", "photos"): ("cable", "wifi", "tunnel", "icloud"),
51
+ ("to_mac", "files"): ("cable",),
52
+ }
53
+ # Routes ``auto`` never picks: they are slow or reach outside the two devices, so the person chooses them.
54
+ CHOSEN_ONLY = frozenset({"icloud"})
55
+ # Codes that mean a route refused before sending anything: auto may try the next one.
56
+ NOT_SENT = frozenset({"needs_cable", "no_device", "tools", "needs_rebuild", "no_mirroring", "permission",
57
+ "airdrop_not_found", "airdrop_ambiguous", "not_ready", "usage_route"})
58
+ ARRIVAL_WAIT_S = {"airdrop": 60, "mirroring": 60, "icloud": 600}
59
+ POLL_S = 2.0
60
+ MAX_ITEMS = 50
61
+ KEEP = 50
62
+ ALBUM = "Mobster"
63
+
64
+
65
+ @dataclass
66
+ class Phone:
67
+ """One iPhone as a transfer reaches it."""
68
+ record: dict
69
+ runner: Optional[Callable[[], Any]] = None
70
+ files: Optional[Callable[..., Any]] = None
71
+ roll: Optional[Callable[..., Any]] = None
72
+ tunnel: Optional[Callable[..., Any]] = None
73
+ handoff: Any = None
74
+ icloud: Any = None
75
+ mirroring: Optional[Callable[..., Any]] = None
76
+
77
+ @property
78
+ def id(self):
79
+ return self.record.get("id") or self.record.get("udid") or self.name
80
+
81
+ @property
82
+ def name(self):
83
+ return self.record.get("name") or self.record.get("id") or "the iPhone"
84
+
85
+ @property
86
+ def cabled(self):
87
+ return self.record.get("kind") == "usb" and self.record.get("transport") != "wifi"
88
+
89
+ def phone_files(self, container=False):
90
+ if self.files is not None:
91
+ return self.files(self.record, container=container)
92
+ from .files import PhoneFiles
93
+ return PhoneFiles(self.record, container=container)
94
+
95
+ def camera_roll(self):
96
+ if self.roll is not None:
97
+ return self.roll(self.record)
98
+ from .photos import CameraRoll
99
+ return CameraRoll(self.phone_files())
100
+
101
+ def tunnel_roll(self):
102
+ if self.tunnel is not None:
103
+ return self.tunnel(self.record)
104
+ from .tunnel import TunnelRoll
105
+ return TunnelRoll(self.record)
106
+
107
+ def handoffs(self):
108
+ if self.handoff is not None:
109
+ return self.handoff
110
+ from . import handoff
111
+ return handoff
112
+
113
+ def cloud(self):
114
+ if self.icloud is not None:
115
+ return self.icloud
116
+ from . import icloud
117
+ return icloud
118
+
119
+
120
+ def phone_from_record(record):
121
+ """A Phone for a devices.py record outside the Mac app; its runner holds the device's lease while open."""
122
+ from . import wda
123
+
124
+ @contextlib.contextmanager
125
+ def runner():
126
+ with wda(record) as request:
127
+ yield request
128
+ return Phone(record, runner=runner if record.get("wdaUrl") else None)
129
+
130
+
131
+ def phone_from_runtime(runtime, device=None):
132
+ """A Phone for a device of the running Mobster; its runner is refused while a task runs on that phone."""
133
+ import json
134
+ import urllib.request
135
+
136
+ from ..journal import JournalError, Lease, LeaseHeld
137
+
138
+ fleet_phone = runtime.resolve_device(device)
139
+ record = {"id": fleet_phone.id, "kind": fleet_phone.kind, "udid": fleet_phone.udid,
140
+ "name": fleet_phone.name or fleet_phone.id, "transport": getattr(fleet_phone, "transport", None),
141
+ "wdaUrl": fleet_phone.wda_url}
142
+
143
+ @contextlib.contextmanager
144
+ def runner():
145
+ def check_busy():
146
+ if getattr(runtime, "closing", False):
147
+ raise PhoneIOError("Mobster is closing; the transfer did not start.", "device_busy")
148
+ if runtime.device_busy(fleet_phone.id):
149
+ raise PhoneIOError(f"{record['name']} is busy with a task.", "device_busy", "Try again when it ends.")
150
+ if fleet_phone.wda_url != record["wdaUrl"]:
151
+ raise PhoneIOError(f"{record['name']}'s connection changed before the transfer started.",
152
+ "device_busy", "Try the transfer again on its current connection.")
153
+
154
+ check_busy()
155
+ url = (record["wdaUrl"] or "").rstrip("/")
156
+ if not url:
157
+ raise PhoneIOError(f"{record['name']} isn't set up yet.", "no_device", "Set it up in Mobster.")
158
+ try:
159
+ held = Lease.device(url, wait=0, udid=record["udid"])
160
+ except (LeaseHeld, JournalError) as error:
161
+ raise PhoneIOError(f"{record['name']} is busy or its device lock is unavailable: {error}",
162
+ "device_busy", "Try again when its task ends.") from None
163
+ try:
164
+ check_busy() # Admission may have changed while the cross-process lease was being acquired.
165
+
166
+ def send(method, path, body, timeout):
167
+ data = json.dumps(body).encode() if body is not None else None
168
+ req = urllib.request.Request(f"{url}/session/{fleet_phone.wda_session()}{path}", data=data,
169
+ method=method, headers={"Content-Type": "application/json"})
170
+ with urllib.request.urlopen(req, timeout=timeout) as response:
171
+ return json.load(response)
172
+
173
+ yield checked_request(send)
174
+ finally:
175
+ held.close()
176
+ return Phone(record, runner=runner if fleet_phone.wda_url else None)
177
+
178
+
179
+ @dataclass
180
+ class Item:
181
+ name: str
182
+ bytes: int = 0
183
+ sent: int = 0
184
+ state: str = "queued" # queued | sending | checking | verified | counted | sent | failed | unknown
185
+ kind: Optional[str] = None
186
+ sha256: Optional[str] = None
187
+ path: Optional[str] = None # the Mac file (to the iPhone: the source; to this Mac: where it landed)
188
+ remote: Optional[str] = None # where it is on the phone: a Photos id or a path, when known
189
+ error: Optional[str] = None
190
+
191
+ def public(self):
192
+ return {"name": self.name, "bytes": self.bytes, "sent": self.sent, "state": self.state, "kind": self.kind,
193
+ "sha256": self.sha256, "path": self.path, "remote": self.remote, "error": self.error}
194
+
195
+
196
+ @dataclass
197
+ class Transfer:
198
+ """One transfer: what moves, which way, to or from which phone, the route it took and how each item ended."""
199
+ direction: str
200
+ what: str
201
+ phone: Phone
202
+ via: str = "auto"
203
+ paths: list = field(default_factory=list) # to the iPhone: the Mac files
204
+ folder: Optional[str] = None # to this Mac: where files land
205
+ count: int = 1 # to this Mac, photos: how many of the newest
206
+ since: Optional[float] = None
207
+ names: list = field(default_factory=list) # to this Mac, files: the names in the app's folder
208
+ app: Optional[str] = None # files: the app's bundle ID
209
+ container: bool = False
210
+ album: Optional[str] = ALBUM
211
+ convert_heic: bool = False
212
+ date_now: bool = False # to the iPhone over the runner: first in Recents (the queue)
213
+ queue_item: Optional[str] = None # the content queue's post this transfer is for
214
+ id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
215
+ state: str = "queued" # queued | running | done | failed | unknown
216
+ route: Optional[str] = None
217
+ items: list = field(default_factory=list)
218
+ before: Optional[int] = None # the phone's photos and videos, before and after
219
+ after: Optional[int] = None
220
+ error: Optional[dict] = None
221
+ tried: list = field(default_factory=list) # routes that refused before sending: [{"via", "why"}]
222
+ created_at: float = field(default_factory=time.time)
223
+ finished_at: Optional[float] = None
224
+ listeners: list = field(default_factory=list)
225
+
226
+ def public(self):
227
+ done = sum(i.sent for i in self.items)
228
+ total = sum(i.bytes for i in self.items)
229
+ return {"id": self.id, "direction": self.direction, "what": self.what, "device": self.phone.id,
230
+ "deviceName": self.phone.name, "via": self.via, "route": self.route, "state": self.state,
231
+ "items": [i.public() for i in self.items], "bytes": total, "sent": done,
232
+ "before": self.before, "after": self.after, "error": self.error, "tried": list(self.tried),
233
+ "album": self.album if self.direction == "to_phone" and self.what == "photos" else None,
234
+ "app": self.app, "folder": self.folder, "queueItem": self.queue_item,
235
+ "createdAt": round(self.created_at * 1000),
236
+ "finishedAt": round(self.finished_at * 1000) if self.finished_at else None,
237
+ "verified": bool(self.items) and all(i.state == "verified" for i in self.items)}
238
+
239
+ def changed(self):
240
+ for listener in list(self.listeners):
241
+ try:
242
+ listener(self)
243
+ except Exception: # noqa: BLE001 - a listener never stops a transfer
244
+ pass
245
+
246
+
247
+ def validate(transfer):
248
+ if transfer.direction not in DIRECTIONS or transfer.what not in WHAT:
249
+ raise PhoneIOError("A transfer goes to_phone or to_mac, with photos or files.", "usage")
250
+ if transfer.via not in VIA or (transfer.via != "auto" and transfer.via not in
251
+ ROUTES[(transfer.direction, transfer.what)]):
252
+ allowed = ", ".join(ROUTES[(transfer.direction, transfer.what)])
253
+ raise PhoneIOError(f"That route doesn't move {transfer.what} that way. Choose auto or one of: {allowed}.",
254
+ "usage")
255
+ if transfer.direction == "to_phone":
256
+ if not transfer.paths or len(transfer.paths) > MAX_ITEMS:
257
+ raise PhoneIOError(f"Name 1 to {MAX_ITEMS} files to send.", "usage")
258
+ for path in transfer.paths:
259
+ if not Path(path).expanduser().is_file():
260
+ raise PhoneIOError(f"There is no file at {path}.", "usage")
261
+ if transfer.what == "files" and not transfer.app:
262
+ raise PhoneIOError("Name the app whose folder the files go to or come from (its bundle ID).", "usage")
263
+ if transfer.direction == "to_mac" and not transfer.folder:
264
+ raise PhoneIOError("Name the folder on this Mac the files go to.", "usage")
265
+ if transfer.album is not None and (not isinstance(transfer.album, str) or len(transfer.album) > 100):
266
+ raise PhoneIOError("An album's name is at most 100 characters.", "usage")
267
+
268
+
269
+ def _candidates(transfer):
270
+ order = ROUTES[(transfer.direction, transfer.what)]
271
+ if transfer.via != "auto":
272
+ return [transfer.via]
273
+ return [route for route in order if route not in CHOSEN_ONLY]
274
+
275
+
276
+ def _ready(transfer, route):
277
+ """None when ``route`` can run now, else why not (a sentence)."""
278
+ phone = transfer.phone
279
+ if phone.record.get("kind") == "simulator" and route != "cable":
280
+ return "a simulator takes files only from this Mac directly"
281
+ if route == "cable":
282
+ if transfer.direction == "to_phone" and transfer.what == "photos":
283
+ if phone.record.get("kind") == "simulator":
284
+ return None
285
+ return None if phone.cabled and phone.runner is not None else (
286
+ "it isn't on a cable" if not phone.cabled else "Mobster's runner isn't set up on it")
287
+ return None if phone.cabled else "it isn't on a cable"
288
+ if route == "wifi":
289
+ if phone.runner is None:
290
+ return "Mobster's runner isn't set up on it"
291
+ return None if not phone.cabled or transfer.via == "wifi" else "it's on the cable (the cable route goes first)"
292
+ if route == "tunnel":
293
+ from .tunnel import tool
294
+ return None if phone.tunnel is not None or tool() else "pymobiledevice3 isn't installed"
295
+ if route == "mirroring":
296
+ if phone.mirroring is not None:
297
+ return None
298
+ if not _mirrors(phone):
299
+ return "this Mac isn't set to mirror it (MOBSTER_MIRRORS names the iPhone it mirrors)"
300
+ return None
301
+ return None
302
+
303
+
304
+ def mirrored_phone():
305
+ """The iPhone this Mac mirrors, as named in MOBSTER_MIRRORS: a drop on the wrong phone can't be undone."""
306
+ value = os.environ.get("MOBSTER_MIRRORS", "").strip()
307
+ return value or None
308
+
309
+
310
+ def _mirrors(phone):
311
+ named = mirrored_phone()
312
+ return named is not None and named in (phone.name, phone.record.get("udid"), phone.record.get("id"))
313
+
314
+
315
+ def run(transfer, *, clock=time.monotonic, sleep=time.sleep):
316
+ """Run ``transfer`` to its end, updating it as it goes (``transfer.changed()`` after each step). Never raises for
317
+ a phone's refusal: the transfer ends ``failed`` with ``error``. Returns the transfer."""
318
+ validate(transfer)
319
+ transfer.state = "running"
320
+ transfer.changed()
321
+ last = None
322
+ for route in _candidates(transfer):
323
+ why = _ready(transfer, route)
324
+ if why is not None:
325
+ transfer.tried.append({"via": route, "why": why})
326
+ last = PhoneIOError(f"{transfer.phone.name}: {why}.", "not_ready")
327
+ continue
328
+ transfer.route = route
329
+ transfer.items = []
330
+ try:
331
+ _ROUTERS[(transfer.direction, transfer.what)](transfer, route, clock, sleep)
332
+ except PhoneIOError as error:
333
+ if error.code == "device_busy":
334
+ _end(transfer, error)
335
+ return transfer
336
+ # A NOT_SENT code is raised only before anything went, and a copy to this Mac changes nothing on the
337
+ # phone: unless an item already arrived, try the next route.
338
+ if (error.code in NOT_SENT or transfer.direction == "to_mac") and not any(
339
+ i.sent or i.state in ("verified", "counted", "sent", "unknown") for i in transfer.items):
340
+ transfer.tried.append({"via": route, "why": str(error)})
341
+ last = error
342
+ if transfer.via == "auto":
343
+ continue
344
+ _end(transfer, error)
345
+ return transfer
346
+ transfer.state = "done" if all(i.state in ("verified", "counted", "sent") for i in transfer.items) else "failed"
347
+ if transfer.state == "failed" and any(i.state == "unknown" for i in transfer.items):
348
+ transfer.state = "unknown"
349
+ transfer.finished_at = time.time()
350
+ transfer.changed()
351
+ return transfer
352
+ _end(transfer, last or PhoneIOError("No route reaches that iPhone right now.", "not_ready"))
353
+ return transfer
354
+
355
+
356
+ def _end(transfer, error):
357
+ transfer.state = "unknown" if error.code == "unknown" else "failed"
358
+ transfer.error = error.public()
359
+ for item in transfer.items:
360
+ if item.state in ("queued", "sending", "checking"):
361
+ item.state = "unknown" if error.code == "unknown" and item.state != "queued" else "failed"
362
+ item.error = item.error or str(error)
363
+ transfer.finished_at = time.time()
364
+ transfer.changed()
365
+
366
+
367
+ def _items_from_paths(transfer):
368
+ from .photos import digest, sniff
369
+ items = []
370
+ for path in transfer.paths:
371
+ file = Path(path).expanduser()
372
+ kind, _ = sniff(file) if transfer.what == "photos" else (None, None)
373
+ items.append(Item(name=file.name, bytes=file.stat().st_size, kind=kind, sha256=digest(file),
374
+ path=str(file.resolve())))
375
+ transfer.items = items
376
+ transfer.changed()
377
+ return items
378
+
379
+
380
+ def _library_count(request):
381
+ from .photos import count
382
+ try:
383
+ value = count(request)
384
+ except PhoneIOError:
385
+ return None
386
+ if not value or "photos" not in value:
387
+ return None
388
+ return value["photos"] + value["videos"]
389
+
390
+
391
+ def _photos_to_phone(transfer, route, clock, sleep):
392
+ items = _items_from_paths(transfer)
393
+ phone = transfer.phone
394
+ if route in ("cable", "wifi"):
395
+ if phone.record.get("kind") == "simulator":
396
+ return _simulator_add(transfer)
397
+ from .photos import send
398
+ with phone.runner() as request:
399
+ transfer.before = _library_count(request)
400
+ transfer.changed()
401
+ for item in items:
402
+ item.state = "sending"
403
+ transfer.changed()
404
+
405
+ def progress(sent, total, item=item):
406
+ item.sent = sent
407
+ transfer.changed()
408
+ try:
409
+ result = send(request, item.path, name=phone.name, album=transfer.album, progress=progress,
410
+ sha256=item.sha256, date_now=transfer.date_now,
411
+ activate=lambda bundle: request("POST", "/wda/apps/activate", {"bundleId": bundle},
412
+ 15))
413
+ except PhoneIOError as error:
414
+ item.state = "unknown" if error.code == "unknown" else "failed"
415
+ item.error = str(error)
416
+ if item is items[0] and error.code in NOT_SENT:
417
+ raise
418
+ if error.code in ("unknown",):
419
+ raise
420
+ continue
421
+ item.sent, item.remote = item.bytes, result.get("localIdentifier")
422
+ item.state = "verified" if result.get("verified") else "sent"
423
+ if result.get("albumError"):
424
+ item.error = result["albumError"]
425
+ transfer.changed()
426
+ transfer.after = _library_count(request)
427
+ _settle_by_count(transfer)
428
+ return
429
+ # AirDrop, Mirroring and iCloud hand the files over and can't say whether the phone kept them: count before and
430
+ # after (the runner, else the cable's camera roll), then read each new item's checksum where it can.
431
+ watch = _Watch(transfer)
432
+ transfer.before = watch.count()
433
+ transfer.changed()
434
+ for item in items:
435
+ item.state = "sending"
436
+ transfer.changed()
437
+ paths = [item.path for item in items]
438
+ if route == "airdrop":
439
+ accepting = threading.Event()
440
+ watcher = threading.Thread(target=_accept_airdrop, args=(phone, accepting, sleep), daemon=True)
441
+ watcher.start()
442
+ try:
443
+ phone.handoffs().airdrop(paths, phone.name)
444
+ finally:
445
+ accepting.set()
446
+ watcher.join(timeout=5)
447
+ elif route == "mirroring":
448
+ _mirroring_photos(transfer, paths)
449
+ else:
450
+ phone.cloud().import_media(paths, album=transfer.album or ALBUM)
451
+ for item in items:
452
+ item.sent, item.state = item.bytes, "checking"
453
+ transfer.changed()
454
+ watch.wait_for(len(items), ARRIVAL_WAIT_S[route], clock, sleep)
455
+ watch.check(items)
456
+ transfer.changed()
457
+
458
+
459
+ def _accept_airdrop(phone, done, sleep=time.sleep, poll=1.0):
460
+ """While Mobster's own AirDrop is on its way, tap Accept on an alert whose text says AirDrop, and nothing else.
461
+ Not journaled: a transfer has no run, so the tap is bounded here (the alert's text, its Accept, the send)."""
462
+ if phone.runner is None:
463
+ return
464
+ try:
465
+ with phone.runner() as request:
466
+ while not done.is_set():
467
+ try:
468
+ text = request("GET", "/alert/text", None, 5)
469
+ if isinstance(text, str) and "airdrop" in text.casefold():
470
+ buttons = request("GET", "/wda/alert/buttons", None, 5)
471
+ if isinstance(buttons, list) and "Accept" in buttons:
472
+ request("POST", "/alert/accept", {"name": "Accept"}, 10)
473
+ return
474
+ except PhoneIOError:
475
+ pass
476
+ sleep(poll)
477
+ except PhoneIOError:
478
+ return
479
+
480
+
481
+ def _mirroring_photos(transfer, paths):
482
+ """Photos in front on the phone (Spotlight, through Mirroring), then the drop in the middle of its window."""
483
+ phone = transfer.phone
484
+ if phone.mirroring is not None:
485
+ phone.mirroring(paths)
486
+ return
487
+ open_on_mirroring("Photos")
488
+ phone.handoffs().mirroring_drop(paths, 0.5, 0.55)
489
+
490
+
491
+ def open_on_mirroring(app_name, helper=None, sleep=time.sleep):
492
+ """Open ``app_name`` on the mirrored iPhone with Spotlight, as Mirroring's driver opens apps."""
493
+ from ..mirroring import Helper, MirroringError
494
+ own = helper is None
495
+ try:
496
+ helper = helper or Helper()
497
+ except Exception as error: # noqa: BLE001 - no helper, no Xcode tools
498
+ raise PhoneIOError(f"Mobster's Mirroring helper couldn't start ({type(error).__name__}).",
499
+ "no_mirroring") from None
500
+ try:
501
+ state = helper.request("state")
502
+ if state.get("state") != "ready":
503
+ raise PhoneIOError("iPhone Mirroring isn't showing the iPhone.", "no_mirroring",
504
+ "Open iPhone Mirroring, lock the iPhone and keep it near this Mac, then try again.")
505
+ helper.request("key", combo="cmd+3")
506
+ sleep(0.8)
507
+ helper.request("type", text=app_name, mode="keys")
508
+ sleep(1.0)
509
+ helper.request("key", combo="return")
510
+ sleep(1.5)
511
+ except MirroringError as error:
512
+ raise PhoneIOError(f"iPhone Mirroring refused: {error}.", "no_mirroring") from None
513
+ finally:
514
+ if own:
515
+ try:
516
+ helper.request("restore", timeout=3)
517
+ except Exception: # noqa: BLE001
518
+ pass
519
+ helper.close()
520
+
521
+
522
+ def _settle_by_count(transfer):
523
+ """Items sent but not checked by checksum become ``counted`` when the library grew by exactly as many."""
524
+ if transfer.before is None or transfer.after is None:
525
+ return
526
+ sent = [i for i in transfer.items if i.state in ("verified", "sent")]
527
+ grew = transfer.after - transfer.before
528
+ for item in sent:
529
+ if item.state == "sent" and grew == len(sent):
530
+ item.state = "counted"
531
+ if grew != len(sent):
532
+ for item in sent:
533
+ item.error = item.error or (f"The library grew by {grew}, not {len(sent)}: look in Photos on "
534
+ f"{transfer.phone.name}.")
535
+
536
+
537
+ def _simulator_add(transfer):
538
+ import subprocess
539
+
540
+ from ..journal import JournalError, Lease, LeaseHeld
541
+ from ..native_helpers import require_devices
542
+ record = transfer.phone.record
543
+ url = record.get("wdaUrl")
544
+ if not url:
545
+ raise PhoneIOError(f"{transfer.phone.name} has no device lock identity.", "no_device")
546
+ udid = record.get("udid") or record.get("id")
547
+ if not isinstance(udid, str) or not udid:
548
+ raise PhoneIOError(f"{transfer.phone.name} has no simulator identity.", "no_device")
549
+ try:
550
+ held = Lease.device(url, wait=0, udid=udid)
551
+ except (LeaseHeld, JournalError) as error:
552
+ raise PhoneIOError(f"{transfer.phone.name} is busy or its device lock is unavailable: {error}",
553
+ "device_busy", "Try again when its task ends.") from None
554
+ try:
555
+ try:
556
+ require_devices()
557
+ except RuntimeError as error:
558
+ raise PhoneIOError(str(error), "tools") from None
559
+ for item in transfer.items:
560
+ item.state = "sending"
561
+ transfer.changed()
562
+ try:
563
+ done = subprocess.run(["xcrun", "simctl", "addmedia", udid, item.path], capture_output=True,
564
+ text=True, timeout=120)
565
+ except (OSError, subprocess.SubprocessError) as error:
566
+ raise PhoneIOError(f"The simulator import may have happened ({type(error).__name__}).",
567
+ "unknown", "Check Photos before sending it again.") from None
568
+ if done.returncode != 0:
569
+ raise PhoneIOError(f"The simulator import may have happened: "
570
+ f"{(done.stderr or done.stdout).strip()[:160]}", "unknown",
571
+ "Check Photos before sending it again.")
572
+ item.state, item.sent = "sent", item.bytes
573
+ transfer.changed()
574
+ finally:
575
+ held.close()
576
+
577
+
578
+ class _Watch:
579
+ """How many photos and videos the phone has, and which arrived."""
580
+
581
+ def __init__(self, transfer):
582
+ self.transfer = transfer
583
+ self.names_before = None
584
+
585
+ def _runner_count(self):
586
+ phone = self.transfer.phone
587
+ if phone.runner is None:
588
+ return None
589
+ try:
590
+ with phone.runner() as request:
591
+ return _library_count(request)
592
+ except PhoneIOError:
593
+ return None
594
+
595
+ def count(self):
596
+ found = self._runner_count()
597
+ if found is not None:
598
+ self.how = "runner"
599
+ return found
600
+ if self.transfer.phone.cabled:
601
+ try:
602
+ media = self.transfer.phone.camera_roll().media()
603
+ except PhoneIOError:
604
+ media = None
605
+ if media is not None:
606
+ self.how, self.names_before = "cable", dict(media)
607
+ return len(media)
608
+ self.how = None
609
+ return None
610
+
611
+ def wait_for(self, grew, limit, clock, sleep):
612
+ transfer = self.transfer
613
+ if transfer.before is None:
614
+ return
615
+ deadline = clock() + limit
616
+ while True:
617
+ now = self._again()
618
+ transfer.after = now
619
+ transfer.changed()
620
+ if now is not None and now - transfer.before >= grew or clock() >= deadline:
621
+ return
622
+ sleep(POLL_S)
623
+
624
+ def _again(self):
625
+ if self.how == "runner":
626
+ return self._runner_count()
627
+ if self.how == "cable":
628
+ try:
629
+ self.names_after = self.transfer.phone.camera_roll().media()
630
+ except PhoneIOError:
631
+ return None
632
+ return len(self.names_after)
633
+ return None
634
+
635
+ def check(self, items):
636
+ """Each item ``verified`` when a new file on the phone has its checksum, else ``counted`` or ``sent``."""
637
+ transfer = self.transfer
638
+ grew = None if transfer.before is None or transfer.after is None else transfer.after - transfer.before
639
+ matched = set()
640
+ if self.how == "cable" and getattr(self, "names_after", None) is not None:
641
+ new = {path: size for path, size in self.names_after.items() if path not in (self.names_before or {})}
642
+ roll = transfer.phone.camera_roll()
643
+ import tempfile
644
+
645
+ from .photos import digest
646
+ with tempfile.TemporaryDirectory(prefix="mobster-check-") as scratch:
647
+ for item in items:
648
+ for path, size in new.items():
649
+ if path in matched or size != item.bytes:
650
+ continue
651
+ try:
652
+ local = roll.fetch(path, scratch, size)
653
+ except PhoneIOError:
654
+ continue
655
+ if digest(local) == item.sha256:
656
+ matched.add(path)
657
+ item.state, item.remote = "verified", path
658
+ break
659
+ elif self.how == "runner":
660
+ matched = self._runner_checks(items, grew)
661
+ for item in items:
662
+ if item.state == "verified":
663
+ continue
664
+ if grew is not None and grew >= len(items):
665
+ item.state = "counted"
666
+ elif grew is None:
667
+ item.state = "sent"
668
+ item.error = item.error or f"Mobster couldn't read {transfer.phone.name}'s library to check it."
669
+ else:
670
+ item.state = "unknown"
671
+ item.error = (f"{transfer.phone.name}'s library grew by {grew} of {len(items)}. Look in Photos "
672
+ "before you send it again.")
673
+
674
+ def _runner_checks(self, items, grew):
675
+ from .photos import checksum, recent
676
+ phone = self.transfer.phone
677
+ found = set()
678
+ try:
679
+ with phone.runner() as request:
680
+ newest = recent(request, max(len(items), grew or 0) + 2, name=phone.name)
681
+ for item in items:
682
+ for candidate in newest:
683
+ if candidate["id"] in found:
684
+ continue
685
+ answer = checksum(request, candidate["id"], phone.name)
686
+ if answer and answer.get("sha256") == item.sha256:
687
+ found.add(candidate["id"])
688
+ item.state, item.remote = "verified", candidate["id"]
689
+ break
690
+ except PhoneIOError:
691
+ pass
692
+ return found
693
+
694
+
695
+ def _files_to_phone(transfer, route, clock, sleep):
696
+ import tempfile
697
+
698
+ from .photos import digest
699
+ items = _items_from_paths(transfer)
700
+ phone_files = transfer.phone.phone_files(container=transfer.container)
701
+ for item in items:
702
+ item.state = "sending"
703
+ transfer.changed()
704
+ try:
705
+ put = phone_files.put(transfer.app, item.path)
706
+ except PhoneIOError as error:
707
+ item.state, item.error = "failed", str(error)
708
+ if item is items[0] and error.code in NOT_SENT:
709
+ raise
710
+ continue
711
+ item.sent, item.remote, item.state = item.bytes, put["path"], "checking"
712
+ transfer.changed()
713
+ with tempfile.TemporaryDirectory(prefix="mobster-check-") as scratch:
714
+ try:
715
+ back = phone_files.get(transfer.app, put["name"], Path(scratch))
716
+ item.state = "verified" if digest(back) == item.sha256 else "failed"
717
+ if item.state == "failed":
718
+ item.error = f"{put['name']} on {transfer.phone.name} isn't the same as this Mac's copy."
719
+ except PhoneIOError:
720
+ item.state = "counted" # its size matched (files.put checks it); the read-back didn't run
721
+ transfer.changed()
722
+
723
+
724
+ def _photos_to_mac(transfer, route, clock, sleep):
725
+ folder = Path(transfer.folder).expanduser()
726
+ folder.mkdir(parents=True, exist_ok=True)
727
+ before = len([p for p in folder.iterdir() if p.is_file()])
728
+ phone = transfer.phone
729
+ if route == "wifi" or (route == "cable" and phone.record.get("kind") == "simulator"):
730
+ from .photos import export, recent
731
+ with phone.runner() as request:
732
+ chosen = recent(request, transfer.count, transfer.since, name=phone.name)
733
+ transfer.items = [Item(name=c["filename"] or c["id"], kind=c["kind"], remote=c["id"]) for c in chosen]
734
+ transfer.changed()
735
+ for item, picked in zip(transfer.items, chosen):
736
+ item.state = "sending"
737
+
738
+ def progress(sent, total, item=item):
739
+ item.sent, item.bytes = sent, total
740
+ transfer.changed()
741
+ try:
742
+ got = export(request, picked, folder, progress=progress, name=phone.name)
743
+ except PhoneIOError as error:
744
+ item.state, item.error = "failed", str(error)
745
+ if item is transfer.items[0] and error.code in NOT_SENT:
746
+ raise
747
+ continue
748
+ item.path, item.sha256, item.bytes, item.sent = got["path"], got["sha256"], got["bytes"], got["bytes"]
749
+ item.state = "verified"
750
+ transfer.changed()
751
+ else:
752
+ if route == "cable":
753
+ got = phone.camera_roll().pull(folder, count=transfer.count, since=transfer.since,
754
+ convert_heic=transfer.convert_heic)
755
+ elif route == "tunnel":
756
+ got = phone.tunnel_roll().pull(folder, count=transfer.count)
757
+ else:
758
+ exported = phone.cloud().export_recent(folder, count=transfer.count)
759
+ from .photos import digest
760
+ got = [{"path": p, "name": Path(p).name, "bytes": Path(p).stat().st_size, "sha256": digest(p),
761
+ "kind": None} for p in exported["paths"]]
762
+ transfer.items = [Item(name=g["name"], bytes=g["bytes"], sent=g["bytes"], kind=g.get("kind"),
763
+ sha256=g.get("sha256"), path=g["path"], remote=g.get("remote"),
764
+ state=_PULL_STATE[route]) for g in got]
765
+ after = len([p for p in folder.iterdir() if p.is_file()])
766
+ transfer.before, transfer.after = before, after
767
+ if after - before != sum(1 for i in transfer.items if i.path):
768
+ for item in transfer.items:
769
+ item.error = item.error or "The folder's count didn't grow by as many as arrived."
770
+ transfer.changed()
771
+
772
+
773
+ def _files_to_mac(transfer, route, clock, sleep):
774
+ from .photos import digest
775
+ folder = Path(transfer.folder).expanduser()
776
+ phone_files = transfer.phone.phone_files(container=transfer.container)
777
+ listing = {e["name"]: e for e in phone_files.ls(transfer.app)}
778
+ names = transfer.names or []
779
+ if not names:
780
+ raise PhoneIOError("Name the files to copy.", "usage")
781
+ transfer.items = [Item(name=n, bytes=(listing.get(n) or {}).get("bytes") or 0) for n in names]
782
+ transfer.changed()
783
+ for item in transfer.items:
784
+ item.state = "sending"
785
+ transfer.changed()
786
+ try:
787
+ saved = phone_files.get(transfer.app, item.name, folder)
788
+ except PhoneIOError as error:
789
+ item.state, item.error = "failed", str(error)
790
+ continue
791
+ item.path, item.sha256, item.sent = str(saved), digest(saved), saved.stat().st_size
792
+ item.state = "counted" if item.sent == item.bytes else "failed"
793
+ transfer.changed()
794
+
795
+
796
+ # What a pull to this Mac proves: the cable compared the file's size with the phone's listing; the tunnel and iCloud
797
+ # compared nothing, so only the wifi route (photos.export) ever reaches ``verified``.
798
+ _PULL_STATE = {"cable": "counted", "tunnel": "sent", "icloud": "sent"}
799
+
800
+ _ROUTERS = {("to_phone", "photos"): _photos_to_phone, ("to_phone", "files"): _files_to_phone,
801
+ ("to_mac", "photos"): _photos_to_mac, ("to_mac", "files"): _files_to_mac}
802
+
803
+
804
+ class Transfers:
805
+ """The running Mobster's transfers: one at a time per phone, phones in parallel; the newest ``KEEP`` are listed."""
806
+
807
+ def __init__(self, runner=run):
808
+ self._run = runner
809
+ self._lock = threading.Lock()
810
+ self._phone_locks = {}
811
+ self._all = {}
812
+
813
+ def start(self, transfer, wait=False):
814
+ validate(transfer)
815
+ with self._lock:
816
+ self._all[transfer.id] = transfer
817
+ while len(self._all) > KEEP:
818
+ oldest = min(self._all.values(), key=lambda t: t.created_at)
819
+ if oldest.state in ("queued", "running"):
820
+ break
821
+ self._all.pop(oldest.id)
822
+ lock = self._phone_locks.setdefault(transfer.phone.id, threading.Lock())
823
+
824
+ def work():
825
+ with lock:
826
+ self._run(transfer)
827
+ if wait:
828
+ work()
829
+ return transfer
830
+ threading.Thread(target=work, name=f"mobster-transfer-{transfer.id}", daemon=True).start()
831
+ return transfer
832
+
833
+ def get(self, transfer_id):
834
+ with self._lock:
835
+ return self._all.get(transfer_id)
836
+
837
+ def all(self):
838
+ with self._lock:
839
+ return sorted(self._all.values(), key=lambda t: t.created_at, reverse=True)
840
+
841
+
842
+ def transfers_for(runtime):
843
+ """The runtime's Transfers, made on first use."""
844
+ if getattr(runtime, "transfers", None) is None:
845
+ runtime.transfers = Transfers()
846
+ return runtime.transfers
847
+
848
+
849
+ def target(runtime=None, *, device=None, profile=None, records=None):
850
+ """The phone a transfer goes to."""
851
+ if profile is not None:
852
+ from ..accounts.store import AccountNotFound, AmbiguousAccount, resolve, store_for
853
+ try:
854
+ account = resolve(store_for(runtime).all(), profile)
855
+ except (AccountNotFound, AmbiguousAccount) as error:
856
+ raise PhoneIOError(str(error), "usage") from None
857
+ wanted = account.get("device")
858
+ if device is not None and wanted and device != wanted:
859
+ raise PhoneIOError(f"@{account['handle']} is on {account.get('deviceName') or 'another phone'}. Leave "
860
+ "the phone out.", "usage")
861
+ device = wanted or device
862
+ if runtime is not None:
863
+ return phone_from_runtime(runtime, device)
864
+ from . import choose_device
865
+ return phone_from_record(choose_device(device, records))
866
+
867
+
868
+ def summary(transfer):
869
+ """One sentence for the CLI and MCP: what moved, where, by which route, and how it was checked."""
870
+ public = transfer.public()
871
+ items = transfer.items
872
+ verified = sum(1 for i in items if i.state == "verified")
873
+ counted = sum(1 for i in items if i.state == "counted")
874
+ sent = sum(1 for i in items if i.state == "sent")
875
+ words = {"cable": "over the cable", "wifi": "over Wi-Fi", "airdrop": "by AirDrop", "mirroring":
876
+ "through iPhone Mirroring", "icloud": "through iCloud Photos", "tunnel": "over Apple's tunnel"}
877
+ where = (f"to {transfer.phone.name}" if transfer.direction == "to_phone" else f"from {transfer.phone.name}")
878
+ if transfer.state == "failed" and not items:
879
+ return str((transfer.error or {}).get("message") or "Nothing moved.")
880
+ parts = [f"Moved {verified + counted + sent} of {len(items)} {where} {words.get(transfer.route or '', '')}".strip()
881
+ + "."]
882
+ if verified:
883
+ parts.append(f"{verified} checked by SHA-256.")
884
+ if counted:
885
+ parts.append(f"{counted} matched by size." if transfer.direction == "to_mac"
886
+ else f"{counted} counted in the library.")
887
+ if sent:
888
+ parts.append(f"{sent} not checked." if transfer.direction == "to_mac"
889
+ else f"{sent} not checked: Mobster couldn't read the phone.")
890
+ if public["before"] is not None and public["after"] is not None and transfer.direction == "to_phone":
891
+ parts.append(f"Photos and videos on the phone: {public['before']:,} before, {public['after']:,} after.")
892
+ failed = [i for i in items if i.state in ("failed", "unknown")]
893
+ if failed:
894
+ parts.append(f"{len(failed)} didn't: {failed[0].error or failed[0].state}")
895
+ return " ".join(parts)