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,375 @@
1
+ """A project's deep links: where they come from, what `mobster links check` does with them, and what a step can say.
2
+
3
+ Four sources, merged by address (a link in several keeps the first source's name, and takes an expected title from
4
+ whichever has one):
5
+
6
+ 1. ``.mobster/links.yaml``: the links you name, with the title each screen should show.
7
+ 2. The Expo config: ``scheme`` and ``ios.associatedDomains`` (app.json, or `npx expo config` for an app.config.*).
8
+ 3. The Xcode project: ``CFBundleURLTypes`` in an Info.plist (each URL scheme) and ``com.apple.developer.associated-
9
+ domains`` in an .entitlements file (each ``applinks:`` domain).
10
+ 4. Links learned: a link a check opened successfully (``learn``, called by the runner) is kept in
11
+ ``.mobster/links-learned.json``. The memory store takes over this file when it lands (CLAIMS.md, deep-link
12
+ registry row).
13
+
14
+ links:
15
+ - name: cart
16
+ url: myapp://cart
17
+ title: Your cart # optional: the text the screen shows when the link works
18
+ # or: cart: myapp://cart # name: address
19
+
20
+ A scheme or a domain alone (``myapp://``, ``https://example.com/``) opens the app and no screen in it; it is listed,
21
+ and ``check`` reports that it opened without judging a title.
22
+
23
+ ``check_for(link, ...)`` is the check a link becomes: launch the app with the link, expect the title. Run through
24
+ `mobster test`'s engine, so the verdict, the screenshot of a failure and the reports are the ones checks have.
25
+ ``expand_steps`` turns a step "open cart" or "open link cart" into "Open the link myapp://cart" for a name in the
26
+ registry, so a step never needs the address.
27
+
28
+ Reading dynamic Expo configuration invokes its CLI; read-only API discovery skips that step. Only a custom scheme
29
+ is learned; web pages and links Mobster never opens (deeplinks.BLOCKED_SCHEMES) are not.
30
+ """
31
+
32
+ import json
33
+ import os
34
+ import plistlib
35
+ import re
36
+ from dataclasses import dataclass, replace
37
+ from pathlib import Path
38
+ from urllib.parse import urlsplit
39
+
40
+ from ..deeplinks import BLOCKED_SCHEMES, LinkError, risk_of
41
+ from ..env_file import writer
42
+ from ..paths import write_text_atomic
43
+
44
+ FILE = "links.yaml"
45
+ LEARNED = "links-learned.json"
46
+ LEARNED_SCHEMA = "mobster.links-learned/1"
47
+ SOURCES = ("links.yaml", "expo", "xcode", "learned") # in the order a duplicate keeps
48
+ MAX_LINKS = 200
49
+ MAX_LEARNED = 50
50
+ SKIP_DIRS = frozenset({"Pods", "build", "DerivedData", "node_modules", ".git", "Carthage", ".mobster", "vendor"})
51
+ SCAN_DEPTH = 5
52
+ NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9 ._-]{0,59}")
53
+ OPEN_STEP = re.compile(r"^\s*open\s+(?:the\s+)?(?:(?:deep\s*)?link\s+)?[\"']?(.+?)[\"']?\s*\.?\s*$", re.I)
54
+
55
+
56
+ class LinksError(Exception):
57
+ """A links file or an argument can't be used: one sentence that says what to do."""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ProjectLink:
62
+ name: str
63
+ url: str
64
+ source: str
65
+ title: str = "" # the text the landing screen shows; "" when nobody said
66
+
67
+ @property
68
+ def screen(self):
69
+ """Whether the address names a screen. A scheme or a domain alone opens the app and nothing in it."""
70
+ parts = urlsplit(self.url)
71
+ return bool(parts.path.strip("/") or parts.query or (parts.netloc and parts.scheme not in ("http", "https")))
72
+
73
+ def public(self):
74
+ return {"name": self.name, "url": self.url, "source": self.source, "title": self.title or None,
75
+ "screen": self.screen}
76
+
77
+
78
+ # -- the sources ------------------------------------------------------------------------------------------------
79
+
80
+ def _file_links(root):
81
+ path = Path(root) / ".mobster" / FILE
82
+ if not path.is_file():
83
+ return []
84
+ import yaml
85
+ try:
86
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
87
+ except (OSError, UnicodeDecodeError):
88
+ raise LinksError(f".mobster/{FILE} can't be read. Check that it is a text file.") from None
89
+ except yaml.YAMLError:
90
+ raise LinksError(f".mobster/{FILE} isn't valid YAML. Write `links:` as a "
91
+ "list of name, url and title.") from None
92
+ items = data.get("links") if isinstance(data, dict) and set(data) == {"links"} else data
93
+ if isinstance(items, dict):
94
+ items = [{"name": key, **(value if isinstance(value, dict) else {"url": value})} for key,
95
+ value in items.items()]
96
+ if not isinstance(items, list) or len(items) > MAX_LINKS:
97
+ raise LinksError(f".mobster/{FILE} holds `links:` as a list of at most {MAX_LINKS}.")
98
+ out = []
99
+ for index, item in enumerate(items, 1):
100
+ if not isinstance(item, dict) or set(item) - {"name", "url", "title"}:
101
+ raise LinksError(f".mobster/{FILE}, link {index}: use name, url and title.")
102
+ name, url, title = str(item.get("name") or "").strip(), item.get("url"), item.get("title") or ""
103
+ if not NAME.fullmatch(name):
104
+ raise LinksError(f".mobster/{FILE}, link {index}: `name` is 1 to 60 letters, digits, spaces, dots, "
105
+ "dashes or underscores.")
106
+ if not isinstance(url, str) or not isinstance(title, str) or len(title) > 200:
107
+ raise LinksError(f".mobster/{FILE}, link {name}: `url` and `title` are text; a title is at most 200.")
108
+ try:
109
+ risk_of(url)
110
+ except LinkError as error:
111
+ raise LinksError(f".mobster/{FILE}, link {name}: {error}") from None
112
+ out.append(ProjectLink(name, url.strip(), "links.yaml", title.strip()))
113
+ return out
114
+
115
+
116
+ def _expo_links(root, runner):
117
+ from . import expo
118
+ root = Path(root)
119
+ if not (root / "package.json").is_file():
120
+ return []
121
+ config = expo.read_config(root, runner=runner)
122
+ schemes = config.get("scheme")
123
+ out = [ProjectLink(scheme, f"{scheme}://", "expo")
124
+ for scheme in ([schemes] if isinstance(schemes, str) else schemes or ())
125
+ if isinstance(scheme, str) and re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]{0,40}", scheme)]
126
+ ios = config.get("ios") if isinstance(config.get("ios"), dict) else {}
127
+ for domain in ios.get("associatedDomains") or ():
128
+ out += _applink(domain, "expo")
129
+ return out
130
+
131
+
132
+ def _applink(entry, source):
133
+ kind, _, host = str(entry).partition(":")
134
+ host = host.split("?")[0].strip()
135
+ if kind != "applinks" or not re.fullmatch(r"(\*\.)?[A-Za-z0-9.-]{1,253}", host) or host.startswith("*"):
136
+ return []
137
+ return [ProjectLink(host, f"https://{host}/", source)]
138
+
139
+
140
+ def _walk(root):
141
+ root = Path(root)
142
+ for folder, dirs, files in os.walk(root):
143
+ depth = len(Path(folder).relative_to(root).parts)
144
+ dirs[:] = (
145
+ [d for d in sorted(dirs) if d not in SKIP_DIRS and not d.endswith(".xcarchive")]
146
+ if depth < SCAN_DEPTH else [])
147
+ for name in sorted(files):
148
+ if name.endswith("Info.plist") or name.endswith(".entitlements"):
149
+ yield Path(folder) / name
150
+
151
+
152
+ def _xcode_links(root):
153
+ out = []
154
+ for path in _walk(root):
155
+ try:
156
+ data = plistlib.loads(path.read_bytes())
157
+ except (OSError, ValueError, plistlib.InvalidFileException):
158
+ continue
159
+ if not isinstance(data, dict):
160
+ continue
161
+ for kind in data.get("CFBundleURLTypes") or ():
162
+ for scheme in (kind.get("CFBundleURLSchemes") if isinstance(kind, dict) else None) or ():
163
+ if isinstance(scheme, str) and re.fullmatch(r"[A-Za-z][A-Za-z0-9+.-]{0,40}", scheme) \
164
+ and scheme.casefold() not in BLOCKED_SCHEMES:
165
+ out.append(ProjectLink(scheme, f"{scheme}://", "xcode"))
166
+ for domain in data.get("com.apple.developer.associated-domains") or ():
167
+ out += _applink(domain, "xcode")
168
+ return out
169
+
170
+
171
+ def _learned(root):
172
+ path = Path(root) / ".mobster" / LEARNED
173
+ try:
174
+ data = json.loads(path.read_text(encoding="utf-8"))
175
+ except (OSError, ValueError):
176
+ return []
177
+ items = data.get("links") if isinstance(data, dict) and data.get("schema") == LEARNED_SCHEMA else None
178
+ if not isinstance(items, list):
179
+ return []
180
+ return [ProjectLink(str(item["name"])[:60], item["url"], "learned") for item in items[:MAX_LEARNED]
181
+ if isinstance(item, dict) and isinstance(item.get("url"), str) and isinstance(item.get("name"), str)]
182
+
183
+
184
+ def discover(root, *, runner=None, deep=True):
185
+ """The project's links, deduplicated by address. ``deep`` False skips `npx expo config` (an app.config.* needs Node,
186
+ which takes seconds): app.json is still read. LinksError when .mobster/links.yaml is wrong."""
187
+ root = Path(root)
188
+ found = _file_links(root)
189
+ if deep or not any((root / name).is_file() for name in ("app.config.js", "app.config.ts")):
190
+ found += _expo_links(root, runner)
191
+ found += _xcode_links(root) + _learned(root)
192
+ merged = {}
193
+ for link in found:
194
+ key = link.url
195
+ if key not in merged:
196
+ merged[key] = link
197
+ elif link.title and not merged[key].title:
198
+ merged[key] = replace(merged[key], title=link.title)
199
+ return list(merged.values())[:MAX_LINKS]
200
+
201
+
202
+ def find(links, wanted):
203
+ """The link a name (case ignored) or an exact address names. LinksError when none or two match."""
204
+ address = str(wanted or "").strip()
205
+ text = " ".join(address.split()).casefold()
206
+ hits = [link for link in links if link.name.casefold() == text] or [link for link in links if link.url == address]
207
+ if not hits:
208
+ names = ", ".join(link.name for link in links[:8])
209
+ raise LinksError(f"No link is called {str(wanted)[:60]}." + (f" You have: {names}." if names else
210
+ " `mobster links list` shows what Mobster found."))
211
+ if len(hits) > 1:
212
+ raise LinksError(f"Several links are called {str(wanted)[:60]}. Use the address.")
213
+ return hits[0]
214
+
215
+
216
+ # -- saving -----------------------------------------------------------------------------------------------------
217
+
218
+ def _write(root, items):
219
+ import yaml
220
+ folder = Path(root) / ".mobster"
221
+ folder.mkdir(exist_ok=True)
222
+ rows = [{key: value for key, value in (("name", link.name), ("url", link.url), ("title", link.title)) if value}
223
+ for link in items]
224
+ write_text_atomic(folder / FILE, yaml.safe_dump({"links": rows}, sort_keys=False, allow_unicode=True))
225
+
226
+
227
+ def save(root, name, url, title=""):
228
+ """Add or replace the link called ``name`` in .mobster/links.yaml.
229
+ LinksError for a name or address that can't be kept."""
230
+ name = " ".join(str(name or "").split())
231
+ if not NAME.fullmatch(name):
232
+ raise LinksError("A link's name is 1 to 60 letters, digits, spaces, dots, dashes or underscores.")
233
+ if not isinstance(url, str):
234
+ raise LinksError("A link needs an address, such as myapp://cart.")
235
+ try:
236
+ risk_of(url)
237
+ except LinkError as error:
238
+ raise LinksError(str(error)) from None
239
+ if not isinstance(title, str) or len(title) > 200:
240
+ raise LinksError("A link's title is text of at most 200 characters.")
241
+ link = ProjectLink(name, url.strip(), "links.yaml", title.strip())
242
+ with writer(Path(root) / ".mobster" / ".links"):
243
+ kept = [item for item in _file_links(root) if item.name.casefold() != name.casefold()]
244
+ if len(kept) >= MAX_LINKS:
245
+ raise LinksError(f"A project keeps at most {MAX_LINKS} links in .mobster/{FILE}.")
246
+ _write(root, kept + [link])
247
+ return link
248
+
249
+
250
+ def remove(root, name):
251
+ """Take the link called ``name`` out of .mobster/links.yaml. Returns whether it was there."""
252
+ text = " ".join(str(name or "").split()).casefold()
253
+ with writer(Path(root) / ".mobster" / ".links"):
254
+ items = _file_links(root)
255
+ kept = [item for item in items if item.name.casefold() != text]
256
+ if len(kept) == len(items):
257
+ return False
258
+ _write(root, kept)
259
+ return True
260
+
261
+
262
+ # -- learning ---------------------------------------------------------------------------------------------------
263
+
264
+ def learn(root, url, name=None):
265
+ """Keep ``url`` as learned after a run opened it. Never raises; does nothing without a .mobster folder or for a
266
+ web page or a link Mobster never opens. True when the link is new."""
267
+ try:
268
+ scheme = urlsplit(url).scheme.casefold()
269
+ if not scheme or scheme in BLOCKED_SCHEMES or scheme in ("http", "https") or risk_of(url) != "opens":
270
+ return False
271
+ folder = Path(root) / ".mobster"
272
+ if not folder.is_dir():
273
+ return False
274
+ with writer(Path(root) / ".mobster" / ".links"):
275
+ known = [{"name": link.name, "url": link.url} for link in _learned(root)]
276
+ if any(item["url"] == url for item in known):
277
+ return False
278
+ parts = urlsplit(url)
279
+ label = name or (parts.netloc + parts.path).strip("/").replace("/", " ") or scheme
280
+ known = ([{"name": re.sub(r"[^A-Za-z0-9 ._-]", "", label)[:60].strip() or scheme, "url": url}]
281
+ + known)[:MAX_LEARNED]
282
+ write_text_atomic(folder / LEARNED, json.dumps({"schema": LEARNED_SCHEMA, "links": known}, indent=1) + "\n")
283
+ return True
284
+ except Exception: # noqa: BLE001 -- what a run learned is a convenience, never a reason to fail it
285
+ return False
286
+
287
+
288
+ # -- steps and checks -------------------------------------------------------------------------------------------
289
+
290
+ def expand_steps(steps, links):
291
+ """``steps`` with each "open <link name>" or "open link <name>" naming a project link turned into "Open the link
292
+ <address>". Other steps, and "open Settings", are left as written."""
293
+ if not links:
294
+ return tuple(steps)
295
+ out = []
296
+ for step in steps:
297
+ match = OPEN_STEP.match(step)
298
+ hit = None
299
+ if match:
300
+ text = " ".join(match.group(1).split()).casefold()
301
+ if any(link.name.casefold() == text for link in links):
302
+ hit = find(links, match.group(1))
303
+ out.append(f"Open the link {hit.url}" if hit else step)
304
+ return tuple(out)
305
+
306
+
307
+ def checkable(found):
308
+ """(the links ``check`` opens, the ones it skips): a link that acts as it opens (a call, a shortcut) is skipped."""
309
+ def opens(link):
310
+ try:
311
+ return risk_of(link.url) == "opens"
312
+ except LinkError:
313
+ return False
314
+ chosen = [link for link in found if opens(link)]
315
+ return chosen, [link for link in found if link not in chosen]
316
+
317
+
318
+ def check_data(link, *, bundle=None, app_path=None):
319
+ """The check file's content for ``link``: launch with the address, expect the title."""
320
+ app = {"path": app_path} if app_path else {"bundle": bundle}
321
+ data = {"name": f"Link: {link.name}", "app": app, "tags": ["links"], "launch": {"url": link.url}}
322
+ if link.title:
323
+ data["expect"] = [{"text": link.title}]
324
+ return data
325
+
326
+
327
+ def checks_for(links, *, bundle=None, app_path=None):
328
+ """[(link, Check)] for the links. A link that names no screen is skipped by the caller, not here."""
329
+ from ..verify.checks import check_from_dict
330
+ return [(link, check_from_dict(check_data(link, bundle=bundle, app_path=app_path))) for link in links]
331
+
332
+
333
+ def bundle_id(root, *, runner=None):
334
+ """The app's bundle ID: the Expo config's ``ios.bundleIdentifier``, else the first literal PRODUCT_BUNDLE_IDENTIFIER
335
+ of an Xcode project (a test target's is skipped). None when neither names one."""
336
+ from . import expo
337
+ root = Path(root)
338
+ if (root / "package.json").is_file():
339
+ ios = expo.read_config(root, runner=runner).get("ios")
340
+ if isinstance(ios, dict) and isinstance(ios.get("bundleIdentifier"), str):
341
+ return ios["bundleIdentifier"]
342
+ for folder, dirs, files in os.walk(root):
343
+ dirs[:] = [d for d in sorted(dirs) if d not in SKIP_DIRS
344
+ and len(Path(folder).relative_to(root).parts) < SCAN_DEPTH]
345
+ if folder.endswith(".xcodeproj") and "project.pbxproj" in files:
346
+ try:
347
+ text = (Path(folder) / "project.pbxproj").read_text(encoding="utf-8", errors="replace")
348
+ except OSError:
349
+ continue
350
+ for found in re.findall(r"PRODUCT_BUNDLE_IDENTIFIER = ([A-Za-z0-9.-]+);", text):
351
+ if not re.search(r"tests?$", found, re.I):
352
+ return found
353
+ return None
354
+
355
+
356
+ def plan_check(root, selected, options, *, bundle=None, app_path=None, named_device=None):
357
+ """A testkit Planned for ``selected`` links: one check each, run by `mobster test`'s engine. LinksError when the
358
+ app isn't known."""
359
+ from ..testkit.api import Planned
360
+ from ..testkit.discover import Entry
361
+ from ..testkit.targets import resolve
362
+ root = Path(root)
363
+ bundle = bundle or (None if app_path else bundle_id(root))
364
+ if not bundle and not app_path:
365
+ raise LinksError("Mobster can't tell which app the links open. Pass --bundle com.example.app, or --app PATH "
366
+ "to a built .app.")
367
+ entries = []
368
+ for link, check in checks_for(selected, bundle=bundle, app_path=app_path):
369
+ file = f".mobster/{FILE}#{link.name}"
370
+ entry = Entry(path=root / ".mobster" / FILE, file=file, check=check,
371
+ classname="links." + re.sub(r"[^A-Za-z0-9_]+", "_", link.name),
372
+ keys=(link.name.casefold(), link.url.casefold()))
373
+ entries.append(entry)
374
+ targets = resolve(sims=options.sims, devices=options.devices, root=root, named_device=named_device)
375
+ return Planned(root=root, entries=entries, targets=targets, options=options)
@@ -0,0 +1,123 @@
1
+ """`mobster links`: the deep links a project has, and whether each one still lands where it should (links.py).
2
+
3
+ mobster links list [--json] every link, where it came from, the title its screen should show
4
+ mobster links check [NAME...] [--sim TYPE] open each on a simulator and say what screen it landed on
5
+
6
+ ``check`` is `mobster test` for links: each link becomes a check that launches the app with the address and expects
7
+ its title, so the verdict, the failure's screenshot and the reports (.mobster/test-results/) are the ones checks have.
8
+ A link with no title is opened and its landing screen recorded; it reports "needs review" until you add a title to
9
+ .mobster/links.yaml. A link that acts as it opens (a call, a shortcut) is never opened by `check`.
10
+
11
+ Exit codes: 0 every link landed where it should, 1 one didn't, 3 couldn't (usage too).
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from . import DevError, main, say, subcommands
19
+
20
+
21
+ def add_arguments(parser, helpers):
22
+ add = subcommands(parser, "links_command")
23
+ listed = add("list", help="list the project's deep links and where each came from",
24
+ description="List the project's deep links: the ones in .mobster/links.yaml, the Expo config's "
25
+ "scheme and associated domains, the Xcode project's URL types and associated domains, "
26
+ "and links a check opened successfully. Dynamic Expo config runs `npx expo config`.")
27
+ listed.add_argument("--json", action="store_true", help="print them as JSON")
28
+ check = add("check", help="open each link on a simulator and record the screen it lands on",
29
+ description="Open each link on a simulator, in the app, and say what screen it landed on: passed "
30
+ "when the screen shows the link's title, failed with a screenshot when it doesn't. "
31
+ "Name links to check some; with none, every link is checked.",
32
+ epilog="exit codes: 0 every link landed where it should, 1 one didn't, 3 couldn't\n\n"
33
+ "examples:\n mobster links check\n mobster links check cart profile --sim \"iPhone 17 Pro\"")
34
+ check.add_argument("names", nargs="*", metavar="NAME", help="links to check, by name or address (default: all)")
35
+ check.add_argument("--sim", action="append", metavar="TYPE", help="a simulator, such as "
36
+ "\"iPhone 17 Pro\"; repeatable")
37
+ check.add_argument("--device", action="append", metavar="NAME", help="an iPhone from `mobster devices`; repeatable")
38
+ check.add_argument("--bundle", metavar="ID", help="the app's bundle ID (default: read from the project)")
39
+ check.add_argument("--app", metavar="PATH", help="a built .app to install (default: the app "
40
+ "already on the simulator)")
41
+ check.add_argument("--json", action="store_true", help="print results.json's document")
42
+ for sub in (listed, check):
43
+ sub.add_argument("--project", metavar="DIR", help="the project folder (default: the one you are in)")
44
+
45
+
46
+ def run(args, *, cwd=None):
47
+ return main("links", {"list": _list, "check": _check}, args, cwd=Path(cwd or Path.cwd()))
48
+
49
+
50
+ def _root(args, cwd):
51
+ from ..verify.checks import project_root
52
+ return project_root(Path(args.project).expanduser() if args.project else cwd)
53
+
54
+
55
+ def _links(root):
56
+ from . import links
57
+ try:
58
+ return links.discover(root)
59
+ except links.LinksError as error:
60
+ raise DevError(str(error)) from None
61
+
62
+
63
+ def _list(args, cwd):
64
+ found = _links(_root(args, cwd))
65
+ if args.json:
66
+ print(json.dumps({"links": [link.public() for link in found]}, ensure_ascii=False), flush=True)
67
+ return 0
68
+ if not found:
69
+ say("No deep links found. Add some to .mobster/links.yaml:\n\n links:\n - name: "
70
+ "cart\n url: myapp://cart\n"
71
+ " title: Your cart")
72
+ return 0
73
+ width = max(len(link.name) for link in found)
74
+ wide = max(len(link.url) for link in found)
75
+ for link in found:
76
+ say(f"{link.name:<{width}} {link.url:<{wide}} {link.source:<10} "
77
+ + (f"\"{link.title}\"" if link.title else ""))
78
+ return 0
79
+
80
+
81
+ def _check(args, cwd):
82
+ from ..style import color_enabled
83
+ from ..testkit.api import Options, UsageError, execute
84
+ from ..testkit.cli import summary_text
85
+ from . import links
86
+ root = _root(args, cwd)
87
+ found = _links(root)
88
+ try:
89
+ chosen = [links.find(found, name) for name in args.names] if args.names else found
90
+ except links.LinksError as error:
91
+ raise DevError(str(error)) from None
92
+ chosen, skipped = links.checkable(chosen)
93
+ for link in skipped:
94
+ say(f"Skipped {link.name}: it acts as it opens, and Mobster doesn't open that one to check it.", sys.stderr)
95
+ if not chosen:
96
+ raise DevError("There are no links to check.", "`mobster links list` shows what Mobster found.")
97
+ options = Options(sims=tuple(args.sim or ()), devices=tuple(args.device or ()), retries=0,
98
+ command="mobster links check")
99
+ try:
100
+ planned = links.plan_check(root, chosen, options, bundle=args.bundle,
101
+ app_path=str(Path(args.app).expanduser().resolve()) if args.app else None)
102
+ doc, paths = execute(planned, progress=lambda line: say(line, sys.stderr))
103
+ except (links.LinksError, UsageError) as error:
104
+ raise DevError(str(error)) from None
105
+ except Exception as error: # TargetError and the engine's own errors say a sentence
106
+ if type(error).__name__ in ("TargetError", "DiscoveryError"):
107
+ raise DevError(str(error)) from None
108
+ raise
109
+ if args.json:
110
+ print(json.dumps(doc, ensure_ascii=False, allow_nan=False), flush=True)
111
+ else:
112
+ say(landings(doc) + "\n" + summary_text(doc, paths, color=color_enabled(sys.stdout)))
113
+ return int(doc.get("exitCode", 3))
114
+
115
+
116
+ def landings(doc):
117
+ """One line per link and device: the screen it landed on."""
118
+ lines = []
119
+ for check in doc.get("checks") or ():
120
+ for result in check.get("results") or ():
121
+ title = (result.get("screen") or {}).get("title")
122
+ lines.append(f"{check.get('name')}: landed on " + (f"\"{title}\"" if title else "a screen with no title"))
123
+ return "\n".join(lines)
@@ -0,0 +1,83 @@
1
+ """A project's deep links over the local API, under /api/tests/links (api_routes owner "testkit"), for the Mac app's
2
+ project page. Logic is links.py's; these routes only name the project and shape the answer.
3
+
4
+ | Route | Auth | Body | Answer |
5
+ |---|---|---|---|
6
+ | GET /api/tests/links?project=/abs/path | token | | {links: [{name, url, source, title, screen, last}]} |
7
+ | POST /api/tests/links | app | {project, name, url, title?} | 201 {link}: saved in .mobster/links.yaml |
8
+ | DELETE /api/tests/links?project=/abs/path&name=… | app | | {removed}: the name taken out of .mobster/links.yaml |
9
+
10
+ ``source`` is links.yaml, expo, xcode or learned. ``screen`` is false for a scheme or a domain alone, which opens the
11
+ app and no screen in it. ``last`` is the newest check of the link (`mobster links check`, or Check links on the
12
+ project's page): {status, device, at}, or null when it was never checked. 400 bad_project without an absolute path to a
13
+ folder; 400 bad_link for a name or address that can't be saved. Checking links opens each on a simulator: `mobster links
14
+ check`, or POST /api/projects/{id}/runs {links: true} for a project Mobster knows (projects/runs.py). Saving rewrites
15
+ .mobster/links.yaml as the `links:` list, so a comment in the file is lost.
16
+ """
17
+
18
+ from pathlib import Path
19
+
20
+ from ..api_errors import APIError
21
+ from ..api_routes import Response, Route, register
22
+ from ..service_support import needs_app
23
+ from . import links
24
+
25
+
26
+ def _root(request, body):
27
+ value = body.get("project") or (request.query.get("project") or [None])[0]
28
+ path = Path(value) if isinstance(value, str) else None
29
+ if path is None or not path.is_absolute() or not path.is_dir():
30
+ raise APIError("Send project: the absolute path of the project's folder.", 400, "bad_project")
31
+ from ..verify.checks import project_root
32
+ return project_root(path)
33
+
34
+
35
+ def _guard(call):
36
+ try:
37
+ return call()
38
+ except links.LinksError as error:
39
+ raise APIError(str(error), 400, "bad_link") from None
40
+
41
+
42
+ def _last_checks(root):
43
+ """{link name: {status, device, at}}: the newest result of each link's check, from the project's history."""
44
+ from ..testkit.history import report
45
+ prefix = f".mobster/{links.FILE}#"
46
+ newest = {}
47
+ for row in report(root)["checks"]:
48
+ file = row.get("file") or ""
49
+ if not file.startswith(prefix) or row.get("lastStatus") is None:
50
+ continue
51
+ name, at = file[len(prefix):], row.get("lastAt") or 0
52
+ if name not in newest or at > (newest[name]["at"] or 0):
53
+ newest[name] = {"status": row["lastStatus"], "device": row.get("device") or None, "at": row.get("lastAt")}
54
+ return newest
55
+
56
+
57
+ def listed(request):
58
+ root = _root(request, {})
59
+ found = _guard(lambda: links.discover(root, deep=False))
60
+ last = _last_checks(root)
61
+ return Response.json({"links": [{**link.public(), "last": last.get(link.name)} for link in found]})
62
+
63
+
64
+ def saved(request):
65
+ needs_app(request)
66
+ body = request.body if isinstance(request.body, dict) else {}
67
+ root = _root(request, body)
68
+ link = _guard(lambda: links.save(root, body.get("name"), body.get("url"), body.get("title") or ""))
69
+ return Response.json({"link": link.public()}, 201)
70
+
71
+
72
+ def removed(request):
73
+ needs_app(request)
74
+ root = _root(request, {})
75
+ name = (request.query.get("name") or [None])[0]
76
+ return Response.json({"removed": _guard(lambda: links.remove(root, name))})
77
+
78
+
79
+ def register_routes():
80
+ for route in (Route("testkit", "GET", "/api/tests/links", listed, body="none"),
81
+ Route("testkit", "POST", "/api/tests/links", saved),
82
+ Route("testkit", "DELETE", "/api/tests/links", removed, body="none")):
83
+ register(route)