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,436 @@
1
+ """Choosing a project's folder, and reading what is in one.
2
+
3
+ ``choose(text)`` turns what the person picked or typed into the folder a project is, or refuses with a sentence.
4
+ ``summary(path)`` is what the Projects list shows: the folder's state, its apps, how many checks and workflow files
5
+ its `.mobster/` holds, its simulator matrix and its branch. ``detail(path)`` adds what costs more to read: the
6
+ checks by name and whether the branch has changes. Both read the folder each time and never write to it.
7
+
8
+ Every git call runs with no terminal prompt, no optional locks (so Mobster never takes the index lock from under
9
+ your own git) and no fsmonitor hook (so a folder's git config can't make Mobster run a program), and gives up after
10
+ 3 seconds.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import time
19
+ from pathlib import Path
20
+
21
+ from ..api_errors import APIError
22
+
23
+ MAX_PATH = 1024
24
+ MAX_APPS = 8
25
+ MAX_LISTED_CHECKS = 200
26
+ GIT_TIMEOUT = 3
27
+ PBXPROJ_LIMIT = 8_000_000
28
+ # Folders an app's sources don't live in: never searched for an Xcode project.
29
+ NOT_SOURCES = frozenset({"node_modules", "Pods", "Carthage", "DerivedData", "build", "dist", "vendor"})
30
+ # Folders that hold everything rather than one app: refused as a project.
31
+ BROAD = ("Desktop", "Documents", "Downloads", "Library", "Movies", "Music", "Pictures", "Public", "Applications")
32
+ SYSTEM = ("/", "/Users", "/System", "/Library", "/Applications", "/Volumes", "/private", "/usr", "/opt", "/tmp",
33
+ "/var", "/bin", "/sbin", "/etc", "/cores")
34
+ WORKFLOW_SUFFIXES = (".yaml", ".yml")
35
+ JSON_LIMIT = 1_000_000
36
+ ICON_DEPTH = 4
37
+ ICON_FOLDERS = 2000
38
+ ICON_LIMIT = 4_000_000
39
+ NOT_IPHONE = ("mac", "watch", "watch-marketing", "car", "tv", "vision", "ipad")
40
+ BUNDLE = re.compile(r'PRODUCT_BUNDLE_IDENTIFIER = "?([^";\s]+)"?;')
41
+ GITHUB = re.compile(r"(?:https://(?:[^@/]+@)?github\.com/|ssh://git@github\.com/|git@github\.com:)"
42
+ r"([A-Za-z0-9-]{1,39})/([A-Za-z0-9._-]{1,100}?)(?:\.git)?/?$")
43
+
44
+
45
+ def shown(path, home=None):
46
+ """``path`` as a sentence shows it: ~/… inside the home folder."""
47
+ home = Path(home or Path.home())
48
+ path = Path(path)
49
+ try:
50
+ return "~/" + path.relative_to(home).as_posix() if path != home else "~"
51
+ except ValueError:
52
+ return str(path)
53
+
54
+
55
+ def choose(text, *, home=None, data_dir=None):
56
+ """The folder ``text`` names, resolved: an absolute path (~ allowed) to a folder Mobster can read that holds one
57
+ project. A `.mobster` folder means the folder holding it. APIError with what to do otherwise."""
58
+ if not isinstance(text, str) or not text.strip() or len(text) > MAX_PATH or "\0" in text:
59
+ raise APIError("Give the folder as a full path, such as ~/Developer/YourApp.", 400, "invalid_path")
60
+ home = Path(home or Path.home()).resolve()
61
+ given = Path(text.strip()).expanduser()
62
+ if not given.is_absolute():
63
+ raise APIError("Give the folder as a full path, such as ~/Developer/YourApp.", 400, "invalid_path")
64
+ try:
65
+ path = given.resolve(strict=True)
66
+ except FileNotFoundError:
67
+ raise APIError(f"There's no folder at {shown(given, home)}. Check the path, or choose the folder again.",
68
+ 404, "missing") from None
69
+ except (OSError, RuntimeError):
70
+ raise APIError(f"Mobster can't read {shown(given, home)}. Check the path, or choose the folder again.",
71
+ 403, "unreadable") from None
72
+ if path.name == ".mobster":
73
+ path = path.parent
74
+ if not path.is_dir():
75
+ raise APIError(f"{shown(path, home)} is a file. Choose the folder that holds your app.", 400, "not_folder")
76
+ system = {Path(p).resolve() for p in SYSTEM}
77
+ if path in system or path == home or path.parent == home and path.name in BROAD:
78
+ raise APIError(f"{shown(path, home)} holds more than one project. Choose your app's own folder inside it.",
79
+ 400, "too_broad")
80
+ if data_dir is not None:
81
+ mine = Path(data_dir).resolve()
82
+ if path == mine or mine in path.parents:
83
+ raise APIError("That's Mobster's own data folder. Choose your app's folder.", 400, "mobster_data")
84
+ unreadable = (f"Mobster can't read {shown(path, home)}. Allow Mobster in System Settings › Privacy & "
85
+ "Security › Files and Folders, then add it again.")
86
+ if not os.access(path, os.R_OK | os.X_OK):
87
+ raise APIError(unreadable, 403, "unreadable")
88
+ try:
89
+ next(path.iterdir(), None)
90
+ except OSError:
91
+ raise APIError(unreadable, 403, "unreadable") from None
92
+ repos = _repositories_in(path)
93
+ if repos >= 2:
94
+ raise APIError(f"{shown(path, home)} holds {repos} repositories. Choose the one with your app.", 400,
95
+ "too_broad")
96
+ return path
97
+
98
+
99
+ def _repositories_in(path, limit=200):
100
+ """How many folders directly in ``path`` are repositories or projects, when ``path`` is neither itself and has
101
+ no app at its root; else 0."""
102
+ try:
103
+ if any((path / name).exists() for name in (".git", ".mobster", "package.json")) \
104
+ or any(path.glob("*.xcodeproj")) or any(path.glob("*.xcworkspace")):
105
+ return 0
106
+ count = 0
107
+ for index, child in enumerate(path.iterdir()):
108
+ if index >= limit:
109
+ break
110
+ if child.is_dir() and ((child / ".git").exists() or (child / ".mobster").is_dir()):
111
+ count += 1
112
+ return count
113
+ except OSError:
114
+ return 0
115
+
116
+
117
+ # -- reading a folder -------------------------------------------------------------------------------------------
118
+
119
+ def state(path, home=None):
120
+ """("ready" | "missing" | "unreadable", a sentence or None)."""
121
+ path = Path(path)
122
+ try:
123
+ if not path.is_dir():
124
+ return "missing", (f"The folder {shown(path, home)} isn't there any more. Move it back, or remove the "
125
+ "project and add the folder where it is now.")
126
+ next(path.iterdir(), None)
127
+ except PermissionError:
128
+ return "unreadable", (f"Mobster can't read {shown(path, home)}. Allow Mobster in System Settings › Privacy & "
129
+ "Security › Files and Folders.")
130
+ except OSError:
131
+ return "unreadable", f"Mobster can't read {shown(path, home)}. Check the folder is still there."
132
+ return "ready", None
133
+
134
+
135
+ def _bundle(xcodeproj):
136
+ """The app's bundle id from the project file: the shortest PRODUCT_BUNDLE_IDENTIFIER that names no build
137
+ setting and no test target. None when there's none."""
138
+ try:
139
+ with open(Path(xcodeproj) / "project.pbxproj", encoding="utf-8", errors="replace") as handle:
140
+ text = handle.read(PBXPROJ_LIMIT)
141
+ except OSError:
142
+ return None
143
+ found = {value for value in BUNDLE.findall(text) if "$" not in value and "test" not in value.lower()}
144
+ return min(found, key=lambda value: (len(value), value)) if found else None
145
+
146
+
147
+ def _no_node(argv, cwd, *args, **kwargs):
148
+ return 1, "" # reading a folder never starts Node: devkit/expo.py falls back to app.json
149
+
150
+
151
+ def apps(root):
152
+ """The apps ``root`` builds, at most MAX_APPS: an Expo or React Native app at its root, then each Xcode workspace
153
+ (else project) at the root or one folder down. Each is {kind, name, path (relative), bundle}."""
154
+ from ..devkit import expo
155
+ root = Path(root)
156
+ out = []
157
+ native = None
158
+ try:
159
+ found = expo.detect(root, runner=_no_node)
160
+ except (OSError, ValueError):
161
+ found = None
162
+ if found is not None:
163
+ out.append({"kind": "expo" if found.expo else "react-native", "name": found.name, "path": ".",
164
+ "bundle": found.bundle})
165
+ native = root / "ios"
166
+ try:
167
+ children = sorted(child for child in root.iterdir() if child.is_dir() and not child.name.startswith(".")
168
+ and child.name not in NOT_SOURCES and child.suffix not in (".xcodeproj", ".xcworkspace"))
169
+ except OSError:
170
+ children = []
171
+ for folder in (root, *children):
172
+ if len(out) >= MAX_APPS:
173
+ break
174
+ if folder == native:
175
+ continue
176
+ try:
177
+ workspaces = sorted(p for p in folder.glob("*.xcworkspace") if p.is_dir())
178
+ projects = sorted(p for p in folder.glob("*.xcodeproj") if p.is_dir())
179
+ except OSError:
180
+ continue
181
+ for path in (workspaces or projects)[:MAX_APPS - len(out)]:
182
+ twin = path.with_suffix(".xcodeproj")
183
+ source = path if path.suffix == ".xcodeproj" else twin if twin.is_dir() else \
184
+ (projects[0] if len(projects) == 1 else None)
185
+ out.append({"kind": "xcode", "name": path.stem, "path": path.relative_to(root).as_posix(),
186
+ "bundle": _bundle(source) if source else None})
187
+ return out
188
+
189
+
190
+ def _inside(root, path):
191
+ """``path`` resolved, when it is a file inside ``root`` (a link can't lead out of the project), else None."""
192
+ try:
193
+ resolved = Path(path).resolve(strict=True)
194
+ except (OSError, RuntimeError):
195
+ return None
196
+ return resolved if resolved.is_file() and Path(root).resolve() in resolved.parents else None
197
+
198
+
199
+ def _json(path):
200
+ try:
201
+ with open(path, encoding="utf-8") as handle:
202
+ value = json.loads(handle.read(JSON_LIMIT))
203
+ except (OSError, ValueError):
204
+ return None
205
+ return value if isinstance(value, dict) else None
206
+
207
+
208
+ def _icon_sets(root):
209
+ """The Xcode asset catalogs' app icon sets under ``root`` (AppIcon first), searched at most ICON_DEPTH folders down
210
+ and through at most ICON_FOLDERS folders, never in hidden folders or NOT_SOURCES."""
211
+ found, seen, queue = [], 0, [(Path(root), 0)]
212
+ while queue and seen < ICON_FOLDERS:
213
+ folder, depth = queue.pop(0)
214
+ seen += 1
215
+ try:
216
+ children = sorted(child for child in folder.iterdir() if child.is_dir() and not child.is_symlink()
217
+ and not child.name.startswith(".") and child.name not in NOT_SOURCES)
218
+ except OSError:
219
+ continue
220
+ for child in children:
221
+ if child.suffix == ".appiconset" and folder.suffix == ".xcassets":
222
+ found.append(child)
223
+ elif depth < ICON_DEPTH and child.suffix not in (".appiconset", ".xcodeproj", ".xcworkspace"):
224
+ queue.append((child, depth + 1))
225
+ return sorted(found, key=lambda path: (path.stem != "AppIcon", "test" in str(path).lower(), str(path)))
226
+
227
+
228
+ _icons = {} # project folder -> (when it was looked for, the icon found or None)
229
+
230
+
231
+ def icon(root, clock=time.monotonic):
232
+ """The app's own icon in the project, a PNG file inside it: Expo's (app.json's ios.icon, else icon), else the
233
+ largest iPhone image of an Xcode asset catalog's app icon set, AppIcon first. None when there is none. What was
234
+ found is kept for a minute, so a list of projects doesn't search every folder each time it is read."""
235
+ now = clock()
236
+ kept = _icons.get(str(root))
237
+ if kept is not None and now - kept[0] < 60 and (kept[1] is None or _inside(root, kept[1]) is not None):
238
+ return kept[1]
239
+ found = _find_icon(Path(root))
240
+ if len(_icons) > 200:
241
+ _icons.clear()
242
+ _icons[str(root)] = (now, found)
243
+ return found
244
+
245
+
246
+ def _find_icon(root):
247
+ config = _json(root / "app.json")
248
+ expo = config.get("expo") if isinstance(config, dict) and isinstance(config.get("expo"), dict) else config
249
+ if isinstance(expo, dict):
250
+ ios = expo.get("ios") if isinstance(expo.get("ios"), dict) else {}
251
+ for value in (ios.get("icon"), expo.get("icon")):
252
+ path = _inside(root, root / value) if isinstance(value, str) and value.lower().endswith(".png") else None
253
+ if path is not None:
254
+ return path
255
+ for folder in _icon_sets(root):
256
+ contents = _json(folder / "Contents.json") or {}
257
+ best = None
258
+ for image in contents.get("images") or ():
259
+ if not isinstance(image, dict) or image.get("appearances") or image.get("idiom") in NOT_IPHONE:
260
+ continue
261
+ name, size, scale = image.get("filename"), str(image.get("size") or ""), str(image.get("scale") or "1x")
262
+ match = re.fullmatch(r"(\d+(?:\.\d+)?)x\1", size)
263
+ path = _inside(root, folder / name) if isinstance(name, str) and name.lower().endswith(".png") else None
264
+ if match and path is not None:
265
+ pixels = float(match.group(1)) * float(scale.rstrip("x") or 1)
266
+ if best is None or pixels > best[0]:
267
+ best = (pixels, path)
268
+ if best is not None:
269
+ return best[1]
270
+ return None
271
+
272
+
273
+ def icon_version(path):
274
+ """A word that changes when the icon file does (its size and change time), for a cache-busting address."""
275
+ try:
276
+ status = Path(path).stat()
277
+ except OSError:
278
+ return None
279
+ return f"{status.st_mtime_ns:x}{status.st_size:x}"
280
+
281
+
282
+ def _count(folder, suffixes):
283
+ try:
284
+ return sum(1 for path in Path(folder).iterdir() if path.is_file() and path.suffix.lower() in suffixes
285
+ and not path.name.startswith("."))
286
+ except OSError:
287
+ return 0
288
+
289
+
290
+ def checks(root, limit=None):
291
+ """The check files in ``root``/.mobster/checks, relative to the project, sorted (testkit/discover's walk)."""
292
+ from ..testkit.discover import check_files
293
+ folder = Path(root) / ".mobster" / "checks"
294
+ if not folder.is_dir():
295
+ return []
296
+ try:
297
+ files = check_files(folder)
298
+ except OSError:
299
+ return []
300
+ return [path.relative_to(root).as_posix() for path in files[:limit]]
301
+
302
+
303
+ def check_tags(root):
304
+ """The tags the project's checks carry, sorted; [] when they can't be read."""
305
+ from ..testkit.discover import DiscoveryError, discover
306
+ try:
307
+ entries = discover([], root)
308
+ except (DiscoveryError, OSError):
309
+ return []
310
+ return sorted({tag for entry in entries for tag in entry.tags})
311
+
312
+
313
+ def simulators(root):
314
+ """(the simulators .mobster/matrix.yaml lists, as "TYPE@RUNTIME", or [], and the file's problem or None)."""
315
+ from ..testkit.targets import TargetError, load_matrix
316
+ path = Path(root) / ".mobster" / "matrix.yaml"
317
+ if not path.is_file():
318
+ return [], None
319
+ try:
320
+ return [f"{t.device_type}@{t.runtime}" if t.runtime else t.device_type for t in load_matrix(path)], None
321
+ except TargetError as error:
322
+ return [], str(error).replace(str(path), ".mobster/matrix.yaml")
323
+
324
+
325
+ _ready = {"at": None, "value": False}
326
+
327
+
328
+ def git_ready(clock=time.monotonic):
329
+ """Whether git can run without macOS offering to install the developer tools (the /usr/bin/git stub opens that
330
+ window when they're missing): a git outside /usr/bin, or a developer folder xcode-select names. Asked again after
331
+ a minute, so installing the tools needs no restart."""
332
+ now = clock()
333
+ if _ready["at"] is not None and now - _ready["at"] < 60:
334
+ return _ready["value"]
335
+ found = shutil.which("git")
336
+ value = bool(found) and Path(found).resolve().parent != Path("/usr/bin")
337
+ if found and not value:
338
+ try:
339
+ result = subprocess.run(["/usr/bin/xcode-select", "-p"], capture_output=True, timeout=GIT_TIMEOUT,
340
+ stdin=subprocess.DEVNULL)
341
+ folder = result.stdout.decode("utf-8", errors="replace").strip() if result.returncode == 0 else ""
342
+ value = bool(folder) and Path(folder, "usr", "bin", "git").is_file()
343
+ except (OSError, subprocess.TimeoutExpired):
344
+ value = False
345
+ _ready.update(at=now, value=value)
346
+ return value
347
+
348
+
349
+ def _git(argv, cwd):
350
+ """(exit code, stdout) of a read-only git call in ``cwd``; never raises, never prompts, never locks."""
351
+ if not git_ready():
352
+ return 1, ""
353
+ env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_OPTIONAL_LOCKS": "0"}
354
+ try:
355
+ result = subprocess.run(["git", "-c", "core.fsmonitor=false", *argv], cwd=str(cwd), capture_output=True,
356
+ timeout=GIT_TIMEOUT, stdin=subprocess.DEVNULL, env=env)
357
+ except (OSError, subprocess.TimeoutExpired):
358
+ return 1, ""
359
+ return result.returncode, result.stdout.decode("utf-8", errors="replace")
360
+
361
+
362
+ def repo_of(url):
363
+ """"owner/name" for a GitHub remote URL, else None."""
364
+ match = GITHUB.match(url or "")
365
+ return f"{match.group(1)}/{match.group(2)}" if match else None
366
+
367
+
368
+ def clean_url(url):
369
+ """A remote URL with any user name and password taken out (https://token@github.com/... is never shown)."""
370
+ return re.sub(r"^(https?://)[^@/]+@", r"\1", url or "") or None
371
+
372
+
373
+ def git(root):
374
+ """{branch, remote, repo} for a git working tree at ``root``, else None. ``branch`` is None when detached."""
375
+ if not (Path(root) / ".git").exists():
376
+ return None
377
+ code, top = _git(["rev-parse", "--show-toplevel"], root)
378
+ if code != 0:
379
+ return None
380
+ code, branch = _git(["symbolic-ref", "--short", "-q", "HEAD"], root)
381
+ _, remote = _git(["config", "--get", "remote.origin.url"], root)
382
+ remote = clean_url(remote.strip())
383
+ return {"branch": (branch.strip() or None) if code == 0 else None, "remote": remote, "repo": repo_of(remote)}
384
+
385
+
386
+ def git_status(root):
387
+ """{head, upstream, ahead, behind, changed} from `git status --porcelain=v2 --branch` (tracked files only), or
388
+ {} when it can't be read in time."""
389
+ code, out = _git(["status", "--porcelain=v2", "--branch", "--untracked-files=no"], root)
390
+ if code != 0:
391
+ return {}
392
+ status = {"head": None, "upstream": None, "ahead": 0, "behind": 0, "changed": 0}
393
+ for line in out.splitlines():
394
+ if line.startswith("# branch.oid "):
395
+ oid = line[13:].strip()
396
+ status["head"] = oid[:12] if re.fullmatch(r"[0-9a-f]{40,64}", oid) else None
397
+ elif line.startswith("# branch.upstream "):
398
+ status["upstream"] = line[18:].strip() or None
399
+ elif line.startswith("# branch.ab "):
400
+ match = re.fullmatch(r"\+(\d+) -(\d+)", line[12:].strip())
401
+ if match:
402
+ status["ahead"], status["behind"] = int(match.group(1)), int(match.group(2))
403
+ elif line and not line.startswith("#"):
404
+ status["changed"] += 1
405
+ return status
406
+
407
+
408
+ def summary(path, home=None):
409
+ """What the Projects list shows for the folder at ``path``."""
410
+ path = Path(path)
411
+ current, problem = state(path, home)
412
+ out = {"state": current, "problem": problem, "mobster": False, "apps": [], "icon": None, "checks": 0,
413
+ "workflows": 0, "simulators": [], "matrixProblem": None, "git": None}
414
+ if current != "ready":
415
+ return out
416
+ out["mobster"] = (path / ".mobster").is_dir()
417
+ out["apps"] = apps(path)
418
+ found = icon(path)
419
+ out["icon"] = icon_version(found) if found else None
420
+ if out["mobster"]:
421
+ out["checks"] = len(checks(path))
422
+ out["workflows"] = _count(path / ".mobster" / "workflows", WORKFLOW_SUFFIXES)
423
+ out["simulators"], out["matrixProblem"] = simulators(path)
424
+ out["git"] = git(path)
425
+ return out
426
+
427
+
428
+ def detail(path, home=None):
429
+ """``summary`` with the checks by file and their tags and, in a git working tree, the commit, upstream and changed
430
+ files."""
431
+ out = summary(path, home)
432
+ out["checkFiles"] = checks(path, MAX_LISTED_CHECKS) if out["mobster"] else []
433
+ out["checkTags"] = check_tags(path) if out["checkFiles"] else []
434
+ if out["git"] is not None:
435
+ out["git"].update(git_status(path))
436
+ return out
@@ -0,0 +1,131 @@
1
+ """Import from GitHub: the repositories your GitHub account can open, through the GitHub connector's sign-in, and that
2
+ sign-in for cloning one of them.
3
+
4
+ ``repositories(token)`` lists them, the one pushed to last first: owner/name, whether it's private, its language and
5
+ default branch and when it was last pushed. ``clone_env(token)`` is what git needs to clone a private repository from
6
+ github.com with that sign-in: an ``http.https://github.com/.extraheader`` set through git's environment, so the token
7
+ is never in a command line, a URL, a log or the clone's own .git/config, and is sent to github.com only.
8
+
9
+ Mobster never asks for more than the connector's sign-in already allows, and never keeps the list.
10
+ """
11
+
12
+ import base64
13
+ import json
14
+ import os
15
+ import urllib.error
16
+ import urllib.request
17
+
18
+ from ..api_errors import APIError
19
+
20
+ API = "https://api.github.com"
21
+ PRESET = "github"
22
+ PAGES = 3
23
+ PER_PAGE = 100
24
+ TIMEOUT = 15
25
+ SIGN_IN_AGAIN = "GitHub didn't accept Mobster's sign-in. Sign in to GitHub again in Connectors, then try again."
26
+ OFFLINE = "Mobster couldn't reach GitHub. Check your connection, then try again."
27
+
28
+
29
+ def token_of(runtime):
30
+ """The GitHub connector's sign-in, or None when there is none (or the Keychain won't give it)."""
31
+ from ..connectors import service as connectors
32
+ service = connectors.service_for(runtime)
33
+ if service is None:
34
+ return None
35
+ try:
36
+ return service.preset_bearer(PRESET)
37
+ except APIError:
38
+ return None
39
+
40
+
41
+ def repositories(token, *, opener=urllib.request.urlopen):
42
+ """Every repository the account can open (owner, collaborator, organization member), newest push first, at most
43
+ PAGES * PER_PAGE of them. APIError with what to do when GitHub refuses or can't be reached."""
44
+ out = []
45
+ for page in range(1, PAGES + 1):
46
+ request = urllib.request.Request(
47
+ f"{API}/user/repos?per_page={PER_PAGE}&page={page}&sort=pushed"
48
+ "&affiliation=owner,collaborator,organization_member",
49
+ headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
50
+ "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "Mobster"})
51
+ try:
52
+ with opener(request, timeout=TIMEOUT) as response:
53
+ rows = json.loads(response.read(8_000_000).decode("utf-8"))
54
+ except urllib.error.HTTPError as error:
55
+ if error.code in (401, 403):
56
+ raise APIError(SIGN_IN_AGAIN, 401, "github_sign_in") from None
57
+ raise APIError(f"GitHub answered {error.code}. Try again in a minute.", 502, "github_error") from None
58
+ except (urllib.error.URLError, TimeoutError, OSError, ValueError):
59
+ raise APIError(OFFLINE, 503, "offline") from None
60
+ if not isinstance(rows, list):
61
+ raise APIError("GitHub sent back something Mobster didn't expect. Try again.", 502, "github_error")
62
+ out += [repo for repo in (_repo(row) for row in rows) if repo is not None]
63
+ if len(rows) < PER_PAGE:
64
+ break
65
+ return out
66
+
67
+
68
+ def pulls(token, repo, *, opener=urllib.request.urlopen):
69
+ """The repository's pull requests from its own branches, the one updated last first (one page of PER_PAGE), as
70
+ {branch: {number, title, url, state (open, draft, merged or closed)}}: the newest for each branch. A pull request
71
+ from a fork names a branch of another repository, so it is left out. APIError as ``repositories``."""
72
+ request = urllib.request.Request(
73
+ f"{API}/repos/{repo}/pulls?state=all&sort=updated&direction=desc&per_page={PER_PAGE}",
74
+ headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
75
+ "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "Mobster"})
76
+ try:
77
+ with opener(request, timeout=TIMEOUT) as response:
78
+ rows = json.loads(response.read(8_000_000).decode("utf-8"))
79
+ except urllib.error.HTTPError as error:
80
+ if error.code in (401, 403):
81
+ raise APIError(SIGN_IN_AGAIN, 401, "github_sign_in") from None
82
+ if error.code == 404: # a repository the sign-in can't see
83
+ return {}
84
+ raise APIError(f"GitHub answered {error.code}. Try again in a minute.", 502, "github_error") from None
85
+ except (urllib.error.URLError, TimeoutError, OSError, ValueError):
86
+ raise APIError(OFFLINE, 503, "offline") from None
87
+ out = {}
88
+ for row in rows if isinstance(rows, list) else ():
89
+ pull = _pull(row, repo)
90
+ if pull is not None:
91
+ out.setdefault(pull.pop("branch"), pull)
92
+ return out
93
+
94
+
95
+ def _pull(row, repo):
96
+ """A pull request as runs by branch show it, checked at the boundary; None for one that isn't, or a fork's."""
97
+ head = row.get("head") if isinstance(row, dict) else None
98
+ head_repo = head.get("repo") if isinstance(head, dict) else None
99
+ if not (isinstance(head, dict) and isinstance(head.get("ref"), str) and isinstance(row.get("number"), int)
100
+ and isinstance(head_repo, dict) and str(head_repo.get("full_name", "")).lower() == repo.lower()):
101
+ return None
102
+ state = "merged" if row.get("merged_at") else "draft" if row.get("draft") is True and row.get("state") == "open" \
103
+ else "open" if row.get("state") == "open" else "closed"
104
+ title = row["title"][:200] if isinstance(row.get("title"), str) else ""
105
+ return {"branch": head["ref"], "number": row["number"], "title": title,
106
+ "url": f"https://github.com/{repo}/pull/{row['number']}", "state": state}
107
+
108
+
109
+ def _repo(row):
110
+ """A repository as the Add page shows it, checked at the boundary; None for one that isn't."""
111
+ if not isinstance(row, dict) or not isinstance(row.get("full_name"), str) or "/" not in row["full_name"]:
112
+ return None
113
+ owner, name = row["full_name"].split("/", 1)
114
+ text = lambda key, high=200: row[key][:high] if isinstance(row.get(key), str) else None # noqa: E731
115
+ return {"repo": row["full_name"], "owner": owner, "name": name, "private": row.get("private") is True,
116
+ "description": text("description", 300), "language": text("language", 40),
117
+ "defaultBranch": text("default_branch", 200), "pushedAt": text("pushed_at", 40),
118
+ "archived": row.get("archived") is True, "url": f"https://github.com/{row['full_name']}.git"}
119
+
120
+
121
+ def clone_env(token, base=None):
122
+ """git's environment for a clone from github.com with the connector's sign-in: http.extraheader for that host
123
+ only, added after any GIT_CONFIG_* entries ``base`` (os.environ) already has, so none of them is lost."""
124
+ base = os.environ if base is None else base
125
+ try:
126
+ count = max(0, int(base.get("GIT_CONFIG_COUNT", "0")))
127
+ except ValueError:
128
+ count = 0
129
+ basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
130
+ return {"GIT_CONFIG_COUNT": str(count + 1), f"GIT_CONFIG_KEY_{count}": "http.https://github.com/.extraheader",
131
+ f"GIT_CONFIG_VALUE_{count}": f"AUTHORIZATION: basic {basic}"}
@@ -0,0 +1,31 @@
1
+ """Track ``projects``' registrations (tracks.load calls ``register(api)`` once per process, staged).
2
+
3
+ - Journal schema ``projects`` v2 (store.py): projects, their check runs and approved CI revisions.
4
+ - The run field ``project`` (public): the project a task belongs to, saved as {id, path, name}, so its memory can be
5
+ the project's (the memory track reads id and path).
6
+ - One service (a ProjectService per Runtime) and the routes under /api/projects. Adding or opening a project tells
7
+ memory its folder (POST /api/memory/projects), when the memory track is here.
8
+
9
+ No CLI command and no MCP provider of its own yet: `mobster test` and the MCP tools already find a project by its
10
+ `.mobster/` folder.
11
+ """
12
+
13
+ from . import routes, service
14
+
15
+
16
+ def validate_project(value, runtime):
17
+ """The run field ``project``: a project's id or {id}, saved as {id, path, name} (service.run_field)."""
18
+ found = service.service_for(runtime)
19
+ if found is None:
20
+ raise ValueError(service.GONE)
21
+ return found.run_field(value)
22
+
23
+
24
+ def register(api):
25
+ """``api`` is mobile_agent.harness_api."""
26
+ from .. import journal
27
+ from . import store
28
+ journal.register_schema(store.SCHEMA, store.VERSION, store.migrate)
29
+ api.register_run_field("project", validate_project, public=True)
30
+ api.register_service(service.start, service.close)
31
+ routes.register_routes()