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,132 @@
1
+ """A check's ``stubs:``: fixed answers to named requests, served by the run's proxy (netproxy.py)."""
2
+
3
+ import difflib
4
+ import json
5
+ import mimetypes
6
+ from pathlib import Path
7
+
8
+ from .assertions import CheckError
9
+ from .conditions import _file
10
+
11
+ KEYS = ("match", "respond")
12
+ MATCH_KEYS = ("host", "path_prefix", "method")
13
+ RESPOND_KEYS = ("status", "json", "file", "delay_ms")
14
+ METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
15
+ STUB_LIMIT = 50
16
+ DELAY_LIMIT = 30000 # ms
17
+ BODY_LIMIT = 8 * 1024 * 1024
18
+
19
+
20
+ def _keys(value, allowed, where):
21
+ for key in value:
22
+ if key not in allowed:
23
+ close = difflib.get_close_matches(str(key), allowed, n=1)
24
+ raise CheckError(f"{where}.{key} isn't a setting; " + (f"did you mean {close[0]}? " if close else "")
25
+ + f"Use {', '.join(allowed)}.")
26
+
27
+
28
+ def _host(value, where):
29
+ text = value.strip().lower() if isinstance(value, str) else ""
30
+ bare = text[2:] if text.startswith("*.") else text
31
+ if not bare or any(c in bare for c in "/:*? ") or bare.startswith(".") or bare.endswith("."):
32
+ raise CheckError(f"{where} must be a host name such as api.example.com or *.example.com, without a scheme, "
33
+ "port or path.")
34
+ return text
35
+
36
+
37
+ def parse(value, base_dir=None):
38
+ """The normalized ``stubs`` list of a check file: () when absent. Raises CheckError."""
39
+ if value is None:
40
+ return ()
41
+ if not isinstance(value, list) or not value:
42
+ raise CheckError("stubs must list requests to answer, such as [{match: {host: api.example.com}, "
43
+ "respond: {status: 200, json: {items: []}}}].")
44
+ if len(value) > STUB_LIMIT:
45
+ raise CheckError(f"stubs has more than {STUB_LIMIT} entries. Keep the ones the check needs.")
46
+ out = []
47
+ for index, item in enumerate(value):
48
+ where = f"stubs[{index}]"
49
+ if not isinstance(item, dict) or set(item) != set(KEYS):
50
+ raise CheckError(f"{where} needs match and respond, such as {{match: {{host: api.example.com}}, "
51
+ "respond: {status: 200, json: {}}}.")
52
+ match, respond = item["match"], item["respond"]
53
+ if not isinstance(match, dict) or not isinstance(respond, dict):
54
+ raise CheckError(f"{where}.match and {where}.respond must each be a mapping.")
55
+ _keys(match, MATCH_KEYS, f"{where}.match")
56
+ _keys(respond, RESPOND_KEYS, f"{where}.respond")
57
+ if "host" not in match:
58
+ raise CheckError(f"{where}.match needs a host.")
59
+ prefix = match.get("path_prefix", "/")
60
+ if not isinstance(prefix, str) or not prefix.startswith("/"):
61
+ raise CheckError(f"{where}.match.path_prefix must start with /, such as /v1/items.")
62
+ method = match.get("method")
63
+ if method is not None and (not isinstance(method, str) or method.upper() not in METHODS):
64
+ raise CheckError(f"{where}.match.method must be one of {', '.join(METHODS)}.")
65
+ status = respond.get("status", 200)
66
+ if isinstance(status, bool) or not isinstance(status, int) or not 100 <= status <= 599:
67
+ raise CheckError(f"{where}.respond.status must be a whole number from 100 to 599.")
68
+ delay = respond.get("delay_ms", 0)
69
+ if isinstance(delay, bool) or not isinstance(delay, int) or not 0 <= delay <= DELAY_LIMIT:
70
+ raise CheckError(f"{where}.respond.delay_ms must be a whole number of milliseconds from 0 to "
71
+ f"{DELAY_LIMIT}.")
72
+ if "json" in respond and "file" in respond:
73
+ raise CheckError(f"{where}.respond has both json and file. Keep one.")
74
+ stub = {"host": _host(match["host"], f"{where}.match.host"), "path_prefix": prefix,
75
+ "method": method.upper() if method else None, "status": status, "delay_ms": delay,
76
+ "json": None, "file": None}
77
+ if "json" in respond:
78
+ try:
79
+ json.dumps(respond["json"])
80
+ except (TypeError, ValueError):
81
+ raise CheckError(f"{where}.respond.json must be plain JSON: text, numbers, lists and "
82
+ "objects.") from None
83
+ stub["json"] = respond["json"]
84
+ elif "file" in respond:
85
+ stub["file"] = _file(respond["file"], f"{where}.respond.file", base_dir)
86
+ if Path(stub["file"]).stat().st_size > BODY_LIMIT:
87
+ raise CheckError(f"{where}.respond.file is larger than {BODY_LIMIT // 1024 // 1024} MiB. Trim it.")
88
+ out.append(stub)
89
+ return tuple(out)
90
+
91
+
92
+ def to_dict(stubs, project_root=None):
93
+ """The file shape of ``stubs``, with paths relative to ``project_root`` where they sit inside it."""
94
+ def relative(path):
95
+ try:
96
+ return str(Path(path).relative_to(Path(project_root).resolve()))
97
+ except (ValueError, TypeError):
98
+ return path
99
+ out = []
100
+ for stub in stubs:
101
+ match = {"host": stub["host"], "path_prefix": stub["path_prefix"]}
102
+ if stub["method"]:
103
+ match["method"] = stub["method"]
104
+ respond = {"status": stub["status"]}
105
+ if stub["json"] is not None:
106
+ respond["json"] = json.loads(json.dumps(stub["json"]))
107
+ if stub["file"]:
108
+ respond["file"] = relative(stub["file"])
109
+ if stub["delay_ms"]:
110
+ respond["delay_ms"] = stub["delay_ms"]
111
+ out.append({"match": match, "respond": respond})
112
+ return out
113
+
114
+
115
+ def host_matches(pattern, host):
116
+ host = host.lower()
117
+ return host.endswith(pattern[1:]) if pattern.startswith("*.") else host == pattern
118
+
119
+
120
+ def matches(stub, host, method, path):
121
+ return (host_matches(stub["host"], host) and (not stub["method"] or stub["method"] == method.upper())
122
+ and path.startswith(stub["path_prefix"]))
123
+
124
+
125
+ def answer(stub):
126
+ """(status, content type, body bytes) of a stub's response."""
127
+ if stub["file"]:
128
+ kind = mimetypes.guess_type(stub["file"])[0] or "application/octet-stream"
129
+ return stub["status"], kind, Path(stub["file"]).read_bytes()
130
+ if stub["json"] is not None:
131
+ return stub["status"], "application/json", json.dumps(stub["json"]).encode()
132
+ return stub["status"], "text/plain", b""
@@ -0,0 +1,174 @@
1
+ from pathlib import Path
2
+
3
+ from .. import brand as tokens
4
+
5
+
6
+ def _between(light, dark):
7
+ return tuple(round((a + b) / 2) for a, b in zip(tokens.rgb(light), tokens.rgb(dark)))
8
+
9
+
10
+ # The brand's light green and coral hold white text at 4.5:1; outlines sit halfway between light and dark tones.
11
+ GREEN, RED = tokens.rgb(tokens.LIGHT["green"]), tokens.rgb(tokens.LIGHT["coral"])
12
+ GREEN_LINE = _between(tokens.LIGHT["green"], tokens.DARK["green"])
13
+ RED_LINE = _between(tokens.LIGHT["coral"], tokens.DARK["coral"])
14
+ SHOWN = tokens.rgb(tokens.LIGHT["violet"]) # violet: every element Mobster read
15
+ SHOWN_ALPHA = round(255 * .3)
16
+ TINT_ALPHA = round(255 * .10)
17
+ FONT_CANDIDATES = ("/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Helvetica.ttc",
18
+ "/Library/Fonts/Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
19
+ FRAME_CSS_WIDTH = 280 # CSS px: the verify report's frame; the suite report's is 264
20
+ MAX_PILLS = 6
21
+
22
+
23
+ def _font(size):
24
+ from PIL import ImageFont
25
+ for path in FONT_CANDIDATES:
26
+ try:
27
+ font = ImageFont.truetype(path, size)
28
+ except OSError:
29
+ continue
30
+ try:
31
+ font.set_variation_by_name("Bold") # SF is a variable font; others keep their one weight
32
+ except Exception:
33
+ pass
34
+ return font
35
+ try:
36
+ return ImageFont.load_default(size=size)
37
+ except TypeError:
38
+ return ImageFont.load_default()
39
+
40
+
41
+ def pill_numbers(items):
42
+ """Which assertions get a pill: ``items`` is [(ok, has_node)] in the check's order."""
43
+ ranked = sorted((index for index, (ok, has_node) in enumerate(items) if has_node),
44
+ key=lambda index: (bool(items[index][0]), index))
45
+ return set(ranked[:MAX_PILLS])
46
+
47
+
48
+ def draw_overlay(frame_path, tree, asserted, out_path):
49
+ """The accessibility overlay: ``asserted`` is [(AssertionResult, [node paths])] in the check's order."""
50
+ from PIL import Image, ImageDraw, ImageFilter
51
+ with Image.open(frame_path) as source:
52
+ base = source.convert("RGBA")
53
+ width, height = base.size
54
+ scale = width / (tree.size[0] or 1)
55
+ k = width / FRAME_CSS_WIDTH # image px per CSS px at the report's size
56
+ layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
57
+ draw = ImageDraw.Draw(layer)
58
+
59
+ def box(rect, grow=0):
60
+ x, y, w, h = rect
61
+ left, top = max(0, x * scale - grow), max(0, y * scale - grow)
62
+ right, bottom = min(width - 1, (x + w) * scale + grow), min(height - 1, (y + h) * scale + grow)
63
+ return (left, top, right, bottom) if right > left and bottom > top else None
64
+
65
+ def rounded(area, radius, **style):
66
+ radius = max(0, min(radius, (area[2] - area[0]) / 2, (area[3] - area[1]) / 2))
67
+ draw.rounded_rectangle(area, radius=radius, **style)
68
+
69
+ shown = tree.shown()
70
+ for node in shown:
71
+ area = box(node.rect)
72
+ # Not the containers that fill the screen: their edges would only rule lines across it.
73
+ if area and _area(area) < .2 * width * height:
74
+ rounded(area, 3 * k, outline=SHOWN + (SHOWN_ALPHA,), width=max(1, round(.75 * k)))
75
+ grow, stroke = round(2.5 * k), max(2, round(1.5 * k))
76
+ items = []
77
+ for result, paths in asserted:
78
+ nodes = [node for node in (tree.by_path(path) for path in paths) if node]
79
+ areas = sorted({area for area in (box(node.rect, grow) for node in nodes) if area},
80
+ key=lambda area: (area[1], area[0]))
81
+ # An element inside another the same assertion matched (a switch's own label) is outlined once, outside.
82
+ areas = [area for area in areas if not any(other != area and _contains(other, area) for other in areas)]
83
+ items.append((result, areas))
84
+ # Held first, failed last, so a red outline is never under a green one.
85
+ for result, areas in sorted(items, key=lambda item: item[0].ok, reverse=True):
86
+ line = (GREEN_LINE if result.ok else RED_LINE) + (255,)
87
+ tint = (GREEN_LINE if result.ok else RED_LINE) + (TINT_ALPHA,)
88
+ for area in areas:
89
+ rounded(area, 7 * k, fill=tint, outline=line, width=stroke)
90
+ numbered = sorted(pill_numbers([(result.ok, bool(areas)) for result, areas in items]),
91
+ key=lambda index: (items[index][0].ok, index))
92
+ asserted_areas = [area for _, areas in items for area in areas]
93
+ edges = base.convert("L").filter(ImageFilter.FIND_EDGES)
94
+ font = _font(max(10, round(12 * k)))
95
+ placed = []
96
+ jobs = [(index, 0) for index in numbered] + [(index, n) for index in numbered if not items[index][0].ok
97
+ for n in range(1, min(3, len(items[index][1])))]
98
+ for index, n in jobs:
99
+ if n and len(placed) >= MAX_PILLS + 2:
100
+ break
101
+ result, areas = items[index]
102
+ _pill(draw, font, index + 1, result.ok, areas[n], (width, height), k, placed,
103
+ [area for area in asserted_areas if area != areas[n]], edges)
104
+ Image.alpha_composite(base, layer).convert("RGB").save(out_path, "JPEG", quality=85)
105
+ return Path(out_path)
106
+
107
+
108
+ def _overlap(a, b):
109
+ return max(0, min(a[2], b[2]) - max(a[0], b[0])) * max(0, min(a[3], b[3]) - max(a[1], b[1]))
110
+
111
+
112
+ def _area(a):
113
+ return max(0, a[2] - a[0]) * max(0, a[3] - a[1])
114
+
115
+
116
+ def _contains(outer, inner):
117
+ return outer[0] <= inner[0] and outer[1] <= inner[1] and outer[2] >= inner[2] and outer[3] >= inner[3]
118
+
119
+
120
+ def _pill(draw, font, number, ok, area, size, k, placed, others, edges):
121
+ from PIL import ImageStat
122
+ text = str(number)
123
+ left, top, right, bottom = draw.textbbox((0, 0), text, font=font, anchor="ls")
124
+ pill_h = round(18 * k)
125
+ pad, mark, gap, ring = round(5.5 * k), round(7.5 * k), round(3 * k), max(1, round(k))
126
+ pill_w = pad + mark + gap + (right - left) + pad
127
+ space = round(1.5 * k) # outside, the pill touches the outline, so it reads as the outline's own label
128
+ x0, y0, x1, y1 = area
129
+ middle = (y0 + y1) / 2 - pill_h / 2 if y1 - y0 < 2 * pill_h else y0
130
+ inset = round(8 * k)
131
+ candidates = [(x0, y0 - space - pill_h, 0), (x0 - space - pill_w, middle, 0), (x1 + space, middle, 0),
132
+ (x0, y1 + space, 0), (x1 - pill_w, y0 - space - pill_h, 0), (x1 - pill_w, y1 + space, 0),
133
+ (x0 + inset, y0 - pill_h / 2, 2), (x1 - inset - pill_w, y0 - pill_h / 2, 2),
134
+ (x0 + space, y0 + space, 4), (x1 - space - pill_w, y0 + space, 4)]
135
+ best = None
136
+ for order, (x, y, own_cost) in enumerate(candidates):
137
+ if x < ring or y < ring or x + pill_w > size[0] - ring or y + pill_h > size[1] - ring:
138
+ continue
139
+ rect = (x - ring, y - ring, x + pill_w + ring, y + pill_h + ring)
140
+ if any(_overlap(rect, other) for other in placed):
141
+ continue
142
+ whole = _area(rect)
143
+ busy = ImageStat.Stat(edges.crop(tuple(round(v) for v in rect))).mean[0]
144
+ cost = (busy + 60 * sum(_overlap(rect, other) for other in others) / whole
145
+ + own_cost * _overlap(rect, area) / whole + .3 * order)
146
+ if best is None or cost < best[0]:
147
+ best = (cost, x, y)
148
+ if busy < 3 and not any(_overlap(rect, other) for other in others) and own_cost == 0:
149
+ break
150
+ if best is None: # nowhere free: the top corner inside, clamped to the frame
151
+ x = min(max(ring, x0 + space), size[0] - pill_w - ring)
152
+ y = min(max(ring, y0 + space), size[1] - pill_h - ring)
153
+ for _ in range(12):
154
+ rect = (x - ring, y - ring, x + pill_w + ring, y + pill_h + ring)
155
+ if not any(_overlap(rect, other) for other in placed):
156
+ break
157
+ y = min(y + pill_h + 2 * ring, size[1] - pill_h - ring)
158
+ else:
159
+ _, x, y = best
160
+ placed.append((x - ring, y - ring, x + pill_w + ring, y + pill_h + ring))
161
+ radius = pill_h / 2
162
+ draw.rounded_rectangle((x - ring, y - ring, x + pill_w + ring, y + pill_h + ring), radius=radius + ring,
163
+ fill=(255, 255, 255, 235))
164
+ draw.rounded_rectangle((x, y, x + pill_w, y + pill_h), radius=radius, fill=(GREEN if ok else RED) + (255,))
165
+ white, line = (255, 255, 255, 255), max(2, round(1.5 * k))
166
+ gx, gy = x + pad, y + (pill_h - mark) / 2
167
+ if ok:
168
+ draw.line([(gx + .1 * mark, gy + .55 * mark), (gx + .4 * mark, gy + .82 * mark),
169
+ (gx + .92 * mark, gy + .2 * mark)], fill=white, width=line, joint="curve")
170
+ else:
171
+ draw.line([(gx + .12 * mark, gy + .12 * mark), (gx + .88 * mark, gy + .88 * mark)], fill=white, width=line)
172
+ draw.line([(gx + .88 * mark, gy + .12 * mark), (gx + .12 * mark, gy + .88 * mark)], fill=white, width=line)
173
+ draw.text((gx + mark + gap - left, y + pill_h / 2 - (top + bottom) / 2), text, font=font, fill=white,
174
+ anchor="ls")
@@ -0,0 +1,239 @@
1
+ """What a check measures: launch time, the app's memory, and hangs (docs/testing.mdx, "What a check measures").
2
+
3
+ Everything here is cheap enough to run on every check: one `ps` every 500 ms, the accessibility reads the run already
4
+ makes, and the app's own log. A number Mobster could not measure is None and is named in ``notMeasured``; a budget on
5
+ a number that is None never passes (see ``judge``)."""
6
+
7
+ import math
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import threading
12
+ from dataclasses import dataclass
13
+
14
+ SAMPLE_EVERY = .5
15
+ HANG_MS = 250
16
+ BUDGET_KEYS = ("launch_ms", "memory_mb", "hangs")
17
+ # The app's runloop monitor: "Hang detected: 0.52s (debugger attached, not reporting)".
18
+ HANG_LINE = re.compile(r"Hang detected: (?P<seconds>\d+(?:\.\d+)?)\s*s\b")
19
+ PS = "/bin/ps"
20
+ NOT_MEASURED = {
21
+ "firstFrame": "A simulator logs no line for the app's first frame, so only the first stable screen is timed.",
22
+ "droppedFrames": "A simulator exposes no Core Animation commit times, so dropped frames are not counted.",
23
+ "iphoneMemory": ("On an iPhone, memory needs `pymobiledevice3 developer dvt sysmon`, which Mobster does not "
24
+ "call yet."),
25
+ }
26
+
27
+
28
+ def parse_ps(text):
29
+ """(rss_mb, cpu_percent) from `ps -o rss=,pcpu=` for one process; None when the line is not that."""
30
+ parts = (text or "").split()
31
+ if len(parts) != 2:
32
+ return None
33
+ try:
34
+ rss_kb, cpu = int(parts[0]), float(parts[1])
35
+ except ValueError:
36
+ return None
37
+ if rss_kb <= 0 or cpu < 0 or not math.isfinite(cpu):
38
+ return None
39
+ return rss_kb / 1024, cpu
40
+
41
+
42
+ def ps_sample(pid, *, run=subprocess.run):
43
+ """One reading of a simulator app's process (a simulator's processes are the Mac's own); None once it has gone."""
44
+ try:
45
+ done = run([PS, "-o", "rss=,pcpu=", "-p", str(int(pid))], capture_output=True, text=True, timeout=2,
46
+ stdin=subprocess.DEVNULL)
47
+ except (OSError, subprocess.SubprocessError, ValueError):
48
+ return None
49
+ return parse_ps(done.stdout) if done.returncode == 0 else None
50
+
51
+
52
+ def percentile(values, share):
53
+ """The nearest-rank percentile of ``values`` (0 < share <= 1)."""
54
+ ordered = sorted(values)
55
+ return ordered[max(0, math.ceil(share * len(ordered)) - 1)]
56
+
57
+
58
+ def memory_summary(samples):
59
+ """{peakMb, p95Mb, cpuPeakPct, samples} over (rss_mb, cpu) samples; None when there are none."""
60
+ if not samples:
61
+ return None
62
+ memory = [mb for mb, _ in samples]
63
+ return {"peakMb": round(max(memory), 1), "p95Mb": round(percentile(memory, .95), 1),
64
+ "cpuPeakPct": round(max(cpu for _, cpu in samples), 1), "samples": len(samples)}
65
+
66
+
67
+ class Sampler:
68
+ """Reads the app's process when it starts, every SAMPLE_EVERY seconds on a thread of its own while the steps run,
69
+ and once more when it stops: a launch-only check that settles in a second still has its readings (26 Oct 2026,
70
+ iOS 26.4 simulator: runs of about 1.3 s got 2, 2, 0 and 1 samples with the start and the thread alone)."""
71
+
72
+ def __init__(self, pid_of, *, sample=None, every=SAMPLE_EVERY):
73
+ self._pid_of, self._sample, self._every = pid_of, sample or ps_sample, every
74
+ self.samples = []
75
+ self._stop = threading.Event()
76
+ self._thread = None
77
+
78
+ def start(self):
79
+ """The first reading is taken here, so a run that ends at once still has one."""
80
+ self._take()
81
+ self._thread = threading.Thread(target=self._run, name="mobster-perf-sampler", daemon=True)
82
+ self._thread.start()
83
+
84
+ def _take(self):
85
+ try:
86
+ pid = self._pid_of()
87
+ reading = self._sample(pid) if pid else None
88
+ except Exception: # a sample never costs a run
89
+ reading = None
90
+ if reading is not None:
91
+ self.samples.append(reading)
92
+
93
+ def _run(self):
94
+ while not self._stop.wait(self._every):
95
+ self._take()
96
+
97
+ def stop(self):
98
+ self._stop.set()
99
+ if self._thread is not None:
100
+ self._thread.join(2)
101
+ self._take()
102
+ return memory_summary(list(self.samples))
103
+
104
+
105
+ def hang_summary(texts, *, threshold_ms=HANG_MS):
106
+ """{count, longestMs, thresholdMs} over the app's log lines: the hang reports of ``threshold_ms`` or longer."""
107
+ found = []
108
+ for text in texts:
109
+ match = HANG_LINE.search(text or "")
110
+ if match and float(match.group("seconds")) * 1000 >= threshold_ms:
111
+ found.append(round(float(match.group("seconds")) * 1000))
112
+ return {"count": len(found), "longestMs": max(found, default=0), "thresholdMs": threshold_ms}
113
+
114
+
115
+ def build(*, launch, sampler_summary, hangs, simulator):
116
+ """The result's ``perf``: the four numbers, the Mac's 1-minute load when they were taken, and what isn't
117
+ measured."""
118
+ missing = {"firstFrame": NOT_MEASURED["firstFrame"]} if simulator else {}
119
+ if simulator:
120
+ missing["droppedFrames"] = NOT_MEASURED["droppedFrames"]
121
+ else:
122
+ missing["droppedFrames"] = "Dropped frames are not measured on an iPhone."
123
+ missing["iphoneMemory"] = NOT_MEASURED["iphoneMemory"]
124
+ try:
125
+ load = round(os.getloadavg()[0], 1)
126
+ except (OSError, AttributeError):
127
+ load = None
128
+ return {"launch": launch, "memory": sampler_summary, "hangs": hangs, "loadAverage": load,
129
+ "notMeasured": missing}
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class Breach:
134
+ key: str
135
+ message: str
136
+
137
+
138
+ def judge(budget, perf):
139
+ """(breaches, unmeasured): which of a check's ``budget`` limits the run broke, and which it could not judge.
140
+
141
+ A limit on a number that was not measured is unmeasured, never met: a run that cannot show it stayed inside the
142
+ budget does not pass it."""
143
+ perf = perf or {}
144
+ values = {
145
+ "launch_ms": ((perf.get("launch") or {}).get("interactiveMs"), "Cold launch to the first stable screen",
146
+ "{:,.0f} ms", "launch time"),
147
+ "memory_mb": ((perf.get("memory") or {}).get("peakMb"), "Peak memory", "{:,.0f} MB", "memory"),
148
+ "hangs": (None if perf.get("hangs") is None else perf["hangs"]["count"], "Hangs of "
149
+ f"{HANG_MS} ms or longer", "{:,.0f}", "hangs"),
150
+ }
151
+ breaches, unmeasured = [], []
152
+ for key in BUDGET_KEYS:
153
+ limit = (budget or {}).get(key)
154
+ if limit is None:
155
+ continue
156
+ value, what, form, short = values[key]
157
+ if value is None:
158
+ verb = "were" if short == "hangs" else "was"
159
+ unmeasured.append(Breach(key, f"{short.capitalize()} {verb} not measured, so budget.{key} could not be "
160
+ "checked."))
161
+ elif value > limit:
162
+ breaches.append(Breach(key, f"{what} was {form.format(value)}, over budget.{key} of {form.format(limit)}."))
163
+ return breaches, unmeasured
164
+
165
+
166
+ # What history keeps of a run's perf, and the unit each is shown in.
167
+ METRICS = {"launchMs": "ms", "memoryMb": "MB", "hangs": ""}
168
+ REGRESS_AT, BASELINE_RUNS, BASELINE_MIN = .20, 10, 3
169
+
170
+
171
+ def clean(found):
172
+ """``found`` ({launchMs, memoryMb, hangs}) with only the finite, non-negative numbers; the boundary for history."""
173
+ found = found if isinstance(found, dict) else {}
174
+ return {key: found[key] for key in METRICS
175
+ if isinstance(found.get(key), (int, float)) and not isinstance(found[key], bool)
176
+ and math.isfinite(found[key]) and found[key] >= 0}
177
+
178
+
179
+ def numbers(perf):
180
+ """{launchMs, memoryMb, hangs} of a result's ``perf``: only the ones that were measured."""
181
+ perf = perf if isinstance(perf, dict) else {}
182
+ return clean({"launchMs": (perf.get("launch") or {}).get("interactiveMs"),
183
+ "memoryMb": (perf.get("memory") or {}).get("peakMb"),
184
+ "hangs": (perf.get("hangs") or {}).get("count")})
185
+
186
+
187
+ def compare(metric, latest, before):
188
+ """(arrow, change) of ``latest`` against the median of ``before`` (a list of earlier passes): ↑ when it is 20 %
189
+ more (a hang count: any more), ↓ when 20 % less, else →; None with fewer than BASELINE_MIN earlier values."""
190
+ before = [value for value in before if value is not None]
191
+ if latest is None or len(before) < BASELINE_MIN:
192
+ return None
193
+ ordered = sorted(before)
194
+ middle = len(ordered) // 2
195
+ base = ordered[middle] if len(ordered) % 2 else (ordered[middle - 1] + ordered[middle]) / 2
196
+ if metric == "hangs":
197
+ arrow = "↑" if latest > base else "↓" if latest < base else "→"
198
+ elif base <= 0:
199
+ return None
200
+ else:
201
+ change = (latest - base) / base
202
+ arrow = "↑" if change > REGRESS_AT else "↓" if change < -REGRESS_AT else "→"
203
+ return arrow, {"latest": latest, "baseline": round(base, 1), "runs": len(before)}
204
+
205
+
206
+ def trend(runs):
207
+ """{metric: {arrow, latest, baseline, runs}} for the newest of ``runs`` (history's, oldest first) against the
208
+ median of the last BASELINE_RUNS passes before it. ↑ is a regression: more time, memory or hangs."""
209
+ if not runs:
210
+ return {}
211
+ *earlier, latest = runs
212
+ passes = [run for run in earlier if run["status"] == "passed"][-BASELINE_RUNS:]
213
+ out = {}
214
+ for metric in METRICS:
215
+ found = compare(metric, (latest.get("perf") or {}).get(metric),
216
+ [(run.get("perf") or {}).get(metric) for run in passes])
217
+ if found is not None:
218
+ out[metric] = {"arrow": found[0], **found[1]}
219
+ return out
220
+
221
+
222
+ def line(found, trends=None):
223
+ """One line for a report: "launch 812 ms ↑, memory 214 MB →, hangs 0"; "" when nothing was measured."""
224
+ values, trends = clean(found), trends or {}
225
+ parts = []
226
+ for metric, word in (("launchMs", "launch"), ("memoryMb", "memory"), ("hangs", "hangs")):
227
+ if metric in values:
228
+ value = values[metric]
229
+ unit = METRICS[metric]
230
+ text = f"{word} {value:,.0f}" + (f" {unit}" if unit else "")
231
+ arrow = (trends.get(metric) or {}).get("arrow")
232
+ parts.append(text + (f" {arrow}" if arrow else ""))
233
+ return ", ".join(parts)
234
+
235
+
236
+ def last_run(row):
237
+ """`mobster test --history`'s second line for a check, its last run's numbers and markers, or nothing."""
238
+ measured = line(row.get("perfLatest") or {}, row.get("perf"))
239
+ return f"\n Last run: {measured}." if measured else ""