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,646 @@
1
+ """FrameClock: the phone's MJPEG stream as a clock for "the action took effect"
2
+ and "the screen is still".
3
+
4
+ WDA streams the screen as MJPEG at ~30 fps (``video.WdaVideo``). Reading
5
+ that stream costs almost nothing, while every AX read costs 130-290 ms and
6
+ proving a screen settled from AX alone needs a 0.5 s quiet period. This module
7
+ decodes each frame at reduced size in greyscale (JPEG DCT draft mode, ~0.25 ms
8
+ a frame), compares 16-px tiles, and answers the questions the WDA driver
9
+ asks around an action:
10
+
11
+ * ``mark()`` before dispatch, then ``wait_effect(token)``: the first frame that
12
+ differs from the pre-action frame outside regions that were already busy.
13
+ * ``quiet_between(token, start, end)`` / ``still_for()``: no changed tile
14
+ (outside constantly busy tiles: video, spinners) in a window, or since a time
15
+ (the settle rules in WDA.wait_for_change / observe_ready); ``wait_stable`` is
16
+ the blocking form.
17
+ * ``displacement(a, b)``: vertical/horizontal content shift between two
18
+ frames (scroll odometry), by row/column-profile matching.
19
+
20
+ It never decides anything on its own: the driver still takes an AX read and
21
+ compares structure before it accepts a screen. A stale stream cannot prove a
22
+ screen settled, so the caller falls back to AX polling.
23
+
24
+ A driver uses it whenever one is attached: on since the live validation of
25
+ 23 Sep 2026 (0/32 premature settles vs 0/32 for AX alone, AX read latency
26
+ unchanged with the stream up).
27
+ """
28
+
29
+ import io
30
+ import json
31
+ import logging
32
+ import math
33
+ import os
34
+ import threading
35
+ import time
36
+ from collections import deque
37
+ from dataclasses import dataclass, field
38
+
39
+ Image = ImageChops = ImageStat = None
40
+ _pil_loaded = False
41
+
42
+ log = logging.getLogger("mobster.frameclock")
43
+
44
+ # Decoded frame height. JPEG draft mode scales by 1/2, 1/4 or 1/8 inside the
45
+ # decoder; the factor is chosen so frames land at >= this many rows whatever
46
+ # the MJPEG scaling (medium: 590x1278 -> 1/4 -> 148x319).
47
+ TARGET_ROWS = 300
48
+ TILE = 16 # tile side in decoded pixels (~43 pt on an iPhone 15 Pro)
49
+ PIXEL_THRESHOLD = 16 # grey levels; identical screens re-encode to identical bytes
50
+ TILE_MIN = 2 # tile mean of the 0/255 change map: >= 2 changed pixels of 256
51
+ # The status bar (clock, battery, Live Activities) is not the app's and changes
52
+ # on its own; it is cropped before any comparison. So is the right edge, where
53
+ # the vertical scroll indicator fades out ~1.1 s after every scroll (measured:
54
+ # 6 tiles changing long after the content stopped).
55
+ STATUS_BAR_FRACTION = .05
56
+ SCROLL_INDICATOR_FRACTION = .03
57
+ EFFECT_TILES = 2 # changed tiles (outside busy regions) that count as an effect
58
+ STABLE_SECONDS = .18 # no change this long after the effect = at rest
59
+ STABLE_MIN_FRAMES = 3 # ...seen over at least this many frames
60
+ BASELINE_SECONDS = 1.5 # activity before mark() that defines busy regions
61
+ QUIET_BEFORE_MARK = .2 # the screen must be still this long before dispatch
62
+ ACTIVE_RATIO = .2 # changed in more than this share of baseline frames = masked
63
+ PERIODIC_CHANGES = 2 # changed at least this often in the baseline = ignored for effects
64
+ RECENT_SECONDS = .6 # ...and still changing this recently (a caret blinks every ~0.5 s)
65
+ MAX_MASKED_FRACTION = .25 # more of the screen busy than this: the clock declines
66
+ STALE_SECONDS = .3 # newest frame older than this: unhealthy
67
+ MIN_FPS = 10 # frames in the last second (measured: ~28 fps, p99 gap 127 ms)
68
+ GAP_SECONDS = 1.0 # a longer pause between frames is logged (the settle is blind meanwhile)
69
+ # A pause this long is a stall: WDA could not take its screenshot ("Cannot take a screenshot within 20000 ms",
70
+ # Maps and Spotify on the phone, B4). Counted once per stall in ``stalls``; WDA.next_step reads it.
71
+ STALL_SECONDS = 3.0
72
+ # MJPEG arrival lags the screen by at most ~55 ms (measured against /screenshot
73
+ # captures during scroll momentum, iPhone 15 Pro over USB).
74
+ CAPTURE_LAG = .06
75
+ WATCH_SECONDS = 12 # a token's watch expires after this long
76
+ MAX_WATCHES = 8
77
+ HISTORY = 96 # (time, changed tiles) per frame, ~3 s; no images
78
+ PROFILE_STRIPS = 32 # columns kept for scroll odometry
79
+ MIN_SHIFT_CONFIDENCE = .3 # below this an axis reports no shift
80
+
81
+
82
+ def _pil():
83
+ """Load Pillow on first use, so a start that never reads a frame doesn't import it; without it the clock reports
84
+ itself unavailable."""
85
+ global Image, ImageChops, ImageStat, _pil_loaded
86
+ if not _pil_loaded:
87
+ _pil_loaded = True
88
+ try:
89
+ from PIL import Image, ImageChops, ImageStat
90
+ except ImportError: # pragma: no cover - exercised only in stripped installs
91
+ pass
92
+
93
+
94
+ def available():
95
+ _pil()
96
+ return Image is not None
97
+
98
+
99
+ _CHANGE_LUT = [0 if value <= PIXEL_THRESHOLD else 255 for value in range(256)]
100
+
101
+
102
+ def decode(data):
103
+ """JPEG bytes -> small greyscale image with the status bar cropped off."""
104
+ _pil()
105
+ image = Image.open(io.BytesIO(data))
106
+ if image.format != "JPEG":
107
+ raise ValueError("FrameClock decodes JPEG frames only")
108
+ width, height = image.size
109
+ factor = 1
110
+ while factor < 8 and height // (factor * 2) >= TARGET_ROWS:
111
+ factor *= 2
112
+ image.draft("L", (max(1, width // factor), max(1, height // factor)))
113
+ image = image.convert("L")
114
+ top = int(image.size[1] * STATUS_BAR_FRACTION)
115
+ right = image.size[0] - max(1, round(image.size[0] * SCROLL_INDICATOR_FRACTION))
116
+ return image.crop((0, top, right, image.size[1]))
117
+
118
+
119
+ def tile_grid(size):
120
+ return math.ceil(size[0] / TILE), math.ceil(size[1] / TILE)
121
+
122
+
123
+ def changed_tiles(a, b):
124
+ """Indexes (row-major) of tiles where ``b`` differs from ``a``; () when equal, every tile when the sizes differ."""
125
+ if a is b:
126
+ return ()
127
+ if a.size != b.size:
128
+ columns, rows = tile_grid(b.size)
129
+ return tuple(range(columns * rows))
130
+ _pil()
131
+ change = ImageChops.difference(a, b).point(_CHANGE_LUT)
132
+ if change.getbbox() is None:
133
+ return ()
134
+ grid = change.reduce(TILE).tobytes()
135
+ return tuple(index for index, value in enumerate(grid) if value >= TILE_MIN)
136
+
137
+
138
+ def _profile(image, strips, axis):
139
+ """Mean intensity per row (axis 0) or column (axis 1), in ``strips`` bands."""
140
+ _pil()
141
+ width, height = image.size
142
+ return image.resize((strips, height) if axis == 0 else (width, strips), Image.BOX)
143
+
144
+
145
+ def _shift_cost(a, b, shift, length, axis):
146
+ """Mean absolute difference when ``b`` shows ``a`` moved by ``shift`` (toward 0)."""
147
+ def cut(image, start, end):
148
+ width, height = image.size
149
+ return image.crop((0, start, width, end)) if axis == 0 else image.crop((start, 0, end, height))
150
+ if shift >= 0:
151
+ x, y = cut(a, shift, length), cut(b, 0, length - shift)
152
+ else:
153
+ x, y = cut(a, 0, length + shift), cut(b, -shift, length)
154
+ _pil()
155
+ return sum(ImageStat.Stat(ImageChops.difference(x, y)).mean)
156
+
157
+
158
+ def _axis_shift(a, b, axis, max_fraction=.9):
159
+ """Content shift of ``b`` against ``a`` along one axis, in decoded pixels."""
160
+ _pil()
161
+ strips = PROFILE_STRIPS
162
+ pa, pb = _profile(a, strips, axis), _profile(b, strips, axis)
163
+ length = pa.size[1] if axis == 0 else pa.size[0]
164
+ flat = (1, length) if axis == 0 else (length, 1)
165
+ rows = ImageChops.difference(pa, pb).resize(flat, Image.BOX).tobytes()
166
+ start, end = 0, length
167
+ while start < end and rows[start] <= 1:
168
+ start += 1
169
+ while end > start and rows[end - 1] <= 1:
170
+ end -= 1
171
+ if end - start < 8:
172
+ return 0, 1.0 if end - start == 0 else 0.0
173
+ if axis == 0:
174
+ pa, pb = pa.crop((0, start, strips, end)), pb.crop((0, start, strips, end))
175
+ else:
176
+ pa, pb = pa.crop((start, 0, end, strips)), pb.crop((start, 0, end, strips))
177
+ span = end - start
178
+ limit = int(span * max_fraction)
179
+ overlap = max(8, int(span * .3))
180
+ # Exhaustive at this resolution: a coarse pass aliases on periodic lists
181
+ # (Settings rows repeat every ~16 decoded pixels). ~10 ms, once per swipe.
182
+ costs = {shift: _shift_cost(pa, pb, shift, span, axis)
183
+ for shift in range(-limit, limit + 1) if span - abs(shift) >= overlap}
184
+ if not costs:
185
+ return 0, 0.0
186
+ best = min(costs, key=costs.get)
187
+ ordered = sorted(costs.values())
188
+ typical = ordered[len(ordered) // 2] or 1e-9
189
+ return best, max(0.0, min(1.0, 1 - costs[best] / typical))
190
+
191
+
192
+ def displacement(a, b, axes="xy"):
193
+ """Content shift from frame ``a`` to frame ``b`` as screen fractions."""
194
+ a = getattr(a, "image", a)
195
+ b = getattr(b, "image", b)
196
+ if a is None or b is None or a.size != b.size:
197
+ return None
198
+ if not changed_tiles(a, b):
199
+ return {"dy": 0.0, "dx": 0.0, "confidence": 1.0}
200
+ dy, vertical = _axis_shift(a, b, 0) if "y" in axes else (0, 0.0)
201
+ dx, horizontal = _axis_shift(a, b, 1) if "x" in axes else (0, 0.0)
202
+ # An axis without a clear match reports no shift: a vertical scroll changes
203
+ # every column profile, and the best horizontal "match" is then noise.
204
+ if vertical < MIN_SHIFT_CONFIDENCE:
205
+ dy = 0
206
+ if horizontal < MIN_SHIFT_CONFIDENCE:
207
+ dx = 0
208
+ width, height = a.size
209
+ # The status bar was cropped off; express shifts against the full screen.
210
+ full_height = height / (1 - STATUS_BAR_FRACTION)
211
+ return {"dy": round(dy / full_height, 4), "dx": round(dx / width, 4),
212
+ "confidence": round(max(vertical, horizontal), 3)}
213
+
214
+
215
+ @dataclass
216
+ class Frame:
217
+ seq: int
218
+ t: float # arrival on this Mac (monotonic)
219
+ image: object
220
+ changed: tuple = () # tiles changed against the previous frame
221
+ same_bytes: bool = False # byte-identical to the previous frame
222
+
223
+
224
+ @dataclass
225
+ class Watch:
226
+ """Everything the worker learns about one action, frame by frame."""
227
+ t: float
228
+ ref: Frame
229
+ stable_mask: frozenset
230
+ effect_mask: frozenset
231
+ usable: bool
232
+ reason: str = ""
233
+ effect: Frame | None = None
234
+ effect_tiles: int = 0
235
+ anchor: Frame | None = None
236
+ latest: Frame | None = None
237
+ frames_since_anchor: int = 0
238
+ stable_at: float | None = None # first time the at-rest condition held
239
+ moves: deque = field(default_factory=lambda: deque(maxlen=400)) # (t, moved) after the effect
240
+ # (t, bytes changed) for every frame after mark(). A static screen re-encodes
241
+ # to identical JPEG bytes (measured: 29/29 frames), so this sees sub-tile
242
+ # motion such as iOS 26's glass morph. Too strict to settle on (measured:
243
+ # bytes keep changing ~1-1.2 s after a Settings push, long after AX is
244
+ # stable), it is kept for diagnostics.
245
+ exact: deque = field(default_factory=lambda: deque(maxlen=400))
246
+ closed: bool = False
247
+ release_t: float | None = None # dispatch returned (+ capture lag)
248
+ release_frame: Frame | None = None # first frame at or after release_t
249
+
250
+
251
+ class FrameClock:
252
+ """Consumes JPEG frames from a ``WdaVideo`` relay, or its own MJPEG connection.
253
+
254
+ Thread-safe. Frames are processed on a private worker thread; the relay is
255
+ never blocked (it only notifies a condition). Memory is bounded: the latest
256
+ frame, one anchor, and per-watch reference frames, plus a short history of
257
+ changed-tile sets without images.
258
+ """
259
+
260
+ def __init__(self, video=None, *, wda_url=None, session=None, mjpeg_url=None,
261
+ clock=time.monotonic, log_path=None, start=True):
262
+ if not available():
263
+ raise RuntimeError("FrameClock needs Pillow")
264
+ self.clock = clock
265
+ self.owns_video = video is None
266
+ if video is None:
267
+ if not wda_url:
268
+ raise ValueError("FrameClock needs a WdaVideo relay or a WDA URL")
269
+ from .video import WdaVideo, default_mjpeg_url
270
+ mjpeg_url = mjpeg_url or default_mjpeg_url(wda_url)
271
+ if not mjpeg_url:
272
+ # A WDA port with no stream paired (8080): never another device's; the driver settles by time.
273
+ raise ValueError("No MJPEG stream is paired with this WDA port")
274
+ if isinstance(session, str):
275
+ session = (lambda value: lambda: value)(session)
276
+ video = WdaVideo(wda_url, mjpeg_url=mjpeg_url, session=session, screenshot_fallback=False)
277
+ self.video = video
278
+ self.log_path = log_path
279
+ self.condition = threading.Condition()
280
+ self.closed = False
281
+ self.viewer = None
282
+ self.worker = None
283
+ self.latest = None # Frame
284
+ self._data = None # bytes of the latest frame (identical-bytes shortcut)
285
+ self.anchor = None # global still-tracker anchor (no mask)
286
+ self.still_since = None # arrival of the last frame with a changed tile
287
+ self.exact_since = None # arrival of the last frame whose bytes changed
288
+ self.history = deque(maxlen=HISTORY) # (t, changed tiles)
289
+ self.arrivals = deque(maxlen=128)
290
+ self.decode_ms = deque(maxlen=256)
291
+ self.watches = []
292
+ self.records = deque(maxlen=256)
293
+ self.frames = 0
294
+ self.skipped = 0
295
+ self.errors = 0
296
+ self.last_seq = 0
297
+ self.resets = 0
298
+ self.stalls = 0
299
+ self._stalled_after = None # the seq of the last frame before a counted stall
300
+ self._log_lock = threading.Lock()
301
+ if start:
302
+ self.start()
303
+
304
+ def start(self):
305
+ with self.condition:
306
+ if self.worker is not None or self.closed:
307
+ return
308
+ self.viewer = self.video.subscribe()
309
+ self.worker = threading.Thread(target=self._run, name="mobster-frame-clock", daemon=True)
310
+ self.worker.start()
311
+
312
+ def close(self, timeout=2):
313
+ with self.condition:
314
+ if self.closed:
315
+ return
316
+ self.closed = True
317
+ viewer, self.viewer = self.viewer, None
318
+ self.watches.clear()
319
+ self.condition.notify_all()
320
+ if viewer is not None:
321
+ try:
322
+ self.video.unsubscribe(viewer) # wakes the worker's next_frame
323
+ except Exception:
324
+ pass
325
+ if self.owns_video:
326
+ try:
327
+ self.video.close()
328
+ except Exception:
329
+ pass
330
+ worker = self.worker
331
+ if worker is not None and worker is not threading.current_thread():
332
+ worker.join(timeout)
333
+
334
+ def __enter__(self):
335
+ return self
336
+
337
+ def __exit__(self, *exc):
338
+ self.close()
339
+
340
+ def _run(self):
341
+ sequence = 0
342
+ while True:
343
+ with self.condition:
344
+ if self.closed:
345
+ return
346
+ viewer = self.viewer
347
+ try:
348
+ frame = self.video.next_frame(viewer, sequence, timeout=.5)
349
+ except Exception:
350
+ frame = None
351
+ if frame is None:
352
+ if getattr(self.video, "closed", False):
353
+ return
354
+ self.check_stall()
355
+ continue
356
+ sequence = frame[0]
357
+ source = frame[5] if len(frame) > 5 else "wda_mjpeg"
358
+ arrival = frame[4] if len(frame) > 4 else self.clock()
359
+ if frame[1] != "image/jpeg" or source != "wda_mjpeg":
360
+ continue # Screenshot fallback frames are too slow to be a clock.
361
+ last = self.latest
362
+ if last is not None and arrival - last.t >= GAP_SECONDS:
363
+ self.record({"kind": "gap", "ms": round((arrival - last.t) * 1000)})
364
+ self.check_stall(arrival)
365
+ self.ingest(frame[2], arrival, sequence)
366
+
367
+ def check_stall(self, now=None):
368
+ """Count a stall (no frame for STALL_SECONDS) once; True when this call counted one."""
369
+ with self.condition:
370
+ last = self.latest
371
+ now = self.clock() if now is None else now
372
+ if last is None or now - last.t < STALL_SECONDS or self._stalled_after == last.seq:
373
+ return False
374
+ self._stalled_after = last.seq
375
+ self.stalls += 1
376
+ self.record({"kind": "stall", "ms": round((now - last.t) * 1000)})
377
+ return True
378
+
379
+ def ingest(self, data, arrival=None, seq=None):
380
+ """Process one JPEG frame. Public so tests (and offline replays) can feed frames."""
381
+ arrival = self.clock() if arrival is None else arrival
382
+ started = time.perf_counter()
383
+ with self.condition:
384
+ previous, previous_data = self.latest, self._data
385
+ if seq is not None and self.last_seq and seq > self.last_seq + 1:
386
+ self.skipped += seq - self.last_seq - 1
387
+ same = previous is not None and data == previous_data
388
+ if same:
389
+ image, changed = previous.image, ()
390
+ else:
391
+ try:
392
+ image = decode(data)
393
+ except Exception:
394
+ self.errors += 1
395
+ return None
396
+ changed = changed_tiles(previous.image, image) if previous is not None else ()
397
+ current = Frame(seq if seq is not None else self.frames + 1, arrival, image, changed, same)
398
+ with self.condition:
399
+ if self.closed:
400
+ return None
401
+ if previous is not None and previous.image.size != image.size:
402
+ self._reset_locked()
403
+ self.latest, self._data = current, data
404
+ self.last_seq = current.seq
405
+ self.frames += 1
406
+ self.arrivals.append(arrival)
407
+ self.history.append((arrival, changed))
408
+ # Global stillness: the last frame with a changed tile (no mask) ...
409
+ if self.anchor is None:
410
+ self.anchor, self.still_since = current, arrival
411
+ elif changed or (not same and changed_tiles(self.anchor.image, image)):
412
+ self.anchor, self.still_since = current, arrival
413
+ # ... and, exactly, the last frame whose bytes changed.
414
+ if self.exact_since is None or not same:
415
+ self.exact_since = arrival
416
+ now = self.clock()
417
+ self.watches = [w for w in self.watches if not w.closed and now - w.t <= WATCH_SECONDS]
418
+ for watch in self.watches:
419
+ self._update_watch(watch, current)
420
+ self.decode_ms.append((time.perf_counter() - started) * 1000)
421
+ self.condition.notify_all()
422
+ return current
423
+
424
+ def _reset_locked(self):
425
+ """Geometry changed mid-stream: every reference is void."""
426
+ self.resets += 1
427
+ self.anchor = self.still_since = None
428
+ self.history.clear()
429
+ for watch in self.watches:
430
+ watch.usable, watch.reason, watch.closed = False, "geometry_changed", True
431
+
432
+ def _update_watch(self, watch, frame):
433
+ if not watch.usable or frame.t <= watch.t:
434
+ return
435
+ watch.latest = frame
436
+ watch.exact.append((frame.t, not frame.same_bytes))
437
+ if watch.release_t is not None and watch.release_frame is None and frame.t >= watch.release_t:
438
+ watch.release_frame = frame
439
+ if watch.effect is None:
440
+ tiles = [t for t in changed_tiles(watch.ref.image, frame.image) if t not in watch.effect_mask]
441
+ if len(tiles) >= EFFECT_TILES:
442
+ watch.effect, watch.effect_tiles = frame, len(tiles)
443
+ watch.anchor, watch.frames_since_anchor = frame, 0
444
+ watch.moves.append((frame.t, True))
445
+ return
446
+ moved = any(t not in watch.stable_mask for t in frame.changed) or any(
447
+ t not in watch.stable_mask for t in changed_tiles(watch.anchor.image, frame.image))
448
+ watch.moves.append((frame.t, moved))
449
+ if moved:
450
+ watch.anchor, watch.frames_since_anchor = frame, 0
451
+ else:
452
+ watch.frames_since_anchor += 1
453
+ if (watch.stable_at is None and frame.t - watch.anchor.t >= STABLE_SECONDS
454
+ and watch.frames_since_anchor >= STABLE_MIN_FRAMES):
455
+ watch.stable_at = frame.t
456
+
457
+ def healthy(self):
458
+ """Fresh frames at a usable rate. Everything else answers None when not."""
459
+ with self.condition:
460
+ return self._healthy_locked()
461
+
462
+ def _healthy_locked(self):
463
+ if self.closed or self.latest is None:
464
+ return False
465
+ now = self.clock()
466
+ if now - self.latest.t > STALE_SECONDS:
467
+ return False
468
+ return self._recent_frames_locked(now) >= MIN_FPS
469
+
470
+ def _recent_frames_locked(self, now):
471
+ """Frames that arrived in the last second."""
472
+ return sum(1 for t in self.arrivals if now - t <= 1.0)
473
+
474
+ def stats(self):
475
+ with self.condition:
476
+ timings = sorted(self.decode_ms)
477
+ now = self.clock()
478
+ return {"healthy": self._healthy_locked(), "frames": self.frames, "skipped": self.skipped,
479
+ "errors": self.errors, "resets": self.resets, "stalls": self.stalls,
480
+ "fps": self._recent_frames_locked(now),
481
+ "frame_age_ms": None if self.latest is None else round((now - self.latest.t) * 1000, 1),
482
+ "process_ms_p50": round(timings[len(timings) // 2], 3) if timings else None,
483
+ "process_ms_p99": round(timings[int(len(timings) * .99)], 3) if timings else None,
484
+ "watches": len(self.watches)}
485
+
486
+ def _baseline_locked(self, end):
487
+ """Busy tiles in the window ending at ``end``; only ongoing activity counts (a past scroll masks nothing)."""
488
+ counts, recent, frames = {}, set(), 0
489
+ for t, changed in self.history:
490
+ if end - BASELINE_SECONDS <= t <= end:
491
+ frames += 1
492
+ for tile in changed:
493
+ counts[tile] = counts.get(tile, 0) + 1
494
+ if t >= end - RECENT_SECONDS:
495
+ recent.update(changed)
496
+ active = frozenset(t for t, n in counts.items()
497
+ if frames >= 10 and n > frames * ACTIVE_RATIO and t in recent)
498
+ periodic = frozenset(t for t, n in counts.items() if n >= PERIODIC_CHANGES and t in recent) | active
499
+ return active, periodic
500
+
501
+ def mark(self):
502
+ """A token for "now" taken right before dispatch; usable on a healthy stream still for QUIET_BEFORE_MARK."""
503
+ with self.condition:
504
+ now = self.clock()
505
+ latest = self.latest
506
+ if not self._healthy_locked():
507
+ return Watch(now, latest, frozenset(), frozenset(), False, "unhealthy", closed=True)
508
+ active, periodic = self._baseline_locked(latest.t)
509
+ columns, rows = tile_grid(latest.image.size)
510
+ watch = Watch(now, latest, active, periodic, True)
511
+ if len(periodic) > MAX_MASKED_FRACTION * columns * rows:
512
+ watch.usable, watch.reason, watch.closed = False, "busy_screen", True
513
+ elif any(t >= latest.t - QUIET_BEFORE_MARK and any(tile not in active for tile in changed)
514
+ for t, changed in self.history):
515
+ watch.usable, watch.reason, watch.closed = False, "not_still_before_action", True
516
+ else:
517
+ if len(self.watches) >= MAX_WATCHES:
518
+ self.watches.pop(0).closed = True
519
+ self.watches.append(watch)
520
+ return watch
521
+
522
+ def set_release(self, watch, t):
523
+ """The action's request returned at ``t``."""
524
+ with self.condition:
525
+ if watch is not None and watch.release_t is None:
526
+ watch.release_t = t + CAPTURE_LAG
527
+
528
+ def release(self, watch):
529
+ if watch is None:
530
+ return
531
+ with self.condition:
532
+ watch.closed = True
533
+ if watch in self.watches:
534
+ self.watches.remove(watch)
535
+ self.condition.notify_all()
536
+
537
+ def _watch_usable_locked(self, watch):
538
+ return (watch is not None and watch.usable and not watch.closed
539
+ and self.clock() - watch.t <= WATCH_SECONDS and self._healthy_locked())
540
+
541
+ def _wait(self, watch, predicate, timeout):
542
+ """Wait for ``predicate()``; None when the watch expires, closes or loses its stream."""
543
+ deadline = self.clock() + max(0.0, timeout)
544
+ with self.condition:
545
+ while self._watch_usable_locked(watch):
546
+ value = predicate()
547
+ if value:
548
+ return value
549
+ remaining = deadline - self.clock()
550
+ if remaining <= 0:
551
+ return None
552
+ self.condition.wait(min(remaining, .05))
553
+ return None
554
+
555
+ def wait_effect(self, watch, timeout):
556
+ """The first frame after ``mark()`` with EFFECT_TILES changed tiles outside busy regions, else None."""
557
+ return self._wait(watch, lambda: watch.effect, timeout)
558
+
559
+ def wait_stable(self, watch, timeout, duration=STABLE_SECONDS):
560
+ """When nothing outside busy tiles changed for ``duration`` after the effect; None on timeout."""
561
+ def settled():
562
+ if watch.effect is None or watch.latest is None or watch.anchor is None:
563
+ return None
564
+ if (watch.latest.t - watch.anchor.t >= duration
565
+ and watch.frames_since_anchor >= STABLE_MIN_FRAMES):
566
+ return watch.anchor
567
+ return None
568
+ return self._wait(watch, settled, timeout)
569
+
570
+ def quiet_between(self, watch, start, end, min_frames=STABLE_MIN_FRAMES, exact=False):
571
+ """After the effect, frames arrived in (start, end] (at least ``min_frames``) and none of them changed."""
572
+ with self.condition:
573
+ if not self._watch_usable_locked(watch) or watch.effect is None or watch.effect.t > start:
574
+ return False
575
+ source = watch.exact if exact else watch.moves
576
+ window = [moved for t, moved in source if start < t <= end]
577
+ return len(window) >= min_frames and not any(window)
578
+
579
+ def last_motion(self, watch):
580
+ with self.condition:
581
+ return max((t for t, moved in watch.moves if moved), default=None)
582
+
583
+ def still_for(self, exact=True):
584
+ """Seconds since the stream's bytes last changed (``exact``: nothing on
585
+ screen changed at all, sub-tile motion included), or since a tile last
586
+ changed anywhere (status bar aside, no mask) when ``exact`` is False;
587
+ None when unhealthy. Tile stillness misses iOS 26's glass morph, which
588
+ changes AX (measured), so callers that skip an AX read use the default."""
589
+ with self.condition:
590
+ since = self.exact_since if exact else self.still_since
591
+ if not self._healthy_locked() or since is None:
592
+ return None
593
+ return self.clock() - since
594
+
595
+ def latest_frame(self):
596
+ with self.condition:
597
+ return self.latest
598
+
599
+ def record(self, entry):
600
+ """Keep a settle comparison in memory and append it to the JSONL log."""
601
+ entry = {"at": round(time.time(), 3), **entry}
602
+ with self.condition:
603
+ self.records.append(entry)
604
+ log.info("frameclock %s", entry)
605
+ if not self.log_path:
606
+ return
607
+ try:
608
+ line = json.dumps(entry, sort_keys=True, default=str) + "\n"
609
+ with self._log_lock:
610
+ if os.path.exists(self.log_path) and os.path.getsize(self.log_path) > 5_000_000:
611
+ os.replace(self.log_path, self.log_path + ".1")
612
+ with open(self.log_path, "a", encoding="utf-8") as handle:
613
+ handle.write(line)
614
+ except OSError:
615
+ pass
616
+
617
+
618
+ def attach_frame_clock(driver, video=None, *, wda_url=None, session=None, log_path=None, log_dir=None,
619
+ mjpeg_url=None):
620
+ """Give a WDA driver a FrameClock for its lifetime.
621
+
622
+ ``video`` is a running ``WdaVideo`` relay to subscribe to (the server's);
623
+ without one the clock opens its own MJPEG connection (CLI, evals). The
624
+ driver's ``close()`` releases it. Settle comparisons go to
625
+ ``MOBSTER_FRAME_CLOCK_LOG``, else ``log_path``, else
626
+ ``<log_dir>/frameclock.jsonl``. Returns the clock, or None when off or
627
+ unavailable; failures never prevent the run.
628
+ """
629
+ if not available() or not hasattr(driver, "frame_clock"):
630
+ return None
631
+ log_path = (os.environ.get("MOBSTER_FRAME_CLOCK_LOG") or log_path
632
+ or (os.path.join(str(log_dir), "frameclock.jsonl") if log_dir else None))
633
+ try:
634
+ if video is not None:
635
+ if not all(hasattr(video, name) for name in ("subscribe", "unsubscribe", "next_frame")):
636
+ return None
637
+ clock = FrameClock(video, log_path=log_path)
638
+ else:
639
+ if not wda_url:
640
+ return None
641
+ clock = FrameClock(wda_url=wda_url, session=session, mjpeg_url=mjpeg_url, log_path=log_path)
642
+ except Exception as error:
643
+ log.info("frameclock unavailable: %s", type(error).__name__)
644
+ return None
645
+ driver.frame_clock = clock
646
+ return clock