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,373 @@
1
+ """Words to a workflow graph (docs/workflows.mdx › Make a workflow from words): the model proposes, the person confirms.
2
+
3
+ ``POST /api/workflows/draft {"words"}`` asks the drafter (Claude Haiku 5.5, as Save as workflow's) for a plan: steps
4
+ with what each waits for. Mobster compiles the plan into a graph by code, checks it as a save would
5
+ (graph/schema.py), and when the check finds problems gives the drafter the list, at most ``REPAIRS`` times. The answer
6
+ is the best graph with any problem still in it listed, so the canvas can mark them. Nothing is saved.
7
+
8
+ With ``graph`` (the workflow as the canvas has it) the words change that workflow: the drafter returns the whole
9
+ plan with the change, steps it keeps keep their ids and places, and the answer adds ``patch``, each change as one
10
+ item the person accepts or rejects.
11
+
12
+ The words are the person's, so they may fill a step's words. Nothing from a phone's screen reaches the drafter.
13
+ """
14
+
15
+ import re
16
+ import time
17
+
18
+ from ..catalog import APPS, pack_apps
19
+ from ..workflows import starters
20
+ from . import chain, edit, schema
21
+ from .catalog import MAX_TIMES
22
+
23
+ REPAIRS = 2
24
+ # The page waits a minute for a draft (lib/workflow-graph.ts DRAFT_MS): the model's tries and their repairs end inside
25
+ # 45 seconds, and the best plan so far is the answer, so a slow repair never leaves the page with nothing (ICP-10).
26
+ BUDGET_S, TRY_S, LEAST_S = 45, 40, 8
27
+ STEP_ID = re.compile(r"[^A-Za-z0-9_-]+")
28
+ KINDS = ("task", "read", "check", "wait", "send", "branch", "loop", "approval", "merge")
29
+ # The canvas draws a node 320 wide and 64 tall (geometry.ts NODE_W, NODE_H): side by side 368 apart, rows 128 (DR-66).
30
+ PITCH, SIDE = 128, 368
31
+
32
+ PLAN_SCHEMA = {"type": "object", "additionalProperties": False, "required": ["name", "schedule", "steps"],
33
+ "properties": {
34
+ "name": {"type": "string"},
35
+ "schedule": {"type": ["string", "null"]},
36
+ "steps": {"type": "array", "items": {
37
+ "type": "object", "additionalProperties": False,
38
+ "required": ["id", "kind", "text", "after", "app", "reader", "seconds", "to", "cases",
39
+ "over", "times", "inside"],
40
+ "properties": {
41
+ "id": {"type": "string"}, "kind": {"type": "string", "enum": list(KINDS)},
42
+ "text": {"type": ["string", "null"]},
43
+ "after": {"type": "array", "items": {"type": "string"}},
44
+ "app": {"type": ["string", "null"]}, "reader": {"type": ["string", "null"]},
45
+ "seconds": {"type": ["integer", "null"]}, "to": {"type": ["string", "null"]},
46
+ "cases": {"type": ["array", "null"], "items": {
47
+ "type": "object", "additionalProperties": False, "required": ["id", "label", "when"],
48
+ "properties": {"id": {"type": "string"}, "label": {"type": "string"},
49
+ "when": {"type": "string"}}}},
50
+ "over": {"type": ["string", "null"]}, "times": {"type": ["integer", "null"]},
51
+ "inside": {"type": ["string", "null"]}}}}}}
52
+
53
+ SYSTEM = """You turn what a person wants Mobster to do on their iPhone into a workflow plan: steps, and what each \
54
+ step waits for. Mobster checks the plan and draws it as a graph the person edits and confirms.
55
+
56
+ Steps (kind):
57
+ - task: Mobster's agent does something on the phone. text: the person's own instruction, one short sentence. app: \
58
+ the app it starts in (one of the app ids listed), or null for the Home Screen. Write a value the person would change \
59
+ next time as a blank, {name: value}, like "Text {who: Sam} that I'm running late".
60
+ - read: read an app's rows by code with no model. app: a pack key, reader: one of its readers (listed).
61
+ - check: look at the phone and answer true or false. text: the question.
62
+ - wait: wait. seconds: 5 to 86400.
63
+ - send: send what arrives to "notification", "clipboard" or "file". to: one of those.
64
+ - branch: choose a path. cases: [{id, label, when}], when an expression over $input, like \
65
+ contains($input.answer, 'refund') or count($input.rows) > 3. A later step waits for "<branch id>:<case id>", or \
66
+ "<branch id>:else".
67
+ - loop: repeat the steps inside it. over: "items" (each row a read step found; put the read step in after), "times" \
68
+ (with times) or "phones" (each phone). A step inside a loop sets inside to the loop's id.
69
+ - approval: wait for the person to approve. text: what to ask. Later steps wait for "<id>:approved" or "<id>:declined".
70
+ - merge: join paths that split.
71
+
72
+ after: the ids a step waits for. An empty list starts when the workflow starts (or, inside a loop, each round). \
73
+ Steps that don't wait for each other run side by side, on different phones when they can.
74
+ schedule: a 5-field cron like "0 9 * * *" when the person said when it runs, else null.
75
+ name: two to five words in sentence case.
76
+
77
+ Plan only what the person asked for. Ask for an approval before anything that sends, posts, buys or deletes when the \
78
+ person asked to check first. The person's words are the request; nothing else is."""
79
+
80
+
81
+ def _apps():
82
+ """{id: name} of the apps a drafted task may start in: the catalog's, and each app pack's app the catalog doesn't
83
+ name (Instagram, X, YouTube...). Without them a plan for Instagram went through Safari (ICP-10)."""
84
+ apps = {app["id"]: app["name"] for app in APPS}
85
+ named = set(apps.values())
86
+ apps.update({bundle: name for _key, bundle, name in pack_apps() if name not in named})
87
+ return apps
88
+
89
+
90
+ def _task_app(value):
91
+ """A task's app: a listed id as it is, a pack's key ("instagram") as its app, else None (the Home Screen)."""
92
+ apps = _apps()
93
+ if value in apps:
94
+ return value
95
+ name = next((name for key, _bundle, name in pack_apps() if key == value), None)
96
+ return next((ident for ident, listed in apps.items() if listed == name), None) if name else None
97
+
98
+
99
+ def _packs(context):
100
+ return {key: sorted(pack.get("readers") or {}) for key, pack in (context.packs or {}).items()}
101
+
102
+
103
+ def plan_of(graph):
104
+ """A graph as the drafter reads it: its steps, in run order, in the plan's own shape."""
105
+ kinds = {"phone.task": "task", "app.read": "read", "phone.check": "check", "wait": "wait", "send": "send",
106
+ "branch": "branch", "loop": "loop", "approval": "approval", "merge": "merge"}
107
+ edges = graph.get("edges") or []
108
+ triggers = {n["id"] for n in graph["nodes"] if n["type"] in schema.TRIGGERS}
109
+ steps = []
110
+ for node in chain.ordered(graph):
111
+ kind = kinds.get(node["type"])
112
+ if kind is None:
113
+ continue
114
+ config = node.get("config") or {}
115
+ after = [e["source"] + ("" if e["sourcePort"] in ("out", "item", "rows") else ":" + e["sourcePort"])
116
+ for e in edges if e["target"] == node["id"] and e["source"] not in triggers
117
+ and not (e["sourcePort"] in ("item", "index") and e["source"] == node.get("parent"))]
118
+ steps.append({"id": node["id"], "kind": kind, "after": after, "inside": node.get("parent"),
119
+ "text": config.get("text") or config.get("question") or config.get("message"),
120
+ "app": config.get("appId") or config.get("app"), "reader": config.get("reader"),
121
+ "seconds": config.get("seconds"), "to": config.get("to"), "cases": config.get("cases"),
122
+ "over": config.get("over"), "times": config.get("times")})
123
+ schedule = next((n["config"].get("cron") for n in graph["nodes"] if n["type"] == "trigger.schedule"), None)
124
+ return {"steps": steps, "schedule": schedule}
125
+
126
+
127
+ def compile_plan(plan, before=None, zone="UTC"):
128
+ """The graph a plan makes, by code. Steps the graph ``before`` had keep their places and titles."""
129
+ steps = plan.get("steps") or []
130
+ ids, seen = {}, set()
131
+ for index, step in enumerate(steps):
132
+ ident = STEP_ID.sub("-", str(step.get("id") or f"s{index + 1}")).strip("-")[:24] or f"s{index + 1}"
133
+ while ident in seen or ident in ("start", "schedule"):
134
+ ident = f"{ident[:20]}-{index + 1}"
135
+ seen.add(ident)
136
+ ids[str(step.get("id"))] = ident
137
+ nodes = [{"id": "start", "type": "trigger.manual", "config": {}}]
138
+ if plan.get("schedule"):
139
+ nodes.append({"id": "schedule", "type": "trigger.schedule",
140
+ "config": {"cron": plan["schedule"], "timezone": zone}})
141
+ edges = []
142
+
143
+ def edge(source, port, target, target_port="in"):
144
+ edges.append({"id": f"e{len(edges) + 1}", "source": source, "sourcePort": port, "target": target,
145
+ "targetPort": target_port})
146
+ by_id = {ids[str(step.get("id"))]: step for step in steps}
147
+ for step in steps:
148
+ ident, kind = ids[str(step.get("id"))], step.get("kind")
149
+ text = step.get("text") or ""
150
+ if kind == "task":
151
+ node = {"type": "phone.task", "config": {"text": text, "appId": _task_app(step.get("app"))}}
152
+ elif kind == "read":
153
+ node = {"type": "app.read", "config": {"app": step.get("app"), "reader": step.get("reader"), "n": 10}}
154
+ elif kind == "check":
155
+ node = {"type": "phone.check", "config": {"question": text}}
156
+ elif kind == "wait":
157
+ node = {"type": "wait", "config": {"seconds": step.get("seconds") or 60}}
158
+ elif kind == "send":
159
+ node = {"type": "send", "config": {"to": step.get("to") or "notification"}}
160
+ elif kind == "branch":
161
+ node = {"type": "branch", "config": {"cases": step.get("cases") or []}}
162
+ elif kind == "loop":
163
+ over = step.get("over") if step.get("over") in ("items", "times", "phones") else "times"
164
+ config = {"over": over}
165
+ if over == "times":
166
+ config["times"] = max(2, min(step.get("times") or 2, MAX_TIMES))
167
+ if over == "phones":
168
+ config["phones"] = "all"
169
+ node = {"type": "loop", "config": config}
170
+ elif kind == "approval":
171
+ node = {"type": "approval", "config": {"message": text or "Go on?"}}
172
+ else:
173
+ node = {"type": "merge", "config": {}}
174
+ node["id"] = ident
175
+ inside = ids.get(str(step.get("inside"))) if step.get("inside") is not None else None
176
+ if inside and by_id.get(inside, {}).get("kind") == "loop":
177
+ node["parent"] = inside
178
+ nodes.append(node)
179
+ waits = [str(a) for a in step.get("after") or []]
180
+ for wanted in waits:
181
+ source, _, port = wanted.partition(":")
182
+ source = ids.get(source)
183
+ if source is None:
184
+ continue
185
+ source_kind = by_id[source].get("kind")
186
+ if kind == "loop" and node["config"]["over"] == "items" and source_kind == "read" and not port:
187
+ edge(source, "rows", ident, "items")
188
+ else:
189
+ edge(source, port or "out", ident)
190
+ if not any(ids.get(w.partition(":")[0]) for w in waits):
191
+ if node.get("parent"):
192
+ edge(node["parent"], "item", ident)
193
+ else:
194
+ for trigger in [n["id"] for n in nodes if n["type"] in schema.TRIGGERS]:
195
+ edge(trigger, "out", ident)
196
+ graph = {"version": schema.VERSION, "inputs": [], "vars": {}, "nodes": nodes, "edges": edges,
197
+ "limits": dict((before or {}).get("limits") or schema.DEFAULT_LIMITS)}
198
+ _layout(graph, before)
199
+ return graph
200
+
201
+
202
+ def _layout(graph, before=None):
203
+ """Places: what ``before`` had keeps its place; the rest in rows by how far from a trigger, side by side."""
204
+ kept = {node["id"]: node for node in (before or {}).get("nodes") or []}
205
+ nodes = {node["id"]: node for node in graph["nodes"]}
206
+ depth = {}
207
+
208
+ def level(ident, trail=()):
209
+ if ident in depth:
210
+ return depth[ident]
211
+ if ident in trail:
212
+ return 0
213
+ sources = [e["source"] for e in graph["edges"] if e["target"] == ident and e["source"] in nodes
214
+ and nodes[e["source"]].get("parent") == nodes[ident].get("parent")]
215
+ depth[ident] = 1 + max((level(s, trail + (ident,)) for s in sources), default=-1)
216
+ return depth[ident]
217
+ rows = {}
218
+ for ident, node in nodes.items():
219
+ old = kept.get(ident)
220
+ if old is not None and old.get("type") == node["type"] and old.get("parent") == node.get("parent"):
221
+ node["position"] = dict(old.get("position") or {"x": 0, "y": 0})
222
+ for key in ("title", "notes"):
223
+ if old.get(key):
224
+ node[key] = old[key]
225
+ continue
226
+ row = (node.get("parent"), level(ident))
227
+ slot = rows[row] = rows.get(row, -1) + 1
228
+ # Triggers sit in the first row at their own, narrower spacing (chain.SIDE), as the canvas draws them.
229
+ side = chain.SIDE if node["type"].startswith("trigger.") else SIDE
230
+ node["position"] = ({"x": 24 + side * slot, "y": 64 + PITCH * row[1]} if node.get("parent")
231
+ else {"x": side * slot, "y": PITCH * row[1]})
232
+ # One new step under exactly two triggers sits centred under them (chain.STEPS_X), so both lines meet it in the
233
+ # middle.
234
+ top = [n for n in nodes.values() if not n.get("parent")]
235
+ triggers = [n for n in top if n["type"].startswith("trigger.")]
236
+ under = [n for n in top if depth.get(n["id"]) == 1 and n["id"] not in kept]
237
+ if len(triggers) == 2 and len(under) == 1:
238
+ under[0]["position"]["x"] = chain.STEPS_X
239
+
240
+
241
+ def _prompt(words, context, current=None, problems=None):
242
+ lines = [f"What the person wants: {words}", "",
243
+ "App ids (name): " + ", ".join(f"{ident} ({name})" for ident, name in sorted(_apps().items())),
244
+ "Packs and their readers: " + "; ".join(f"{key}: {', '.join(readers)}"
245
+ for key, readers in _packs(context).items())]
246
+ if current is not None:
247
+ import json
248
+ lines += ["", "The workflow now (keep the ids of steps that stay):",
249
+ json.dumps(current, ensure_ascii=False)[:6000]]
250
+ if problems:
251
+ lines += ["", "Your last plan had these problems. Fix them and return the whole plan:"]
252
+ lines += [f"- {problem.get('node') or problem.get('edge') or ''}: {problem['message']}" for problem in problems]
253
+ return "\n".join(lines)
254
+
255
+
256
+ DAYS = {"sunday": 0, "monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4, "friday": 5, "saturday": 6}
257
+ PARTS = {"morning": 9, "afternoon": 14, "evening": 18, "night": 21, "day": 9, "weekday": 9, "weekend": 9}
258
+ WHEN = re.compile(r"\b(?:every|each)\s+(?:(?P<part>morning|afternoon|evening|night|day|weekday|weekend|hour)|"
259
+ r"(?P<name>sunday|monday|tuesday|wednesday|thursday|friday|saturday))s?"
260
+ r"(?:\s+(?:at\s+)?(?P<h>\d{1,2})(?::(?P<m>\d{2}))?\s*(?P<ap>[ap])\.?m\.?)?\b\.?", re.I)
261
+ OPEN_APP = re.compile(r"^open\s+(?:the\s+)?(?P<name>.+?)(?:\s+app)?(?:\s+on\b.*)?$", re.I)
262
+
263
+
264
+ def _cron_of(found):
265
+ """The 5-field cron a schedule phrase means, or None when its time can't be read (an hour past 12 with am/pm)."""
266
+ part, name = (found["part"] or "").lower(), (found["name"] or "").lower()
267
+ if part == "hour":
268
+ return "0 * * * *" if not found["h"] else None
269
+ hour, minute = PARTS.get(part, 9), int(found["m"] or 0)
270
+ if found["h"]:
271
+ hour = int(found["h"])
272
+ if found["ap"]:
273
+ if not 1 <= hour <= 12:
274
+ return None
275
+ hour = hour % 12 + (12 if found["ap"].lower() == "p" else 0)
276
+ if not 0 <= hour <= 23 or not 0 <= minute <= 59:
277
+ return None
278
+ days = {"weekday": "1-5", "weekend": "0,6"}.get(part) or (str(DAYS[name]) if name else "*")
279
+ return f"{minute} {hour} * * {days}"
280
+
281
+
282
+ def schedule_of(words):
283
+ for found in WHEN.finditer(words or ""):
284
+ cron = _cron_of(found)
285
+ if cron is not None:
286
+ rest = (words[:found.start()] + " " + words[found.end():]).strip(" ,;")
287
+ return cron, re.sub(r"\s+", " ", re.sub(r"\s+,", ",", rest)).strip(" ,;") or words
288
+ return None, words
289
+
290
+
291
+ def _clauses(sentence):
292
+ """A sentence's steps: "open X, read Y, and tell me Z" is three, split at commas and "and then"."""
293
+ parts = [p.strip() for p in re.split(r",\s*(?:and\s+|then\s+)?|\s+and\s+then\s+", sentence) if p.strip()]
294
+ return parts if len(parts) > 1 and all(len(p.split()) >= 3 for p in parts) else [sentence]
295
+
296
+
297
+ def local(words):
298
+ schedule, words = schedule_of(words)
299
+ sentences = []
300
+ for part in re.split(r"(?<=[.!?])\s+|\n+|,?\s+then\s+", words or ""):
301
+ part = re.sub(r"^(?:and\s+)?then\s+", "", part.strip(), flags=re.I)
302
+ for clause in _clauses(part) if part else []:
303
+ sentences.append(clause[:1].upper() + clause[1:])
304
+ cut = len(sentences) - 12
305
+ sentences = sentences[:12]
306
+ names = {app["name"].casefold(): app["id"] for app in APPS}
307
+ opened = OPEN_APP.match(sentences[0].rstrip(".")) if sentences else None
308
+ start_app = names.get(opened["name"].casefold()) if opened else None
309
+ steps = [{"id": f"s{index + 1}", "kind": "task", "text": text.rstrip("."), "after": [f"s{index}"] if index else [],
310
+ "app": start_app if index == 0 else None, "reader": None, "seconds": None, "to": None, "cases": None,
311
+ "over": None, "times": None, "inside": None} for index, text in enumerate(sentences)]
312
+ return {"name": starters.suggest_name(sentences[0]) if sentences else "Workflow", "schedule": schedule,
313
+ "steps": steps, "cut": max(0, cut)}
314
+
315
+
316
+ def draft(words, context, *, client=None, current=None, zone="UTC", budget=BUDGET_S):
317
+ if not isinstance(words, str) or not 1 <= len(words.strip()) <= 2000:
318
+ raise ValueError("Say what the workflow does in 1 to 2,000 characters")
319
+ words = " ".join(words.split())
320
+ shown = plan_of(current) if current is not None else None
321
+ best, problems, drafter = None, [], "local"
322
+ if client is not None:
323
+ deadline = time.monotonic() + budget
324
+ for _attempt in range(REPAIRS + 1):
325
+ left = deadline - time.monotonic()
326
+ if left < LEAST_S:
327
+ break
328
+ messages = [{"role": "system", "content": SYSTEM},
329
+ {"role": "user", "content": [{"type": "text", "text": _prompt(words, context, shown,
330
+ problems)}]}]
331
+ try:
332
+ plan, _usage = client.complete(messages, PLAN_SCHEMA, timeout=min(TRY_S, left))
333
+ except Exception: # noqa: BLE001 -- the model is optional here; the rule drafts instead
334
+ break
335
+ if not isinstance(plan, dict) or not plan.get("steps"):
336
+ break
337
+ drafter = getattr(client, "model", "model")
338
+ graph = compile_plan(plan, current, zone)
339
+ try:
340
+ best, problems = (plan, schema.validate(graph, context)), []
341
+ break
342
+ except schema.GraphError as error:
343
+ best, problems = (plan, graph), error.errors
344
+ if best is None:
345
+ plan = local(words)
346
+ graph = compile_plan(plan, current, zone)
347
+ try:
348
+ graph, problems = schema.validate(graph, context), []
349
+ except schema.GraphError as error:
350
+ problems = error.errors
351
+ best, drafter = (plan, graph), "local"
352
+ plan, graph = best
353
+ name = " ".join(str(plan.get("name") or "").split()).rstrip(".")[:60] or starters.suggest_name(words)
354
+ out = {"draft": {"name": name, "graph": graph}, "drafter": drafter, "errors": problems,
355
+ "drafted_by": "rules" if drafter == "local" else "model"}
356
+ if drafter == "local":
357
+ # The rules read an English schedule, a step per sentence (twelve at most), and nothing else: the page says so.
358
+ out["notes"] = ["Drafted by simple rules, not a model: event triggers, loops, approvals and schedules that "
359
+ "aren't in English are not read. Connect an AI account or sign in to a coding agent to draft "
360
+ "with a model." if client is None else
361
+ "The model didn't answer, so simple rules drafted this: event triggers, loops, approvals and "
362
+ "schedules that aren't in English are not read."]
363
+ if plan.get("cut"):
364
+ out["notes"].append(f"Only the first 12 steps were kept; {plan['cut']} more were left out.")
365
+ if current is not None:
366
+ changes = edit.diff(current, graph)
367
+ out["patch"] = ([{"op": "add", "node": n["id"], "after": n} for n in changes["nodes"]["added"]]
368
+ + [{"op": "remove", "node": n["id"], "before": n} for n in changes["nodes"]["removed"]]
369
+ + [{"op": "change", "node": c["id"], "before": c["before"], "after": c["after"]}
370
+ for c in changes["nodes"]["changed"]]
371
+ + [{"op": "add", "edge": e["id"], "after": e} for e in changes["edges"]["added"]]
372
+ + [{"op": "remove", "edge": e["id"], "before": e} for e in changes["edges"]["removed"]])
373
+ return out
@@ -0,0 +1,76 @@
1
+ """Edits to a saved graph that aren't the canvas's own: the old schedule fields applied to its schedule trigger, and
2
+ what changed between two versions (a diff, and a line for the versions list)."""
3
+
4
+ from . import chain, schema
5
+
6
+
7
+ def with_schedule(graph, cron, zone):
8
+ nodes = [dict(node) for node in graph["nodes"]]
9
+ edges = [dict(edge) for edge in graph.get("edges") or []]
10
+ schedule = next((node for node in nodes if node["type"] == "trigger.schedule"), None)
11
+ if cron is None:
12
+ if schedule is None:
13
+ return graph
14
+ return {**graph, "nodes": [n for n in nodes if n is not schedule],
15
+ "edges": [e for e in edges if e["source"] != schedule["id"]]}
16
+ if schedule is not None:
17
+ schedule["config"] = {"cron": cron, "timezone": zone}
18
+ return {**graph, "nodes": nodes, "edges": edges}
19
+ taken = {node["id"] for node in nodes} | {edge["id"] for edge in edges}
20
+ ident = chain._unique("schedule", taken)
21
+ manual = next((node for node in nodes if node["type"] == "trigger.manual"), None)
22
+ at = (manual or {}).get("position") or {"x": 0, "y": 0}
23
+ nodes.append({"id": ident, "type": "trigger.schedule", "config": {"cron": cron, "timezone": zone},
24
+ "position": {"x": at["x"] + chain.SIDE, "y": at["y"]}})
25
+ for edge in [e for e in edges if manual is not None and e["source"] == manual["id"]]:
26
+ edges.append({**edge, "id": chain._unique("e" + ident, taken), "source": ident})
27
+ return {**graph, "nodes": nodes, "edges": edges}
28
+
29
+
30
+ def diff(before, after):
31
+ """{"nodes": {"added", "removed", "changed": [{"id", "before", "after"}]}, "edges": {"added", "removed"}}."""
32
+ old = {node["id"]: node for node in before.get("nodes") or []}
33
+ new = {node["id"]: node for node in after.get("nodes") or []}
34
+ old_edges = {edge["id"]: edge for edge in before.get("edges") or []}
35
+ new_edges = {edge["id"]: edge for edge in after.get("edges") or []}
36
+ return {"nodes": {"added": [new[i] for i in new if i not in old], "removed": [old[i] for i in old if i not in new],
37
+ "changed": [{"id": i, "before": old[i], "after": new[i]} for i in new if i in old
38
+ and old[i] != new[i]]},
39
+ "edges": {"added": [new_edges[i] for i in new_edges if old_edges.get(i) != new_edges[i]],
40
+ "removed": [old_edges[i] for i in old_edges if new_edges.get(i) != old_edges[i]]},
41
+ "settings": {key: {"before": before.get(key), "after": after.get(key)}
42
+ for key in ("inputs", "vars", "limits") if before.get(key) != after.get(key)}}
43
+
44
+
45
+ def change_line(before, after):
46
+ """What changed between two versions' graphs, in a line: "Added 2 steps, changed Reply"."""
47
+ if before is None:
48
+ return "Created"
49
+ old = {node["id"]: node for node in before.get("nodes") or []}
50
+ new = {node["id"]: node for node in after.get("nodes") or []}
51
+
52
+ def label(node):
53
+ known = schema.TYPES.get(node["type"])
54
+ return node.get("title") or (known.label if known else node["id"])
55
+
56
+ added = [new[i] for i in new if i not in old]
57
+ removed = [old[i] for i in old if i not in new]
58
+ changed = [new[i] for i in new if i in old and {k: v for k, v in new[i].items() if k != "position"}
59
+ != {k: v for k, v in old[i].items() if k != "position"}]
60
+ moved = [i for i in new if i in old and new[i].get("position") != old[i].get("position")]
61
+ parts = []
62
+ if added:
63
+ parts.append(f"Added {len(added)} step{'s' * (len(added) > 1)}" if len(added) > 1
64
+ else f"Added {label(added[0])}")
65
+ if removed:
66
+ parts.append(f"removed {len(removed)} steps" if len(removed) > 1 else f"removed {label(removed[0])}")
67
+ if changed:
68
+ parts.append(f"changed {len(changed)} steps" if len(changed) > 1 else f"changed {label(changed[0])}")
69
+ if (before.get("edges") or []) != (after.get("edges") or []) and not (added or removed):
70
+ parts.append("rewired")
71
+ if (not parts and (moved or before.get("limits") != after.get("limits")
72
+ or before.get("inputs") != after.get("inputs")
73
+ or before.get("vars") != after.get("vars"))):
74
+ parts.append("Rearranged" if moved else "Changed its settings")
75
+ line = ", ".join(parts) or "Saved"
76
+ return line[0].upper() + line[1:]
@@ -0,0 +1,189 @@
1
+ """What a workflow graph should cost before it runs (GET /api/workflows/{id}/estimate): each node's range in dollars,
2
+ its model calls and who decides what it does, read from the node's own last runs when it has them, else from Mobster's
3
+ agent's estimate for its words. A loop multiplies its steps by its rounds when the run's rounds are known before it
4
+ starts; when they aren't (items, until), the estimate is for one round and says so (``partial``)."""
5
+
6
+ from statistics import median
7
+
8
+ from . import catalog
9
+
10
+ HISTORY_RUNS = 5
11
+ PHONE = frozenset({"phone.task", "phone.taught", "phone.check"})
12
+ CHARS_PER_TOKEN = 4
13
+ ASK_OUTPUT_TOKENS = (200, 2000) # an ai.ask answer, short to long, before it has run
14
+
15
+
16
+ def estimate(graph, *, history, agent, rounds, child=None, ask_cost=None, replays=None):
17
+ """Each node's cost range and model calls, and the total, from its history or the agent's estimate."""
18
+ by_id = {node["id"]: node for node in graph.get("nodes") or ()}
19
+ children = {}
20
+ for node in by_id.values():
21
+ children.setdefault(node.get("parent"), []).append(node["id"])
22
+ out = {}
23
+
24
+ def one(node):
25
+ kind, config = node["type"], node.get("config") or {}
26
+ past = (history.get(node["id"]) or [])[:HISTORY_RUNS]
27
+ if kind == "phone.task" and config.get("mode") == "replay":
28
+ return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "replay", "decides": "replays"}
29
+ if kind in ("phone.task", "phone.taught") and replays is not None and (decides := replays(node)):
30
+ return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "replay", "decides": decides}
31
+ if kind in PHONE or kind == "ai.ask":
32
+ if past:
33
+ costs, calls = [p["costUsd"] for p in past], [p.get("modelCalls") for p in past]
34
+ return {"lowUsd": round(min(costs), 4), "highUsd": round(max(costs), 4),
35
+ "modelCalls": None if None in calls else round(median(calls)), "basis": "history",
36
+ "runs": len(past), **({"decides": _decides(kind, past[0])} if kind in PHONE else {})}
37
+ if kind == "ai.ask":
38
+ return _ask(config, ask_cost)
39
+ words = config.get("text") or config.get("question") or ""
40
+ guess = agent(words, config.get("outputSchema")) if words or kind == "phone.taught" else None
41
+ entry = {"lowUsd": 0.0 if kind == "phone.taught" else (guess or {}).get("lowUsd"),
42
+ "highUsd": (guess or {}).get("highUsd"), "modelCalls": None, "basis": "estimate",
43
+ "decides": "taught" if kind == "phone.taught" else "model"}
44
+ if (guess or {}).get("billing") == "plan":
45
+ entry["billing"] = "plan"
46
+ return entry
47
+ if kind == "workflow.call":
48
+ total = child(node) if child is not None else None
49
+ if total is None:
50
+ return {"lowUsd": None, "highUsd": None, "modelCalls": None, "basis": "child"}
51
+ return {**{k: total.get(k) for k in ("lowUsd", "highUsd", "modelCalls")}, "basis": "child",
52
+ **({"partial": True} if total.get("partial") else {})}
53
+ if kind == "loop":
54
+ return _loop(node, rounds(node))
55
+ return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "none",
56
+ **({"decides": "code"} if kind == "app.read" else {})}
57
+
58
+ def _loop(node, count):
59
+ body = [walk(by_id[ident]) for ident in children.get(node["id"], ())]
60
+ for ident in children.get(node["id"], ()):
61
+ out[ident]["perRound"] = True
62
+ times = count if count is not None else 1
63
+ entry = _sum(body, times)
64
+ entry.update(basis="estimate", rounds=count)
65
+ if count is None:
66
+ entry["partial"] = True
67
+ return entry
68
+
69
+ def walk(node):
70
+ if node["id"] not in out:
71
+ out[node["id"]] = one(node)
72
+ return out[node["id"]]
73
+
74
+ top = [walk(node) for node in by_id.values() if node.get("parent") is None]
75
+ total = _sum(top, 1)
76
+ limit = (graph.get("limits") or {}).get("maxSpendUsd", catalog.DEFAULT_LIMITS["maxSpendUsd"])
77
+ total["overLimit"] = total["highUsd"] is not None and total["highUsd"] > limit
78
+ total.setdefault("partial", False)
79
+ return {"nodes": out, "total": total}
80
+
81
+
82
+ def of_workflow(owner, workflow, graph, depth=0):
83
+ from .. import engines
84
+ from ..api_errors import APIError
85
+ from ..model_clients import cost_usd
86
+ from . import nodes as graph_nodes
87
+ history = {}
88
+ for record in owner.batches.recent(workflow["id"], HISTORY_RUNS * 2):
89
+ for entry in owner.batches.nodes(record["id"]):
90
+ cost = entry.get("costUsd")
91
+ if entry.get("state") != "done" or entry.get("reused") or entry.get("pinned") \
92
+ or isinstance(cost, bool) or not isinstance(cost, (int, float)):
93
+ continue
94
+ history.setdefault(entry["node"], []).append(
95
+ {"costUsd": float(cost), "modelCalls": entry.get("modelCalls"), "replayed": entry.get("replayed")})
96
+ estimates = getattr(owner.runtime, "engine_estimates", None)
97
+
98
+ def agent(text, schema):
99
+ try:
100
+ # With no key for Mobster's agent a model step runs on the person's ready coding agent (Runtime.plan_agent,
101
+ # DR-56): on their plan, so it costs nothing per call, and the label says so instead of API prices (DR-33).
102
+ plan = getattr(owner.runtime, "plan_agent", None)
103
+ chosen = plan() if callable(plan) else None
104
+ if chosen:
105
+ return {"lowUsd": 0.0, "highUsd": 0.0, "billing": "plan", "agent": chosen}
106
+ except Exception: # noqa: BLE001 -- unknown: priced as Mobster's agent is, below
107
+ pass
108
+ try:
109
+ return estimates(text, schema)[engines.SMART] if callable(estimates) else None
110
+ except Exception: # noqa: BLE001 -- no estimate is said as unknown (None), never as free
111
+ return None
112
+
113
+ def child(node):
114
+ called = (node.get("config") or {}).get("workflowId")
115
+ if depth >= catalog.MAX_CALL_DEPTH or not called:
116
+ return None
117
+ try:
118
+ return owner.estimate(called, _depth=depth + 1)["total"]
119
+ except (APIError, LookupError):
120
+ return None
121
+
122
+ def rounds(node):
123
+ config = node.get("config") or {}
124
+ if config.get("over") == "times":
125
+ return config.get("times")
126
+ if config.get("over") == "rows":
127
+ return min(len(config["rows"]), config["max"]) if isinstance(config.get("rows"), list) else None
128
+ if config.get("over") == "phones":
129
+ try:
130
+ return len(graph_nodes.loop_phones(owner.runtime, config.get("phones"))[:config.get("max") or 1000])
131
+ except graph_nodes.NodeFailed:
132
+ return None
133
+ return None # items and until: told only as the run goes
134
+
135
+ def replays(node):
136
+ return replay_kind(owner, node)
137
+
138
+ return estimate(graph, history=history, agent=agent, rounds=rounds, child=child, replays=replays,
139
+ ask_cost=lambda model, tokens_in, tokens_out: cost_usd(
140
+ model, {"prompt_tokens": tokens_in, "completion_tokens": tokens_out}))
141
+
142
+
143
+ def replay_kind(owner, node):
144
+ from pathlib import Path
145
+
146
+ from ..learned.zero import replay_kind as kind_of
147
+ try:
148
+ state_db = getattr(getattr(owner.runtime, "config", None), "state_db", None)
149
+ if not state_db:
150
+ return None
151
+ config = node.get("config") or {}
152
+ goal = config.get("text")
153
+ if node.get("type") == "phone.taught":
154
+ with owner.journal.lock:
155
+ goal = (owner._get(config.get("workflowId")) or {}).get("goal")
156
+ if config.get("mode") == "agent":
157
+ return None
158
+ return kind_of(Path(state_db).parent, goal, learned=node.get("type") == "phone.task")
159
+ except Exception: # noqa: BLE001 -- unknown is priced as a model step, never as free
160
+ return None
161
+
162
+
163
+ def _decides(kind, newest):
164
+ if kind == "phone.taught":
165
+ return "taught"
166
+ return "replays" if newest.get("replayed") else "model"
167
+
168
+
169
+ def _ask(config, ask_cost):
170
+ from .nodes import AI_MODELS, AI_SYSTEM, INPUT_CHARS
171
+ model = AI_MODELS.get(config.get("model") or "haiku", AI_MODELS["haiku"])
172
+ base = (len(AI_SYSTEM) + len(str(config.get("instruction") or ""))) // CHARS_PER_TOKEN
173
+ low = ask_cost(model, base, ASK_OUTPUT_TOKENS[0]) if ask_cost is not None else None
174
+ high = ask_cost(model, base + INPUT_CHARS // CHARS_PER_TOKEN, ASK_OUTPUT_TOKENS[1]) if ask_cost is not None \
175
+ else None
176
+ return {"lowUsd": None if low is None else round(low, 4), "highUsd": None if high is None else round(high, 4),
177
+ "modelCalls": 1, "basis": "estimate"}
178
+
179
+
180
+ def _sum(entries, times):
181
+ """The entries' sum, ``times`` over; a part that can't be told (None) makes the whole None."""
182
+ out = {}
183
+ for key, digits in (("lowUsd", 4), ("highUsd", 4), ("modelCalls", None)):
184
+ values = [entry.get(key) for entry in entries]
185
+ out[key] = None if any(v is None for v in values) else \
186
+ (round(sum(values) * times, digits) if digits else sum(values) * times)
187
+ if any(entry.get("partial") for entry in entries):
188
+ out["partial"] = True
189
+ return out