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,691 @@
1
+ """Saved programs for Smart: a request with the same words as one that worked, but other values, repeats that
2
+ run with code doing the checking, and the model only where code can't.
3
+
4
+ Replaying only a run's navigation before the model starts (#215's procedure memory, removed 12 Oct 2026: off by
5
+ default, and its A/B left the median unchanged) still paid the contract call, every typing and commit turn and the
6
+ DONE turn. A saved program keeps the whole run:
7
+
8
+ - **Learning.** A Smart run that ended done (the task contract proved every item, a write only from screens after
9
+ it) is kept as a program:
10
+ - its request as a template: each typed text, each tapped control's name and each row a SCROLL_TO looked for that
11
+ appears in the request (case folded, whole words) is a blank ``{pN}``;
12
+ - its steps (replay_core path steps): the action, the app, the target's role and label, and the screen before as
13
+ hashed role/label pairs (``StepRecord.shape``; no screen text). A step may type only a blank's value or a
14
+ search typed from the request's own words; the steps end at the first one that types anything else, uses a
15
+ point, a macro or a skill (the model does it each run). A SCROLL_TO is kept only when the row it looked for is a
16
+ blank: "Open Settings › General › Fonts" scrolls to "Fonts" on the General page, and its siblings to their own
17
+ page (11 Oct, on an iPhone: the scroll ended the program, "Fonts" stayed literal and no sibling matched);
18
+ - its task contract's items, with each blank's value as its blank. An item whose quote or payload holds words
19
+ the template doesn't is not kept: then no contract is kept, and the run compiles one.
20
+ Never kept: answers, notes, screen text, text the model wrote, secrets.
21
+ - **Matching** (``match``): programs from the same start app and phone are candidates. Code accepts a match only
22
+ when every value is words of the new request and the filled template is the request (case, spacing and end
23
+ punctuation aside): typed values come only from the new request, and none runs across a clause (``crosses``). One
24
+ call to the run's model picks and fills only when code finds more than one way (below).
25
+ - **Replay** (``FrontierAgent(saved_program=...)``): the program's steps are decisions with no model call, through
26
+ the same per-action path as the model's (guards, the contract's commit check, approvals, the journal); each one
27
+ only on a screen like its recorded one with its target there exactly once. The contract is the program's, filled.
28
+ When a step doesn't fit, the model takes over with the program's remaining steps as a suggested plan; a run whose
29
+ contract is proven with nothing to report ends with no DONE call.
30
+ - **Trust.** A program is learned *provisional*: a contract can be proven on screen by a run whose write didn't land
31
+ (a CalTrack log that the app never kept, 11 Oct). Its first replay is supervised: the steps run, but the model's
32
+ DONE turn and the DONE gate decide the end, never code alone. It becomes *trusted* only after a replay that took
33
+ every step and whose run ended done with every contract item proven (a write only from screens after the replay's
34
+ own actions, #230). A provisional program that fails is forgotten at once; only a trusted one finishes in code.
35
+ - **Forgetting.** For a trusted program, a replay that stops before its last step or a run that doesn't end done is
36
+ a miss; missed twice in a row, it is forgotten. A done run after a replay is learned again.
37
+ - **Matching without a call.** When code fills exactly one template so that it is the request, with one way to split
38
+ the values, that's the match: no call. The call is made only when that's ambiguous (two templates, or two ways to
39
+ split one); when no template can be the request, there's no call either.
40
+
41
+ ``MOBSTER_PROGRAMS``: ``on`` learns and replays, ``learn`` only learns, ``off`` turns it off. Unset, it is on for runs
42
+ from Mobster for Mac, ``mobster serve``'s API, your coding agent's ``phone_task`` through ``mobster mcp`` and Mobster
43
+ Remote (DEFAULT_ON_ORIGINS) and off for the rest; any other value is off.
44
+ On by default since 11 Oct 2026: on an iPhone 15 Pro (iOS 26.0.1), exact repeats took 7.87 -> 3.91 s (Settings ›
45
+ General › About) and 4.37 -> 1.10 s (Clock's Stopwatch tab) at the median of 4 runs each, 16/16 passed, no unsafe act,
46
+ approvals unchanged; on iOS Simulators, code-graded siblings took 2.33 calls instead of 5.06 at the same pass rate.
47
+ Programs live in the data folder's ``replays/programs/`` (0700, files 0600), at most MAX_PROGRAMS.
48
+ """
49
+
50
+ import json
51
+ import os
52
+ import re
53
+ import time
54
+ from pathlib import Path
55
+
56
+ from ..replay_core.path import ELEMENT_OPS, SWIPES, TEXT_OPS, describe, first_of, new_path
57
+ from ..replay_core.recorder import typed_plainly
58
+ from ..replay_core.store import PROGRAM_ID, Cache, env_of
59
+
60
+ MAX_PROGRAMS = 100
61
+ MAX_TAUGHT = 100 # taught programs, kept apart: learned ones never push one out
62
+ MAX_NAME = 80
63
+ PROVISIONAL, TRUSTED = "provisional", "trusted"
64
+ MISSES_TO_FORGET = 2
65
+ MAX_CANDIDATES = 5
66
+ MIN_BLANK = 3 # a tapped name shorter than this never becomes a blank ("OK", "Go")
67
+ MIN_LITERAL = 8
68
+ ITEM_FIELDS = ("kind", "app", "what", "payload", "act", "count", "condition", "quote")
69
+ FREE_FIELDS = frozenset({"what", "payload", "condition", "quote"})
70
+ PROGRAM_OPS = ELEMENT_OPS | SWIPES | frozenset({"LAUNCH_APP", "HOME"})
71
+ BLANK = re.compile(r"\{(p\d+)\}")
72
+ ROW_SPLIT = re.compile(r",|\s[·•|–—-]\s|\n")
73
+
74
+
75
+ # Run origins where saved programs are on while MOBSTER_PROGRAMS is unset: Mobster for Mac, `mobster serve`'s API, a
76
+ # coding agent's `phone_task` through `mobster mcp` ("mcp") and Mobster Remote ("remote"). Every one of them is a run in
77
+ # Mobster's own agent, with the same approvals as the app's: a replayed step that sends, posts, deletes or buys asks
78
+ # like any other (the permission mode also rises to ask-important for "mcp" and "remote", permissions/policy.py).
79
+ DEFAULT_ON_ORIGINS = frozenset({"app", "api", "mcp", "remote"})
80
+ ORIGINS = frozenset({"app", "tui", "cli", "api", "mcp", "remote"})
81
+
82
+
83
+ def mode(env=None, origin=None):
84
+ env = os.environ if env is None else env
85
+ value = (env.get("MOBSTER_PROGRAMS") or "").strip().casefold()
86
+ if value in ("on", "learn", "off"):
87
+ return value
88
+ return "on" if not value and origin in DEFAULT_ON_ORIGINS else "off"
89
+
90
+
91
+ TAUGHT_ORIGINS = frozenset({"workflow", "schedule"})
92
+
93
+
94
+ def taught_on(env=None, origin=None):
95
+ env = os.environ if env is None else env
96
+ value = (env.get("MOBSTER_PROGRAMS") or "").strip().casefold()
97
+ return value not in ("off", "learn") and origin in (ORIGINS | TAUGHT_ORIGINS)
98
+
99
+
100
+ def programs_dir(data_dir):
101
+ return Path(data_dir) / "replays" / "programs" if data_dir else None
102
+
103
+
104
+ def normalized(text):
105
+ return " ".join(str(text or "").split())
106
+
107
+
108
+ def folded(text):
109
+ """For comparing a request with a filled template: case, spacing, quotes and end punctuation aside."""
110
+ text = normalized(text).casefold().translate(str.maketrans({"‘": "'", "’": "'", "“": '"',
111
+ "”": '"'}))
112
+ return text.rstrip(" .!?")
113
+
114
+
115
+ def blank_for(template, values, value):
116
+ value = normalized(value)
117
+ if len(value) < MIN_BLANK or "{" in value or "}" in value:
118
+ return template, None
119
+ for name, known in values.items():
120
+ if known.casefold() == value.casefold():
121
+ return template, name
122
+ found = re.search(r"(?<![\w{])" + re.escape(value) + r"(?![\w}])", template, flags=re.IGNORECASE)
123
+ if not found:
124
+ return template, None
125
+ name = f"p{len(values) + 1}"
126
+ values[name] = template[found.start():found.end()]
127
+ return template[:found.start()] + "{" + name + "}" + template[found.end():], name
128
+
129
+
130
+ def program_from(goal, steps):
131
+ template, values, out = normalized(goal), {}, []
132
+ for step in steps:
133
+ op = step.get("op")
134
+ if op == "SCROLL_TO":
135
+ # The row it looked for, when that row is the request's own words: a blank, as a typed value is.
136
+ sought = step.get("scroll")
137
+ if not (isinstance(sought, str) and typed_plainly(sought)):
138
+ break
139
+ template, name = blank_for(template, values, sought)
140
+ if name is None:
141
+ break # a row the request doesn't name: the model scrolls each run
142
+ screen = step.get("screen") if isinstance(step.get("screen"), dict) else {}
143
+ out.append({"op": op, "app": step.get("app"), "target": None, "param": name, "text": None,
144
+ "commit": None, "point": None, "live": False,
145
+ "screen": {"hash": str(screen.get("hash") or "")[:64],
146
+ **({"shape": list(screen["shape"])} if screen.get("shape") else {})}})
147
+ continue
148
+ if step.get("live") or op not in PROGRAM_OPS:
149
+ break
150
+ step = {key: value for key, value in step.items() if key not in ("param", "label_param", "label_prefix")}
151
+ target = dict(step.get("target") or {}) if isinstance(step.get("target"), dict) else None
152
+ if op in TEXT_OPS:
153
+ text = step.get("text")
154
+ if not (isinstance(text, str) and typed_plainly(text)):
155
+ break
156
+ template, name = blank_for(template, values, text)
157
+ if name is None:
158
+ break
159
+ step["param"], step["text"] = name, None
160
+ elif op in ("TAP", "LONG_PRESS") and target is not None and not step.get("commit"):
161
+ # A commit's control is never a blank: "Pay" in "pay Maya $12" is the act, not a value to swap.
162
+ label = target.get("label") or ""
163
+ template, name = blank_for(template, values, label)
164
+ if name is not None:
165
+ step["label_param"] = name
166
+ else:
167
+ # A row named by the request, then its details ("Banana Bread, Recipes, ·, …"): the name is a blank
168
+ # and the row is found again by a name that starts the same way (``fits``).
169
+ head = ROW_SPLIT.split(label, 1)[0].strip()
170
+ if head and head != label.strip():
171
+ template, name = blank_for(template, values, head)
172
+ if name is not None:
173
+ step["label_prefix"] = name
174
+ target["label"] = head
175
+ if target is not None:
176
+ target["id"] = target.get("id")
177
+ step["target"] = target
178
+ screen = step.get("screen") if isinstance(step.get("screen"), dict) else {}
179
+ # No screen text: the hash and the hashed pairs only (replay_core.path.screen_shape).
180
+ step["screen"] = {"hash": str(screen.get("hash") or "")[:64],
181
+ **({"shape": list(screen["shape"])} if screen.get("shape") else {})}
182
+ out.append(step)
183
+ if not out or len(BLANK.sub("", template).strip()) < MIN_LITERAL:
184
+ return None
185
+ for kept, step in zip(out, steps):
186
+ # Which values the screen before a step showed, by blank name (its text is dropped): a part ending before this
187
+ # step checks them again (``exit_holds``).
188
+ kept["shows"] = shown(values, (step.get("screen") or {}).get("signature"))
189
+ return template, out, values
190
+
191
+
192
+ def shown(values, pairs):
193
+ """The blanks whose values a screen's role/label pairs show."""
194
+ labels = [folded(label) for _, label in pairs or ()]
195
+ return sorted(name for name, value in values.items() if value and any(folded(value) in label for label in labels))
196
+
197
+
198
+ def contract_template(items, values, template):
199
+ """The items with blanks for their values, or None when an item holds words not in the template."""
200
+ out = []
201
+ words = set(re.findall(r"\w+", BLANK.sub(" ", template).casefold()))
202
+ for item in items or ():
203
+ entry = {}
204
+ for name in ITEM_FIELDS:
205
+ text = normalized(getattr(item, name, None) if not isinstance(item, dict) else item.get(name))
206
+ if name in FREE_FIELDS:
207
+ # Whole words only: "pay" is a blank's value, but never the "Pay" inside "SplitPay".
208
+ for blank, value in sorted(values.items(), key=lambda pair: -len(pair[1])):
209
+ text = re.sub(r"(?<![\w{])" + re.escape(value) + r"(?![\w}])", "{" + blank + "}", text,
210
+ flags=re.IGNORECASE)
211
+ entry[name] = text
212
+ for name in ("quote", "payload"):
213
+ rest = set(re.findall(r"\w+", BLANK.sub(" ", entry[name]).casefold()))
214
+ if not rest <= words:
215
+ return None
216
+ out.append(entry)
217
+ return out
218
+
219
+
220
+ def fill(text, values):
221
+ return BLANK.sub(lambda found: values.get(found.group(1), found.group(0)), text or "")
222
+
223
+
224
+ class Programs:
225
+ """Programs in ``directory``: replay_core paths of kind "program"."""
226
+
227
+ def __init__(self, directory, version=None):
228
+ self.directory = Path(directory)
229
+ self.cache = Cache(directory)
230
+ # The app's version when the route corpus is on (compiled_read/routes.py): saved under it, offered for it.
231
+ self.version = version
232
+
233
+ @staticmethod
234
+ def identity(app, template, device):
235
+ return {"kind": "program", "app": app or None, "template": template, "device": device or None}
236
+
237
+ def all(self):
238
+ from ..replay_core.path import PathError, validate
239
+ out = []
240
+ try:
241
+ files = list(self.directory.glob("*.json"))
242
+ except OSError:
243
+ return out
244
+ for file in files:
245
+ try:
246
+ path = validate(json.loads(file.read_text(encoding="utf-8")))
247
+ except (OSError, ValueError, PathError):
248
+ continue
249
+ if (path.get("identity") or {}).get("kind") == "program":
250
+ out.append(path)
251
+ return sorted(out, key=lambda p: -float(p.get("used_at") or p.get("created_at") or 0))
252
+
253
+ def candidates(self, app, device, goal, limit=MAX_CANDIDATES, taught_only=False):
254
+ """Programs started in ``app`` on ``device``, the ones whose template holds most of ``goal``'s words first."""
255
+ words = set(re.findall(r"\w+", folded(goal)))
256
+ found = []
257
+ for path in self.all():
258
+ identity = path.get("identity") or {}
259
+ if taught_only and not path.get("taught"):
260
+ continue
261
+ if identity.get("app") != (app or None) and not path.get("taught") \
262
+ or identity.get("device") != (device or None):
263
+ continue
264
+ if self.version is not None and (path.get("env") or {}).get("app_version") not in (None, self.version):
265
+ continue
266
+ own = set(re.findall(r"\w+", folded(BLANK.sub(" ", identity.get("template") or ""))))
267
+ found.append((len(own & words) / max(1, len(own)), path))
268
+ found.sort(key=lambda pair: -pair[0])
269
+ return [path for _, path in found[:limit]]
270
+
271
+ def save(self, app, goal, device, steps, items, *, run_id, trusted=None, final_pairs=None, first=None):
272
+ """Keep a done run's program: trusted only as the program this run proved or one already trusted."""
273
+ made = program_from(goal, steps)
274
+ if made is None:
275
+ return None
276
+ template, steps, values = made
277
+ identity = self.identity(app, template, device)
278
+ env = env_of(self.version, None, device)
279
+ path = new_path(steps, kind="program", identity=identity, env=env, source_run=run_id)
280
+ path["contract"] = contract_template(items, values, template)
281
+ path["blanks"] = sorted(values)
282
+ if final_pairs:
283
+ from ..replay_core.path import screen_shape, signature_hash
284
+ path["final"] = {"hash": signature_hash(final_pairs), "shape": screen_shape(final_pairs),
285
+ "shows": shown(values, final_pairs)}
286
+ path["used_at"], path["misses"] = round(time.time(), 3), 0
287
+ try:
288
+ # Learned again after a replay: the count of replays and the trust carry over.
289
+ previous = self.cache.load(identity, env)
290
+ except (OSError, ValueError):
291
+ previous = None
292
+ if isinstance(previous, dict) and previous.get("taught"):
293
+ # What the person taught stays as they taught it: a run that replayed it settles it (``note``), and never
294
+ # replaces its steps with the run's own (which end at the first coordinate step).
295
+ return None
296
+ if isinstance(previous, dict):
297
+ path["replays"] = int(previous.get("replays") or 0)
298
+ path["parts"] = previous.get("parts") or {}
299
+ kept = first_of(previous if isinstance(previous, dict) else None, first)
300
+ if kept is not None:
301
+ path["first"] = kept
302
+ proven = trusted is not None and dict(trusted) == identity
303
+ path["status"] = TRUSTED if proven or (isinstance(previous, dict) and previous.get("status") == TRUSTED) \
304
+ else PROVISIONAL
305
+ saved = self.cache.save(path)
306
+ for old in [other for other in self.all() if not other.get("taught")][MAX_PROGRAMS:]:
307
+ self.forget(old)
308
+ return saved
309
+
310
+ def note(self, path, *, missed, run_id=None):
311
+ """After a replay."""
312
+ path = dict(path)
313
+ if path.get("taught"):
314
+ return self._note_taught(path, missed=missed, run_id=run_id)
315
+ if path.get("status") != TRUSTED:
316
+ if missed:
317
+ self.forget(path)
318
+ return None
319
+ path["status"] = TRUSTED
320
+ path["misses"] = int(path.get("misses") or 0) + 1 if missed else 0
321
+ path["replays"] = int(path.get("replays") or 0) + 1
322
+ path["used_at"], path["last_run"] = round(time.time(), 3), run_id
323
+ if path["misses"] >= MISSES_TO_FORGET:
324
+ self.forget(path)
325
+ return None
326
+ try:
327
+ self.cache.save(path)
328
+ except (OSError, ValueError):
329
+ pass
330
+ return path
331
+
332
+ def _note_taught(self, path, *, missed, run_id):
333
+ try:
334
+ current = self.cache.load(path.get("identity") or {}, path.get("env") or {}) or path
335
+ except (OSError, ValueError):
336
+ current = path
337
+ current = dict(current)
338
+ current["replays"] = int(current.get("replays") or 0) + 1
339
+ current["misses"] = int(current.get("misses") or 0) + 1 if missed else 0
340
+ if not missed:
341
+ current["status"] = TRUSTED
342
+ elif current["misses"] >= MISSES_TO_FORGET:
343
+ current["status"], current["misses"] = PROVISIONAL, 0
344
+ current["used_at"], current["last_run"] = round(time.time(), 3), run_id
345
+ current["last"] = {"run": run_id, "at": round(time.time(), 3), "ok": not missed}
346
+ try:
347
+ self.cache.save(current)
348
+ except (OSError, ValueError):
349
+ pass
350
+ return current
351
+
352
+ def forget(self, path):
353
+ return self.cache.forget(path.get("identity") or {}, path.get("env") or {})
354
+
355
+ @staticmethod
356
+ def id_of(path):
357
+ from ..replay_core.store import file_name
358
+ return file_name(path.get("identity") or {}, path.get("env") or {})[:-len(".json")]
359
+
360
+ def by_id(self, program_id):
361
+ if not isinstance(program_id, str) or not PROGRAM_ID.fullmatch(program_id):
362
+ return None
363
+ return next((path for path in self.all() if self.id_of(path) == program_id), None)
364
+
365
+ def rename(self, program_id, name):
366
+ name = " ".join(str(name or "").split())
367
+ if not name or len(name) > MAX_NAME:
368
+ raise ValueError(f"A program's name is 1 to {MAX_NAME} characters")
369
+ path = self.by_id(program_id)
370
+ if path is None:
371
+ raise LookupError("No saved program has that id")
372
+ path = dict(path)
373
+ path["name"] = name
374
+ self.cache.save(path)
375
+ return path
376
+
377
+ def note_part(self, path, key, outcome, *, trusted):
378
+ path = self.cache.load(path.get("identity") or {}, path.get("env") or {})
379
+ if path is None:
380
+ return None
381
+ stats = dict((path.get("parts") or {}).get(key) or {})
382
+ stats["hits"] = int(stats.get("hits") or 0) + (outcome == "hit")
383
+ stats["misses"] = int(stats.get("misses") or 0) + 1 if outcome == "miss" else 0
384
+ stats["status"] = TRUSTED if outcome == "hit" or trusted and stats["misses"] < MISSES_TO_FORGET else "forgotten"
385
+ path["parts"] = {**(path.get("parts") or {}), key: stats}
386
+ try:
387
+ self.cache.save(path)
388
+ except (OSError, ValueError):
389
+ pass
390
+ return stats
391
+
392
+
393
+ def learn(store, *, app, goal, device, steps, items, done, run_id=None, trusted=None, origin=None, final_pairs=None,
394
+ first=None):
395
+ """Keep a run as a program when it ended done; returns the saved path or None."""
396
+ if not done or mode(origin=origin) == "off" or not steps:
397
+ return None
398
+ try:
399
+ return store.save(app, goal, device, steps, items, run_id=run_id, trusted=trusted, final_pairs=final_pairs,
400
+ first=first)
401
+ except (OSError, ValueError):
402
+ return None
403
+
404
+
405
+ def public(path, workflows=()):
406
+ """A program as the Programs list shows it: no screen text, no step details beyond plain lines."""
407
+ from ..replay_core.path import describe
408
+ identity = path.get("identity") or {}
409
+ template = identity.get("template") or ""
410
+ example = path.get("example") if isinstance(path.get("example"), dict) else {}
411
+ taught = path.get("taught") if isinstance(path.get("taught"), dict) else None
412
+ linked = [workflow["id"] for workflow in workflows if isinstance(workflow, dict)
413
+ and code_fills(template, workflow_words(workflow))]
414
+ last = path.get("last") if isinstance(path.get("last"), dict) else None
415
+ return {"id": Programs.id_of(path), "name": path.get("name") or template, "template": template,
416
+ "request": fill(template, example) if example else template,
417
+ "blanks": [{"name": name, "example": example.get(name)} for name in path.get("blanks") or ()],
418
+ "taught": taught is not None, "status": path.get("status") or PROVISIONAL,
419
+ "steps": len(path.get("steps") or ()), "coordinateSteps": (taught or {}).get("points", 0),
420
+ "asks": sum(1 for step in path.get("steps") or () if step.get("commit")),
421
+ "lines": [describe({**step, "text": None}) for step in (path.get("steps") or ())[:MAX_LINES]],
422
+ "replays": int(path.get("replays") or 0), "device": identity.get("device"),
423
+ "createdAt": _ms(path.get("created_at")), "usedAt": _ms(path.get("used_at")),
424
+ "last": {"ok": bool(last.get("ok")), "at": _ms(last.get("at"))} if last else None,
425
+ "first": first_of(path), "workflowIds": linked}
426
+
427
+
428
+ MAX_LINES = 12
429
+
430
+
431
+ def _ms(seconds):
432
+ return round(float(seconds) * 1000) if isinstance(seconds, (int, float)) and seconds else None
433
+
434
+
435
+ def workflow_words(workflow):
436
+ """A saved workflow's words as its run sends them with each blank's default (starters.fill)."""
437
+ from ..workflows import starters
438
+ try:
439
+ return starters.fill(workflow.get("goal") or "", None)
440
+ except Exception: # noqa: BLE001
441
+ return workflow.get("goal") or ""
442
+
443
+
444
+ def settle(store, *, matched, done, replayed_all, run_id=None):
445
+ """After a run: note its replayed program (a miss unless it took every step and the run ended done)."""
446
+ if matched is None:
447
+ return None
448
+ proven = bool(done and replayed_all)
449
+ store.note(matched.path, missed=not proven, run_id=run_id)
450
+ return dict(matched.path.get("identity") or {}) if proven else None
451
+
452
+
453
+ MATCH_SYSTEM = """You match a phone task request to saved programs: each is the template of a request that was \
454
+ done before, with blanks like {p1} where that request had its own values. A program fits when the new request is \
455
+ its template with other values in the blanks: the same task, the same words, other values. Pick the one that fits \
456
+ and fill each of its blanks with the new request's words, copied exactly. If none fits, answer null."""
457
+
458
+ MATCH_SCHEMA = {"type": "object", "additionalProperties": False, "required": ["program", "values"], "properties": {
459
+ "program": {"type": ["integer", "null"]},
460
+ "values": {"type": "array", "items": {"type": "object", "additionalProperties": False,
461
+ "required": ["blank", "value"],
462
+ "properties": {"blank": {"type": "string"},
463
+ "value": {"type": "string"}}}}}}
464
+
465
+
466
+ def blank_roles(path):
467
+ """How each blank is used, for the matching call: 'typed into "Title"', 'tapped "…" (Button)'."""
468
+ roles = {}
469
+ for step in path.get("steps") or ():
470
+ target = step.get("target") or {}
471
+ if step.get("param"):
472
+ roles.setdefault(step["param"],
473
+ f'typed into the {target.get("role") or "field"} "{target.get("label") or ""}"')
474
+ if step.get("label_param"):
475
+ roles.setdefault(step["label_param"], f'the name of a {target.get("role") or "control"} it tapped')
476
+ if step.get("label_prefix"):
477
+ roles.setdefault(step["label_prefix"], f'the name of a list row ({target.get("role") or "row"}) it opened')
478
+ return roles
479
+
480
+
481
+ class Matched:
482
+ """A program filled for one request: its steps and contract items with the request's values."""
483
+
484
+ def __init__(self, path, values):
485
+ self.path, self.values = path, dict(values)
486
+ self.steps = filled_steps(path, values)
487
+ contract = path.get("contract")
488
+ if not contract and path.get("taught"):
489
+ # A program taught before its contract was made at save time: made now, by code, from its lesson.
490
+ from .taught import lesson_items
491
+ contract = lesson_items((path.get("identity") or {}).get("template") or "", path.get("steps") or [],
492
+ path.get("final"))
493
+ self.items = ([{name: fill(entry.get(name), values) for name in ITEM_FIELDS} for entry in contract]
494
+ if isinstance(contract, list) and contract else None)
495
+ # Only a trusted program finishes in code; a provisional one's first replay ends through the model's DONE turn.
496
+ self.trusted = path.get("status") == TRUSTED
497
+ # Taught by hand on the live view (save_from_demo): the run says so, by the program's name.
498
+ self.taught = bool(path.get("taught"))
499
+ self.name = path.get("name") or None
500
+ # A practice run (the person watches and answers each step): ``ask`` is the run's approval request, and
501
+ # their corrections are kept here, then applied to the program after the run (apply_corrections).
502
+ self.practice, self.ask, self.corrections = False, None, []
503
+ self.paused_at, self.rejoins, self.helped = None, 0, False
504
+ self.deciding = False
505
+ self.at = 0
506
+ self.stopped = None
507
+ self.expect = None
508
+
509
+ @staticmethod
510
+ def read_by_code(snapshot, request):
511
+ """A question the screen in front answers, read by code (learned/code_read.py), else None."""
512
+ from .code_read import answer
513
+ return answer(snapshot, request)
514
+
515
+ @property
516
+ def template(self):
517
+ return (self.path.get("identity") or {}).get("template") or ""
518
+
519
+ # How FrontierAgent runs a saved program's steps: it matches each to the screen and words it with these, so the
520
+ # engine needs nothing from this package.
521
+ @staticmethod
522
+ def fits(step, snapshot):
523
+ return fits(step, snapshot)
524
+
525
+ @staticmethod
526
+ def describe(step):
527
+ return describe(step)
528
+
529
+ @staticmethod
530
+ def correction(snapshot, x, y, among=None):
531
+ from ..replay_core.path import target_of
532
+ from .teach import element_at, screen_of
533
+ under = element_at(snapshot, x, y, among=among)
534
+ screen, _ = screen_of(snapshot)
535
+ if under is None:
536
+ screen["fingerprint"] = snapshot.content_fingerprint
537
+ return ({"op": "TAP" if under is not None else "TAP_XY", "target": target_of(under),
538
+ "point": None if under is not None else [round(x, 4), round(y, 4)], "screen": screen}, under)
539
+
540
+ def variant(self, step, snapshot):
541
+ alts = step.get("alts") or ()
542
+ if not alts:
543
+ return step
544
+ from ..replay_core.engine import screen_likeness
545
+ best, score = step, -1.0
546
+ try:
547
+ for candidate in [step] + [{**step, **alt, "alts": None} for alt in alts]:
548
+ if fits(candidate, snapshot)[0]:
549
+ likeness = screen_likeness(candidate, snapshot)
550
+ if likeness > score:
551
+ best, score = candidate, likeness
552
+ except Exception: # noqa: BLE001 -- the step as taught: its own check decides
553
+ return step
554
+ return best
555
+
556
+ def remaining(self, limit=8):
557
+ """The steps not taken yet, as plain lines (no typed text): the model's suggested plan when it takes over."""
558
+ left = self.steps[self.at:]
559
+ lines = [describe({**step, "text": None}) for step in left[:limit]]
560
+ return "; ".join(lines) + ("; …" if len(left) > limit else "")
561
+
562
+
563
+ def filled_steps(path, values):
564
+ out = []
565
+ for step in path.get("steps") or ():
566
+ step = dict(step)
567
+ if isinstance(step.get("purpose"), str):
568
+ step["purpose"] = fill(step["purpose"], values)
569
+ name = step.get("label_param") or step.get("label_prefix")
570
+ if name and values.get(name):
571
+ target = dict(step.get("target") or {})
572
+ target["label"], target["id"] = values[name], None
573
+ step["target"] = target
574
+ out.append(step)
575
+ return out
576
+
577
+
578
+ def fits(step, snapshot):
579
+ from ..replay_core.engine import WEAK, match, plain, screen_likeness
580
+ if not step.get("label_prefix"):
581
+ return match(step, snapshot, settled=True)
582
+ target = step.get("target") or {}
583
+ want, role = plain(target.get("label")), target.get("role") or ""
584
+ app, now = step.get("app"), getattr(snapshot, "bundle_id", "") or ""
585
+ if app and now and app != now:
586
+ return False, None, f"{now} is in front, not {app}"
587
+ found = []
588
+ for element in getattr(snapshot, "elements", ()) or ():
589
+ head = ROW_SPLIT.split(normalized(element.label), 1)[0].strip().casefold()
590
+ if want and head == want and (not role or element.role == role):
591
+ found.append(element)
592
+ if len(found) != 1:
593
+ return False, None, (f'no single row named "{target.get("label")}" is on the screen')
594
+ likeness = screen_likeness(step, snapshot)
595
+ if likeness < WEAK:
596
+ return False, None, f"the screen is different ({round(likeness * 100)}% alike)"
597
+ return True, found[0], ""
598
+
599
+
600
+ # A value never runs across a clause: a quote mark (not an apostrophe in a word), ";", a sentence end, or "then"
601
+ # outside quotes. A longer request once filled a shorter template's last blank with its other clauses (12 Oct 2026).
602
+ CLAUSE = re.compile(r"(?<![^\W_])['\"‘’“”]|['\"‘’“”](?![^\W_])|;|[.!?](?=\s)")
603
+
604
+
605
+ def crosses(template, name, value):
606
+ quoted = re.search(r"['\"‘’“”]\{" + name + r"\}['\"‘’“”]", template)
607
+ return bool(CLAUSE.search(value or "") or not quoted and re.search(r"\bthen\b", value or "", re.I))
608
+
609
+
610
+ def code_fills(template, goal):
611
+ """The ways code fills ``template`` into ``goal``; a value crossing a clause is no fill: the run is the model's."""
612
+ template, goal = normalized(template).rstrip(" .!?"), normalized(goal).rstrip(" .!?")
613
+ parts = re.split(r"(\{p\d+\})", template)
614
+ if not BLANK.search(template):
615
+ return [{}] if folded(template) == folded(goal) else []
616
+ fills = []
617
+ for lazy in (True, False):
618
+ pattern = ""
619
+ for part in parts:
620
+ found = re.fullmatch(r"\{(p\d+)\}", part)
621
+ pattern += f"(?P<{found.group(1)}>.+{'?' if lazy else ''})" if found else re.escape(part)
622
+ hit = re.fullmatch(pattern, goal, flags=re.IGNORECASE | re.S)
623
+ if hit:
624
+ fill_ = {name: normalized(value) for name, value in hit.groupdict().items()}
625
+ if fill_ not in fills and not any(crosses(template, name, v) for name, v in fill_.items()):
626
+ fills.append(fill_)
627
+ return fills
628
+
629
+
630
+ def match(client, store, *, goal, app, device, secret=None, timeout=30, taught_only=False):
631
+ """(Matched, usage) for ``goal``, or (None, usage)."""
632
+ fillable = []
633
+ for path in store.candidates(app, device, goal, taught_only=taught_only):
634
+ fills = code_fills((path.get("identity") or {}).get("template") or "", goal)
635
+ if fills:
636
+ fillable.append((path, fills))
637
+ if not fillable:
638
+ return None, {}
639
+ if len(fillable) == 1 and len(fillable[0][1]) == 1:
640
+ return accept(fillable[0][0], fillable[0][1][0], goal, secret=secret), {}
641
+ candidates = [path for path, _ in fillable]
642
+ listing = []
643
+ for index, path in enumerate(candidates):
644
+ template = (path.get("identity") or {}).get("template") or ""
645
+ roles = blank_roles(path)
646
+ uses = "; ".join(f"{{{name}}}: {roles.get(name, 'a value')}" for name in path.get("blanks") or ())
647
+ listing.append(f"{index}. {template}" + (f" ({uses})" if uses else ""))
648
+ messages = [{"role": "system", "content": MATCH_SYSTEM}, {"role": "user", "content": [
649
+ {"type": "text", "text": f"Request: {normalized(goal)}\n\nPrograms:\n" + "\n".join(listing)}]}]
650
+ try:
651
+ out, usage = client.complete(messages, MATCH_SCHEMA, timeout=timeout)
652
+ except Exception:
653
+ return None, {}
654
+ pick = (out or {}).get("program")
655
+ if not isinstance(pick, int) or isinstance(pick, bool) or not 0 <= pick < len(candidates):
656
+ return None, usage
657
+ path = candidates[pick]
658
+ values = {}
659
+ for pair in (out or {}).get("values") or ():
660
+ if isinstance(pair, dict) and isinstance(pair.get("blank"), str) and isinstance(pair.get("value"), str):
661
+ values[pair["blank"].strip("{} ")] = normalized(pair["value"])
662
+ return accept(path, values, goal, secret=secret), usage
663
+
664
+
665
+ def accept(path, values, goal, *, secret=None):
666
+ """A Matched when ``values`` fill the template into ``goal``, each the request's words and no secret; else None."""
667
+ template = (path.get("identity") or {}).get("template") or ""
668
+ names = set(BLANK.findall(template))
669
+ if set(values) != names:
670
+ return None
671
+ asked = folded(goal)
672
+ for name, value in values.items():
673
+ if not value or folded(value) not in asked or (secret is not None and secret(value)) \
674
+ or crosses(template, name, value):
675
+ return None
676
+ if folded(fill(template, values)) != asked:
677
+ return None
678
+ return Matched(path, values)
679
+
680
+
681
+ def holds_secret(value):
682
+ """True for a value secret_filter would mask: it never fills a blank."""
683
+ try:
684
+ from ..secret_filter import redact
685
+ return redact(value) != value
686
+ except Exception:
687
+ return True
688
+
689
+
690
+ __all__ = ["Programs", "programs_dir", "public", "code_fills", "workflow_words", "Matched", "PROVISIONAL", "TRUSTED",
691
+ "mode", "taught_on"]