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,305 @@
1
+ """What the X-ray crawler may tap. It fails closed: only a navigation-shaped control whose words name no act."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+ from .. import harness_api
7
+ from ..packs.core import ACT_LABEL, NEVER
8
+ from ..secret_filter import redact
9
+
10
+ # Words a crawler never taps, on top of the engine's commit words, wherever they sit in a label. Whole words, case
11
+ # folded.
12
+ # Screen text is data: a label is matched against these lists and never adds to them.
13
+ DENY = re.compile(
14
+ r"\b(delete|erase|reset|wipe|remove|sign ?out|log ?out|sign ?off|deactivate|disable account|"
15
+ r"buy|purchase|subscribe|subscription|trial|try (?:it )?free|"
16
+ r"upgrade|go premium|get premium|get pro|unlock|pay|checkout|check out|order|book|donate|tip|"
17
+ r"call|dial|facetime|message|send|post|share|invite|publish|upload|reply|comment|"
18
+ r"allow|don'?t allow|accept|agree|decline|approve|confirm|submit|verify|authenticate|"
19
+ r"sign ?in|log ?in|sign ?up|register|create (?:an )?account|"
20
+ r"block|mute|unfollow|follow|unmatch|cancel subscription|install|download|"
21
+ r"turn (?:on|off)|enable|disable|disconnect|pair|forget|"
22
+ r"emergency|sos|power off|shut ?down|restart|redeem|claim|activate)\b", re.I)
23
+ # Act verbs common on screens as nouns ("Feels like 67°", "Clear"): they count only as a label's first word.
24
+ LEADING_ONLY = ("like", "likes", "liked", "react", "get", "request", "cancel", "clear", "next", "update", "report",
25
+ "done", "continue", "start", "begin", "restore", "apply", "join", "connect", "save", "open in", "go")
26
+ _LEADING_ONLY = re.compile(r"\b(" + "|".join(re.escape(word) for word in LEADING_ONLY) + r")\b", re.I)
27
+ # A label that starts with any of these, or with a word above, reads as a command: never tapped, whatever follows.
28
+ LEADING = re.compile(r"^(?:" + "|".join(re.escape(word) for word in LEADING_ONLY) + r"|add|new|create|edit|compose|"
29
+ r"write|record|scan|play|shuffle|try|rate|leave|end|archive|hide|pin|unpin|mark|empty|free up|"
30
+ r"offload|translate|retry|skip|ok|okay|yes|no|tap|swipe|press|click|accept|send)\b", re.I)
31
+ BUTTON_VERB = re.compile(r"^(edit|add|new|create|open|get|go|try|start|join|sign in|log in|sign up|learn more|"
32
+ r"skip|close|ok|yes|no|retry|refresh|search|filter|sort|select|view|show|hide|play|"
33
+ r"pause|stop|record|scan)\b", re.I)
34
+ SEE_ALL = re.compile(r"^(see all|show all|view all|more|show more|see more|all)\b", re.I)
35
+ # A developer's identifier standing in for a label: where it leads is unknown.
36
+ IDENTIFIER = re.compile(r"^(?=.*[_.]|[a-z0-9]+(?:-[a-z0-9]+){2,}$)[A-Za-z0-9_.\-]+$")
37
+ # Roles never tapped.
38
+ CONTROL_ROLES = frozenset({"Switch", "Toggle", "Slider", "Stepper", "Picker", "PickerWheel", "DatePicker",
39
+ "SegmentedControl", "TextField", "SecureTextField", "SearchField", "TextView", "Key",
40
+ "Keyboard", "Menu", "MenuItem", "Alert", "Sheet", "Image", "Icon", "StatusBar",
41
+ "NavigationBar", "Toolbar", "TabBar", "ProgressIndicator", "ActivityIndicator",
42
+ "PageIndicator", "ScrollBar", "Map", "WebView"})
43
+ TOGGLES = frozenset({"Switch", "Toggle"})
44
+ CHROME_ROLES = frozenset({"Toolbar", "TabBar", "SearchField", "Keyboard"})
45
+ CHEVRON = re.compile(r"^(chevron\.(forward|right)|disclosure|›|>)$", re.I)
46
+ # A row's own info button ("More Info", the circled i): the button opens the details, and the row itself may act
47
+ # (a Wi-Fi network joins, a Bluetooth device connects). Such a row is never tapped.
48
+ INFO = re.compile(r"^(more info|info|info\.circle|i)$", re.I)
49
+ CARD_MIN_H, CARD_MIN_W = .1, .3
50
+ DATA_ROW_MIN_W, DATA_ROW_PARTS = .7, 3
51
+ ICON_WORDS = frozenset({"heart", "star", "bookmark", "plus", "minus", "ellipsis", "more", "menu", "close", "xmark",
52
+ "trash", "camera", "mic", "microphone", "play", "pause", "share", "cart", "bag", "bell",
53
+ "search", "filter", "gift", "favorite", "favourite", "settings", "gear", "info"})
54
+ # A row outside the content area (under the bars) is reached by scrolling, not tapped where it sits.
55
+ CONTENT_TOP, CONTENT_BOTTOM = .12, .88
56
+ TAB_BAR_TOP = .88
57
+ OCR_ROW_LEFT_MAX = .4
58
+ OCR_ROW_MAX_WORDS = 5
59
+ OCR_ALIGN = .03
60
+ OCR_CENTRED = .1
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class Verdict:
65
+ ok: bool
66
+ reason: str # why: "cell", "chevron row", "tab", "link", "see all", "list row" when ok; the refusal otherwise
67
+
68
+
69
+ def image_only(snapshot):
70
+ # Unsure is image-only, where stricter rules apply: it fails closed.
71
+ if getattr(snapshot, "source", "") in ("mirroring", "screen", "ocr"):
72
+ return True
73
+ roles = {element.role for element in snapshot.elements}
74
+ return not roles - {"StaticText", "SearchField", "TextField"}
75
+
76
+
77
+ def plain(text):
78
+ """A label on one line, masked here at the source (secret_filter), so no map, event or report holds a secret."""
79
+ return redact(" ".join(str(text or "").replace("\u00a0", " ").split()))
80
+
81
+
82
+ def _first_word_only(text):
83
+ """``text`` with the LEADING_ONLY words after its first word taken out: what the act lists read."""
84
+ first, _, rest = text.partition(" ")
85
+ return " ".join((first + " " + _LEADING_ONLY.sub(" ", rest)).split())
86
+
87
+
88
+ def act_words(label):
89
+ text = plain(label)
90
+ if not text:
91
+ return ""
92
+ lead = LEADING.match(text)
93
+ if lead:
94
+ return f"never tapped: “{lead.group(0).lower()}”"
95
+ read = _first_word_only(text)
96
+ act = harness_api.commit_act("TAP", read, None, None)
97
+ if act:
98
+ return f"names an act ({act})"
99
+ if ACT_LABEL.match(text) or NEVER.search(read):
100
+ return "names an act"
101
+ # An identifier's words count too: "AUTH_PROX_SIGN_IN_CELL" says "sign in".
102
+ spaced = re.sub(r"[_\-.]+", " ", text)
103
+ spaced_read = _first_word_only(spaced)
104
+ found = (DENY.search(read) or DENY.search(spaced_read) or LEADING.match(spaced)
105
+ or (NEVER.search(spaced_read) if spaced != text else None))
106
+ if found:
107
+ return f"never tapped: “{found.group(0).lower()}”"
108
+ return ""
109
+
110
+
111
+ def _inside(point, rect, pad=0.0):
112
+ x, y = point
113
+ rx, ry, rw, rh = rect
114
+ return rx - pad <= x <= rx + rw + pad and ry - pad <= y <= ry + rh + pad
115
+
116
+
117
+ def _contains(outer, inner):
118
+ return (outer[0] - .005 <= inner[0] and outer[1] - .005 <= inner[1]
119
+ and inner[0] + inner[2] <= outer[0] + outer[2] + .005 and inner[1] + inner[3] <= outer[1] + outer[3] + .005)
120
+
121
+
122
+ def in_tab_bar(element, snapshot):
123
+ if "XCUIElementTypeTabBar" in (element.locator or ""):
124
+ return True
125
+ bars = [e for e in snapshot.elements if e.role == "TabBar"]
126
+ return any(_inside(element.center, bar.rect) for bar in bars)
127
+
128
+
129
+ def holds_toggle(element, snapshot):
130
+ """A cell or row with a switch inside it: tapping it could flip the switch."""
131
+ for other in snapshot.elements:
132
+ if other is element or other.role not in TOGGLES:
133
+ continue
134
+ if _inside(other.center, element.rect) or (element.locator and other.locator.startswith(element.locator + "/")):
135
+ return True
136
+ return False
137
+
138
+
139
+ def has_chevron(element, snapshot):
140
+ """A row with a disclosure chevron: an image named like one inside it (Settings' rows: chevron.forward)."""
141
+ for other in snapshot.elements:
142
+ if other is element or not CHEVRON.match(plain(other.label)):
143
+ continue
144
+ if (element.locator and other.locator.startswith(element.locator + "/")) or _inside(other.center, element.rect):
145
+ return True
146
+ return False
147
+
148
+
149
+ def holds_info(element, snapshot):
150
+ """A row with an info button inside it (Settings' Wi-Fi networks and Bluetooth devices)."""
151
+ for other in snapshot.elements:
152
+ if other is element or other.role not in ("Button", "Image") or not INFO.match(plain(other.label)):
153
+ continue
154
+ if (element.locator and other.locator.startswith(element.locator + "/")) or _inside(other.center, element.rect):
155
+ return True
156
+ return False
157
+
158
+
159
+ CHECKMARK = re.compile(r"^(selected|checkmark|checked|checkmark\.circle(\.fill)?)$", re.I)
160
+
161
+
162
+ def choice_list(snapshot):
163
+ # A tap on a row of a list of options picks one, which changes a setting. A selected tab or segment is not one.
164
+ for element in snapshot.elements:
165
+ if in_tab_bar(element, snapshot) or "SegmentedControl" in (element.locator or ""):
166
+ continue
167
+ if element.selected and element.role == "Cell":
168
+ return True
169
+ if element.role in ("Image", "Button") and CHECKMARK.match(plain(element.label)):
170
+ return True
171
+ return False
172
+
173
+
174
+ def under_chrome(element, snapshot):
175
+ """Whether the tap would land on a toolbar, a search bar or the keyboard drawn over the content."""
176
+ point = element.tap_point
177
+ for other in snapshot.elements:
178
+ if other is element or other.role not in CHROME_ROLES:
179
+ continue
180
+ if _inside(point, other.rect):
181
+ return True
182
+ return False
183
+
184
+
185
+ def decide(element, snapshot):
186
+ """Whether the crawler may tap ``element`` on ``snapshot``, and why."""
187
+ label = plain(element.label)
188
+ words = act_words(label) or act_words(element.value if element.role not in TOGGLES else "")
189
+ if words:
190
+ return Verdict(False, words)
191
+ if element.role in TOGGLES or element.role in CONTROL_ROLES:
192
+ return Verdict(False, "a control, not navigation" if element.role not in TOGGLES else "a switch")
193
+ if element.editable:
194
+ return Verdict(False, "a text field")
195
+ if not element.enabled or "TAP" not in element.actions:
196
+ return Verdict(False, "not tappable")
197
+ if not label:
198
+ return Verdict(False, "no label")
199
+ if holds_toggle(element, snapshot):
200
+ return Verdict(False, "holds a switch")
201
+ if image_only(snapshot):
202
+ return _decide_ocr(element, snapshot, label)
203
+ if IDENTIFIER.match(label):
204
+ return Verdict(False, "no readable label")
205
+ x, y = element.tap_point
206
+ tab = in_tab_bar(element, snapshot)
207
+ if not tab and not CONTENT_TOP <= y <= CONTENT_BOTTOM:
208
+ return Verdict(False, "outside the content area")
209
+ if not tab and under_chrome(element, snapshot):
210
+ return Verdict(False, "under a bar")
211
+ if tab and element.role == "Button":
212
+ return Verdict(True, "tab")
213
+ if choice_list(snapshot) and not has_chevron(element, snapshot) and element.role != "Link":
214
+ return Verdict(False, "an option in a choice list")
215
+ if holds_info(element, snapshot):
216
+ return Verdict(False, "a row with its own info button: the row itself may join or connect")
217
+ if element.role == "Cell":
218
+ return Verdict(True, "cell")
219
+ if element.role == "Link":
220
+ return Verdict(True, "link")
221
+ if element.role == "Button" and has_chevron(element, snapshot):
222
+ return Verdict(True, "chevron row")
223
+ if element.role == "Button" and "XCUIElementTypeCell[" in (element.locator or ""):
224
+ return Verdict(True, "cell")
225
+ if element.role in ("Button", "StaticText") and SEE_ALL.match(label):
226
+ return Verdict(True, "see all")
227
+ if element.role in ("Button", "Other") and card_shaped(element, label):
228
+ return Verdict(True, "card")
229
+ return Verdict(False, "not navigation-shaped")
230
+
231
+
232
+ def card_shaped(element, label):
233
+ """A card (tall) or a full-width row of data whose label isn't one icon's word: Weather's modules and days."""
234
+ x, y, w, h = element.rect
235
+ if label.casefold() in ICON_WORDS or len(re.findall(r"[A-Za-z]{2,}", label)) == 0:
236
+ return False
237
+ if " " not in label and re.search(r"[a-z][A-Z]", label):
238
+ return False # one token with capitals inside it ("KxLxpHjhdScjmXV"): an id, not words
239
+ if h >= CARD_MIN_H and w >= CARD_MIN_W:
240
+ return True
241
+ return w >= DATA_ROW_MIN_W and len([part for part in label.split(",") if part.strip()]) >= DATA_ROW_PARTS
242
+
243
+
244
+ def _decide_ocr(element, snapshot, label):
245
+ """Image-only: a list row or a tab label, from where the line sits and what sits beside it."""
246
+ x, y, w, h = element.rect
247
+ cx, cy = element.center
248
+ words = label.split()
249
+ if any(ch.isdigit() for ch in label) and not re.search(r"[A-Za-z]{3}", label):
250
+ return Verdict(False, "a number")
251
+ if re.fullmatch(r"[A-Z0-9 &'’.,-]{2,}", label) and len(label) > 3:
252
+ return Verdict(False, "a heading")
253
+ if BUTTON_VERB.match(label) and len(words) <= 3:
254
+ return Verdict(False, "reads like a button")
255
+ if label.endswith((".", "!", "?", ":")) or len(words) > OCR_ROW_MAX_WORDS:
256
+ return Verdict(False, "a sentence")
257
+ lines = [e for e in snapshot.elements if e is not element and e.label]
258
+ if cy >= TAB_BAR_TOP:
259
+ row = [e for e in lines if abs(e.center[1] - cy) < .015 and len(plain(e.label).split()) <= 2]
260
+ if len(row) >= 2 and len(words) <= 2:
261
+ return Verdict(True, "tab")
262
+ return Verdict(False, "outside the content area")
263
+ if not CONTENT_TOP <= cy <= CONTENT_BOTTOM:
264
+ return Verdict(False, "outside the content area")
265
+ if abs(cx - .5) < OCR_CENTRED and x > .15:
266
+ return Verdict(False, "centred, reads like a button")
267
+ if SEE_ALL.match(label):
268
+ return Verdict(True, "see all")
269
+ if x > OCR_ROW_LEFT_MAX:
270
+ return Verdict(False, "not a list row")
271
+ aligned = [e for e in lines if abs(e.rect[0] - x) < OCR_ALIGN and e.center[1] != cy
272
+ and abs(e.center[1] - cy) < .12 and len(plain(e.label).split()) <= OCR_ROW_MAX_WORDS]
273
+ if not aligned:
274
+ return Verdict(False, "not a list row")
275
+ return Verdict(True, "list row")
276
+
277
+
278
+ SAFE_DISMISS = re.compile(r"^(not now|cancel|close)$", re.I)
279
+
280
+
281
+ def safe_dismiss(buttons):
282
+ """The one dialog button the crawler may press to get it out of the way, or None: "Not Now", "Cancel" or "Close"."""
283
+ # Anything else (Allow, Don't Allow, OK, Continue) is the user's to answer.
284
+ for name in buttons or ():
285
+ if SAFE_DISMISS.match(plain(name)):
286
+ return name
287
+ return None
288
+
289
+
290
+ # Apps where a tap on a plain row acts in a way words and shapes can't show: App X-ray doesn't start in them.
291
+ ACTING_APPS = {
292
+ "com.apple.mobilephone": ("Phone", "a tap on a row there places a call"),
293
+ "com.apple.facetime": ("FaceTime", "a tap on a row there places a call"),
294
+ "com.apple.shortcuts": ("Shortcuts", "a tap on a tile there runs a shortcut"),
295
+ "com.apple.Home": ("Home", "a tap on a tile there switches an accessory"),
296
+ }
297
+
298
+
299
+ def app_refusal(bundle):
300
+ """One sentence for an app X-ray won't map (what happened, then what to do), or None for any other app."""
301
+ found = ACTING_APPS.get(bundle if isinstance(bundle, str) else "")
302
+ if found is None:
303
+ return None
304
+ name, why = found
305
+ return f"App X-ray doesn't map {name}: {why}. Pick another app."
@@ -0,0 +1,43 @@
1
+ from ..api_errors import APIError
2
+ from ..api_routes import Response, Route
3
+ from ..api_routes import register as add_route
4
+ from . import job, policy
5
+
6
+ HOOK_ORDER = 1 # before memory routines (10): an x-ray run is never a routine
7
+
8
+
9
+ def start(request):
10
+ """POST /api/xray/app: {"appId": bundle, "device"?, "screens"?, "minutes"?, "key"?} -> 201 {"runId"}."""
11
+ # A task like any other: Runtime.create asks the plan gate, the phone's owner and the queue.
12
+ body = request.body if isinstance(request.body, dict) else {}
13
+ if set(body) - {"appId", "device", "screens", "minutes", "key"}:
14
+ raise ValueError("App X-ray takes appId, device, screens, minutes and key")
15
+ bundle = body.get("appId")
16
+ if not isinstance(bundle, str) or not 1 <= len(bundle) <= 160:
17
+ raise ValueError("appId is the app's bundle ID")
18
+ key, device = body.get("key"), body.get("device")
19
+ if key is not None and (not isinstance(key, str) or not 1 <= len(key) <= 100):
20
+ raise ValueError("key is 1 to 100 characters")
21
+ if device is not None and not isinstance(device, str):
22
+ raise ValueError("device must be a device's id, UDID or name")
23
+ refusal = policy.app_refusal(bundle)
24
+ if refusal:
25
+ raise APIError(refusal, 422, "xray_app_refused")
26
+ options = job.clean({name: body[name] for name in ("screens", "minutes") if name in body})
27
+ runtime = request.runtime
28
+ app = job.find_app(runtime, bundle, device)
29
+ if app is None:
30
+ raise ValueError("Choose an app that is installed on the phone")
31
+ origin = request.origin if request.origin in ("app", "tui", "cli") else "api"
32
+ created = runtime.create(app["id"], job.GOAL.format(name=app["name"]), "live", f"xray-{key}" if key else None,
33
+ **({"device": device} if device is not None
34
+ else {}), extras={"xray": options}, origin=origin, queue=True)
35
+ run, _ = created if key else (created, False)
36
+ return Response.json({"runId": run.id}, 201)
37
+
38
+
39
+ def register(api):
40
+ """``api`` is mobile_agent.harness_api."""
41
+ api.register_run_field("xray", lambda value, runtime: job.clean(value), model=False) # it maps with no model
42
+ api.register_pre_run(job.hook, order=HOOK_ORDER)
43
+ add_route(Route("xray", "POST", "/api/xray/app", start))
@@ -0,0 +1,283 @@
1
+ """An X-ray map as one self-contained page (report.html): it makes no requests, and every screen string is escaped."""
2
+
3
+ import base64
4
+ from pathlib import Path
5
+
6
+ from .. import links
7
+ from ..verify.report import BASE_STYLE, CSP, _e, brand, icon
8
+
9
+ XRAY_STYLE = """
10
+ .tag.tone::before { background: var(--violet); }
11
+ .facts-card { margin-top: 32px; }
12
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 20px 16px; }
13
+ .tile { min-width: 0; scroll-margin-top: 24px; }
14
+ .tile:target .thumb-x { box-shadow: 0 0 0 2px var(--violet); }
15
+ .thumb-x { display: block; aspect-ratio: 402 / 874; border-radius: 18px; overflow: hidden; background: var(--sunken);
16
+ box-shadow: var(--shadow-thumb); }
17
+ .thumb-x img, .thumb-x svg { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
18
+ .tile h3 { margin: 10px 0 0; font-size: 14px; line-height: 20px; font-weight: var(--weight-medium);
19
+ overflow-wrap: anywhere; }
20
+ .tile .via { margin: 2px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-3); overflow-wrap: anywhere; }
21
+ .tile .opens { margin: 4px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-2); }
22
+ .tile .pw { color: var(--amber); font-weight: var(--weight-medium); }
23
+ .depth + .depth { margin-top: 32px; }
24
+ .depth h3.level { margin: 0 0 12px; font-size: 13px; line-height: 20px; font-weight: var(--weight-medium);
25
+ color: var(--ink-3); }
26
+ .wf-row { fill: color-mix(in srgb, var(--ink) 6%, transparent); }
27
+ .wf-bar { fill: color-mix(in srgb, var(--ink) 9%, transparent); }
28
+ .wf-text { fill: color-mix(in srgb, var(--ink) 22%, transparent); }
29
+ .wf-control { fill: color-mix(in srgb, var(--violet) 35%, transparent); }
30
+ .paywall { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 20px; padding: 20px; }
31
+ .paywall + .paywall { border-top: 1px solid var(--line); }
32
+ .paywall h3 { margin: 0; font-size: 16px; line-height: 24px; font-weight: var(--weight-title); }
33
+ .paywall .headline { margin: 2px 0 0; color: var(--ink-2); overflow-wrap: anywhere; }
34
+ table.plans { width: 100%; margin-top: 12px; border-collapse: collapse; font-size: 13px; line-height: 20px;
35
+ font-variant-numeric: tabular-nums; }
36
+ table.plans th { text-align: left; font-weight: var(--weight-medium); color: var(--ink-3); padding: 6px 12px 6px 0;
37
+ border-bottom: 1px solid var(--line); }
38
+ table.plans td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
39
+ table.plans tr:last-child td { border-bottom: 0; }
40
+ .signals { margin: 10px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-3); }
41
+ ul.tree, ul.tree ul { list-style: none; margin: 0; padding: 0; }
42
+ ul.tree { padding: 16px 20px; font-size: 13px; line-height: 22px; }
43
+ ul.tree ul { padding-left: 18px; border-left: 1px solid var(--line); margin-left: 6px; }
44
+ ul.tree a { text-decoration: none; }
45
+ ul.tree a:hover { text-decoration: underline; }
46
+ ul.tree .edge { color: var(--ink-3); }
47
+ ul.tree .again { color: var(--ink-3); }
48
+ table.left { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 20px; }
49
+ table.left td, table.left th { text-align: left; padding: 10px 16px; border-bottom: 1px solid var(--line);
50
+ vertical-align: top; overflow-wrap: anywhere; }
51
+ table.left th { font-weight: var(--weight-medium); color: var(--ink-3); }
52
+ table.left tr:last-child td { border-bottom: 0; }
53
+ table.left td.n { font-variant-numeric: tabular-nums; color: var(--ink-2); white-space: nowrap; }
54
+ .why { margin: 0 0 12px; color: var(--ink-2); max-width: 720px; }
55
+ @media (max-width: 640px) {
56
+ .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 12px; }
57
+ .paywall { grid-template-columns: 96px minmax(0, 1fr); padding: 16px; gap: 16px; }
58
+ }
59
+ """
60
+ STYLE = BASE_STYLE + XRAY_STYLE
61
+ APP_NAMES = {"com.apple.Preferences": "Settings", "com.apple.weather": "Weather", "com.apple.mobilesafari": "Safari",
62
+ "com.apple.Maps": "Maps", "com.apple.MobileSMS": "Messages", "com.apple.mobilenotes": "Notes"}
63
+ PERIODS = {"week": "Weekly", "month": "Monthly", "quarter": "Quarterly", "half-year": "Every 6 months",
64
+ "year": "Yearly", "lifetime": "Lifetime", "": "Not stated"}
65
+
66
+
67
+ def duration(seconds):
68
+ seconds = float(seconds or 0)
69
+ if seconds < 60:
70
+ return f"{round(seconds)} s"
71
+ minutes, rest = divmod(round(seconds), 60)
72
+ return f"{minutes} min {rest:02d} s"
73
+
74
+
75
+ def app_name(result):
76
+ bundle = (result.get("app") or {}).get("bundle", "")
77
+ if bundle in APP_NAMES:
78
+ return APP_NAMES[bundle]
79
+ screens = result.get("screens") or []
80
+ return (screens[0].get("title") if screens else "") or bundle or "App"
81
+
82
+
83
+ def _thumb_src(base, screen):
84
+ name = screen.get("thumb")
85
+ if not name:
86
+ return None
87
+ try:
88
+ data = (Path(base) / name).read_bytes()
89
+ except OSError:
90
+ return None
91
+ return "data:image/jpeg;base64," + base64.b64encode(data).decode()
92
+
93
+
94
+ def wireframe(screen):
95
+ """The screen's layout as a drawn skeleton: rows, bars, controls and text as bars of their own width."""
96
+ parts = ['<svg viewBox="0 0 100 217" preserveAspectRatio="none" aria-hidden="true">']
97
+ for box in screen.get("boxes") or ():
98
+ x, y, w, h, kind = box[0] * 100, box[1] * 217, box[2] * 100, box[3] * 217, box[4]
99
+ if kind == "row":
100
+ parts.append(f'<rect class="wf-row" x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{max(h - .6, .6):.1f}" '
101
+ 'rx="1.5"/>')
102
+ elif kind == "bar":
103
+ parts.append(f'<rect class="wf-bar" x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}"/>')
104
+ else:
105
+ line = min(h, 3.2)
106
+ cls = "wf-control" if kind == "control" else "wf-text"
107
+ parts.append(f'<rect class="{cls}" x="{x:.1f}" y="{y + (h - line) / 2:.1f}" width="{max(w, 2):.1f}" '
108
+ f'height="{line:.1f}" rx="{line / 2:.1f}"/>')
109
+ parts.append("</svg>")
110
+ return "".join(parts)
111
+
112
+
113
+ def _thumb(base, screen, alt):
114
+ src = _thumb_src(base, screen)
115
+ inner = (f'<img src="{src}" alt="{_e(alt)}" loading="lazy">' if src else wireframe(screen))
116
+ return f'<span class="thumb-x">{inner}</span>'
117
+
118
+
119
+ def _plans(paywall):
120
+ rows = []
121
+ for plan in paywall.get("plans") or ():
122
+ per_month = (f"{plan.get('currency', '')} {plan['per_month']:.2f}" if plan.get("per_month")
123
+ and plan.get("period") not in ("month", "lifetime") else "")
124
+ rows.append(f"<tr><td>{_e(plan.get('name') or PERIODS.get(plan.get('period', ''), ''))}</td>"
125
+ f"<td>{_e(plan.get('price'))}</td><td>{_e(PERIODS.get(plan.get('period', ''), plan.get('period')))}"
126
+ f"</td><td>{_e(per_month)}</td><td>{_e(plan.get('trial') or paywall.get('trial') or '')}</td></tr>")
127
+ if not rows:
128
+ return '<p class="empty">No prices read on this screen.</p>'
129
+ return ('<table class="plans"><thead><tr><th>Plan</th><th>Price</th><th>Billed</th><th>A month</th>'
130
+ '<th>Trial</th></tr></thead><tbody>' + "".join(rows) + "</tbody></table>")
131
+
132
+
133
+ def _crumbs(screen):
134
+ return " › ".join(step.get("label", "") for step in screen.get("path") or ()) or "First screen"
135
+
136
+
137
+ def render(result, base="."):
138
+ screens = result.get("screens") or []
139
+ edges = result.get("edges") or []
140
+ stats = result.get("stats") or {}
141
+ by_id = {s["id"]: s for s in screens}
142
+ name = app_name(result)
143
+ bundle = (result.get("app") or {}).get("bundle", "")
144
+ out_edges = {}
145
+ for edge in edges:
146
+ out_edges.setdefault(edge["from"], []).append(edge)
147
+ connection = "Pixels (iPhone Mirroring)" if result.get("connection") == "pixels" else "Accessibility tree"
148
+ lede = (f"{stats.get('screens', 0)} screens and {stats.get('edges', 0)} links, {stats.get('max_depth', 0)} taps "
149
+ f"deep, mapped in {duration(stats.get('wall_seconds'))} with no model.")
150
+ parts = [
151
+ "<!doctype html>", '<html lang="en"><head><meta charset="utf-8">',
152
+ f'<meta http-equiv="Content-Security-Policy" content="{CSP}">',
153
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
154
+ '<meta name="color-scheme" content="light dark">',
155
+ f"<title>{_e(name)} X-ray</title>", f"<style>{STYLE}</style></head><body><main>",
156
+ brand("App X-ray", bundle),
157
+ '<header class="head"><div class="chips">',
158
+ f'<span class="tag tone">{_e(connection)}</span><span class="tag">{icon("coin")}$0 in model calls</span>',
159
+ f"</div><h1>{_e(name)}</h1>", f'<p class="lede">{_e(lede)}</p>',
160
+ '<ul class="meta">',
161
+ f'<li>{icon("app")}<span class="mono">{_e(bundle)}</span></li>',
162
+ f'<li>{icon("calendar")}{_e((result.get("created_at") or "")[:16].replace("T", " "))}</li>',
163
+ f'<li>{icon("clock")}Stopped: {_e(result.get("stopped"))}</li></ul></header>',
164
+ ]
165
+ facts = [("Screens", stats.get("screens")), ("Links", stats.get("edges")), ("Deepest", stats.get("max_depth")),
166
+ ("Time", duration(stats.get("wall_seconds"))), ("Screens a minute", stats.get("screens_per_minute")),
167
+ ("Taps", stats.get("taps")), ("Left alone", stats.get("refused")), ("Paywalls", stats.get("paywalls")),
168
+ ("Ends with nothing to open", stats.get("dead_ends")), ("Model cost", "$0")]
169
+ parts.append('<section class="facts-card"><dl class="facts card">' + "".join(
170
+ f"<div><dt>{_e(k)}</dt><dd>{_e(v)}</dd></div>" for k, v in facts) + "</dl></section>")
171
+
172
+ paywalled = [s for s in screens if s.get("paywall")]
173
+ if paywalled:
174
+ parts.append(f'<section><h2>Paywalls<span class="count">{len(paywalled)}</span></h2><div class="card">')
175
+ for screen in paywalled:
176
+ pw = screen["paywall"]
177
+ parts.append('<div class="paywall"><a '
178
+ f'href="#{_e(screen["id"])}">{_thumb(base, screen, screen.get("name") or screen["title"])}</a>'
179
+ f'<div><h3>{_e(screen.get("name") or screen.get("title") or "Untitled")}</h3>'
180
+ f'<p class="headline">{_e(pw.get("headline") or _crumbs(screen))}</p>{_plans(pw)}'
181
+ f'<p class="signals">Read as a paywall from: {_e(", ".join(pw.get("signals") or ()))}. '
182
+ f'{"Shows Restore Purchases. " if pw.get("restore") else ""}Nothing on it was tapped.</p>'
183
+ "</div></div>")
184
+ parts.append("</div></section>")
185
+
186
+ parts.append(f'<section><h2>Screens<span class="count">{len(screens)}</span></h2>')
187
+ for heading, group in _branches(screens, by_id):
188
+ parts.append(f'<div class="depth"><h3 class="level">{_e(heading)} ({len(group)})</h3><div class="grid">')
189
+ for screen in group:
190
+ parts.append(_tile(base, screen, by_id, out_edges))
191
+ parts.append("</div></div>")
192
+ parts.append("</section>")
193
+
194
+ parts.append('<section><h2>Paths</h2><div class="card"><ul class="tree">'
195
+ + _outline(screens, out_edges, by_id) + "</ul></div></section>")
196
+
197
+ refused = {}
198
+ for screen in screens:
199
+ for item in screen.get("refused") or ():
200
+ refused.setdefault(item["reason"], []).append(item["label"])
201
+ if refused:
202
+ parts.append(f'<section><h2>Left alone<span class="count">{stats.get("refused", 0)}</span></h2>'
203
+ '<p class="why">The crawler taps only controls shaped like navigation whose words name no act. '
204
+ 'These it saw and didn’t tap.</p><div class="card"><table class="left"><thead><tr><th>Why</th>'
205
+ '<th>Count</th><th>Examples</th></tr></thead><tbody>')
206
+ for reason, labels in sorted(refused.items(), key=lambda kv: -len(kv[1])):
207
+ examples = ", ".join(f"“{label}”" for label in list(dict.fromkeys(labels))[:6])
208
+ parts.append(f'<tr><td>{_e(reason)}</td><td class="n">{len(labels)}</td><td>{_e(examples)}</td></tr>')
209
+ parts.append("</tbody></table></div></section>")
210
+
211
+ parts.append(f'<footer class="foot"><span><a href="{links.MADE_WITH_XRAY}">Mobster</a> App X-ray, '
212
+ 'experimental.</span>'
213
+ "<span>Screen text is shown as read from the app.</span></footer></main></body></html>")
214
+ return "".join(parts)
215
+
216
+
217
+ def _branches(screens, by_id):
218
+ def top(screen):
219
+ seen = set()
220
+ while screen.get("depth", 0) > 1 and screen.get("parent") in by_id and screen["id"] not in seen:
221
+ seen.add(screen["id"])
222
+ screen = by_id[screen["parent"]]
223
+ return screen
224
+ opening = [s for s in screens if s.get("depth", 0) == 0]
225
+ groups = [("Where it opens", opening)] if opening else []
226
+ sections = {}
227
+ for screen in screens:
228
+ if screen.get("depth", 0) == 0:
229
+ continue
230
+ sections.setdefault(top(screen)["id"], []).append(screen)
231
+ for head_id, group in sections.items():
232
+ head = by_id.get(head_id, group[0])
233
+ groups.append((head.get("name") or head.get("title") or "Untitled", group))
234
+ return groups
235
+
236
+
237
+ def _tile(base, screen, by_id, out_edges):
238
+ leads = out_edges.get(screen["id"], [])
239
+ opens = f"Opens {len(leads)}" if leads else "Nothing further"
240
+ parent = by_id.get(screen.get("parent"))
241
+ via = (f'From <a href="#{_e(parent["id"])}">{_e(parent.get("name") or parent.get("title") or parent["id"])}</a>'
242
+ f' by “{_e(screen.get("via"))}”' if parent else "Where the app opens")
243
+ flag = ' <span class="pw">Paywall</span>' if screen.get("paywall") else ""
244
+ return (f'<article class="tile" id="{_e(screen["id"])}">'
245
+ f'{_thumb(base, screen, screen.get("name") or screen.get("title"))}'
246
+ f'<h3>{_e(screen.get("name") or screen.get("title") or "Untitled")}{flag}</h3><p class="via">{via}</p>'
247
+ f'<p class="opens">{_e(opens)}</p></article>')
248
+
249
+
250
+ def _outline(screens, out_edges, by_id):
251
+ """The crawl as nested lists: each screen under the one it was found from, each link with the control tapped."""
252
+ children = {}
253
+ for screen in screens:
254
+ children.setdefault(screen.get("parent") or "", []).append(screen)
255
+ lines = []
256
+
257
+ def walk(screen, depth):
258
+ lines.append('<li><a '
259
+ f'href="#{_e(screen["id"])}">{_e(screen.get("name") or screen.get("title") or "Untitled")}</a>')
260
+ kids = children.get(screen["id"], [])
261
+ again = [e for e in out_edges.get(screen["id"], [])
262
+ if e.get("to") and by_id.get(e["to"], {}).get("parent") != screen["id"]]
263
+ if kids or again:
264
+ lines.append("<ul>")
265
+ for kid in kids:
266
+ walk(kid, depth + 1)
267
+ for edge in again[:8]:
268
+ target = by_id.get(edge["to"], {})
269
+ lines.append(f'<li class="again">“{_e(edge["label"])}” goes back to <a href="#{_e(edge["to"])}">'
270
+ f'{_e(target.get("name") or target.get("title") or edge["to"])}</a></li>')
271
+ lines.append("</ul>")
272
+ lines.append("</li>")
273
+
274
+ for root in children.get("", []):
275
+ walk(root, 0)
276
+ return "".join(lines)
277
+
278
+
279
+ def write(result, folder, out=None):
280
+ folder = Path(folder)
281
+ path = Path(out) if out else folder / "report.html"
282
+ path.write_text(render(result, folder), encoding="utf-8")
283
+ return path