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,740 @@
1
+ """`mobster chat`: talk to Mobster's agent in a conversation, from Terminal."""
2
+
3
+ import json
4
+ import queue
5
+ import socket
6
+ import sys
7
+ import threading
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from ..narrate import approval_title, verbs
12
+ from .chat_once import EXIT_COULDNT, EXIT_DONE, once, proposals_of, routed_as, where_to_remember, workflow_once
13
+ from .client import NO_THREADS, NOT_RUNNING, Client, NoService, ServiceError
14
+ from .recent import last_thread, remember_thread
15
+ from .term import Term, isatty
16
+
17
+ ATTACH_MAX = 25 * 1024 * 1024
18
+ HELP = ("/new start a new conversation /threads your recent conversations\n"
19
+ "/open ID continue one /stop stop the task /pause pause or continue it\n"
20
+ "/attach PATH add a file to your next message /quit leave (the task goes on)")
21
+ # A route the running Mobster doesn't have (404).
22
+ NO_PAUSE = "This Mobster can't pause a task yet."
23
+ NO_FILES = "This Mobster can't take files yet."
24
+ DECISIONS = {"approved": "Approved", "denied": "Declined", "redirected": "Declined; Mobster does what you said "
25
+ "instead", "timeout": "Nobody answered in time, so Mobster didn't do it", "stopped": "Stopped",
26
+ "answered": "Answered"}
27
+
28
+
29
+ def add_arguments(parser, helpers):
30
+ bounded = helpers.get("bounded")
31
+ parser.add_argument("message", nargs="*", metavar="MESSAGE",
32
+ help="what to ask Mobster's agent; leave it out for a conversation in this terminal")
33
+ which = parser.add_mutually_exclusive_group()
34
+ which.add_argument("--thread", metavar="ID", help="continue this conversation (default: the one this terminal "
35
+ "used last)")
36
+ which.add_argument("--new", action="store_true", help="start a new conversation")
37
+ parser.add_argument("--device", metavar="NAME",
38
+ help="the iPhone or simulator: an id, UDID or name (default: the conversation's)")
39
+ parser.add_argument("--json", action="store_true",
40
+ help="print one JSON object when the task finishes or needs you; never answers an approval")
41
+ parser.add_argument("--keep-waiting", action="store_true",
42
+ help="with --json and the app or mobster serve running: wait while the task waits for you, "
43
+ "until it ends or --wait runs out")
44
+ parser.add_argument("--wait", type=bounded(float, 0, 3600) if bounded else float, default=None,
45
+ metavar="SECONDS", help="with the app or mobster serve running: how long to follow a task "
46
+ "(default 300); it goes on after")
47
+ parser.add_argument("--url", metavar="URL", help="Mobster's address (default: $MOBSTER_URL, else the running "
48
+ "Mac app's, else http://127.0.0.1:8765)")
49
+ parser.add_argument("--workflow", metavar="NAME", help=(
50
+ "run a saved workflow (its name or id) instead of a "
51
+ "message: in a new conversation, its own one, or --thread ID"))
52
+ parser.add_argument("--set", action="append", default=[], metavar="BLANK=VALUE",
53
+ help="with --workflow: a value for one of its blanks; repeat it for each")
54
+
55
+
56
+ class Chat:
57
+ def __init__(self, client, term, *, thread_id=None, device=None, color=None, interactive=True):
58
+ from ..style import Palette, color_enabled
59
+ self.client, self.term = client, term
60
+ self.thread_id, self.device = thread_id, device
61
+ self.paint = Palette(color_enabled(term.stdout) if color is None else color)
62
+ self.interactive = interactive
63
+ self.events = queue.Queue()
64
+ self.run_id = None # the task being followed
65
+ self.pending = None # the run's approval or question, as GET /api/runs/{id} shows it
66
+ self.paused = False
67
+ self.attachments = [] # [(id, name)] for the next message
68
+ self.renderers = {}
69
+ self.outcomes = set() # runs whose outcome is printed
70
+ self.finished = {} # run id -> the run, once finished
71
+ self.followers = {}
72
+ self.quit = False
73
+ self.held = "" # the line you were typing when an approval came; back once it's answered
74
+ self.offers = [] # suggestions to remember that wait for y or n here, oldest first
75
+ self.escape = None # inside a terminal key sequence (an arrow key): None, "esc", "csi" or "ss3"
76
+
77
+ # -- printing
78
+
79
+ def say(self, text, *roles):
80
+ self.term.print(self.paint(text, *roles) if roles else text)
81
+
82
+ def note(self, text, glyph="!", role="amber", indent=" "):
83
+ """A line whose glyph carries the colour and whose words stay the terminal's own colour, so it reads on
84
+ light and dark profiles alike."""
85
+ self.term.print(indent + self.paint(glyph, role, "bold") + " " + text)
86
+
87
+ def keys(self, *pairs):
88
+ """"[y] Send [n] Don't send" with each key in bold."""
89
+ return " ".join(self.paint(f"[{key}]", "bold") + f" {label}" for key, label in pairs)
90
+
91
+ def renderer(self, run_id):
92
+ renderer = self.renderers.get(run_id)
93
+ if renderer is None:
94
+ from ..console import ConsoleRenderer
95
+ term = self.term
96
+
97
+ class Lines(ConsoleRenderer):
98
+ def line(self, text=""):
99
+ term.print(text)
100
+ renderer = self.renderers[run_id] = Lines(term.stdout, color=bool(self.paint.codes), live=False)
101
+ return renderer
102
+
103
+ # -- following a task
104
+
105
+ def follow(self, run_id):
106
+ """Stream the task's events into the queue (a thread of its own), from the start."""
107
+ self.run_id = run_id
108
+ if run_id in self.followers:
109
+ return
110
+ thread = threading.Thread(target=self._stream, args=(run_id,), daemon=True, name=f"mobster-chat-{run_id}")
111
+ self.followers[run_id] = thread
112
+ thread.start()
113
+
114
+ def _stream(self, run_id):
115
+ last, tries = None, 0
116
+ while True:
117
+ try:
118
+ for event in self.client.events(f"/api/runs/{run_id}/events", last, timeout=30):
119
+ tries = 0
120
+ if event is None:
121
+ continue
122
+ last = event.get("seq", last)
123
+ self.events.put(("event", run_id, event))
124
+ if event.get("event") == "run_finished":
125
+ self.events.put(("end", run_id, None))
126
+ return
127
+ return self._ended(run_id)
128
+ except (socket.timeout, TimeoutError):
129
+ continue
130
+ except ServiceError as error:
131
+ self.events.put(("lost", run_id, str(error)))
132
+ return
133
+ except NoService:
134
+ tries += 1
135
+ if tries > 5:
136
+ self.events.put(("lost", run_id, "Lost the connection to Mobster."))
137
+ return
138
+ time.sleep(min(2.0, 0.2 * tries))
139
+
140
+ def _ended(self, run_id):
141
+ self.events.put(("end", run_id, None))
142
+
143
+ def pump(self, timeout=0.0):
144
+ """Handle what the followers queued; waits up to ``timeout`` for the first."""
145
+ try:
146
+ item = self.events.get(timeout=timeout) if timeout else self.events.get_nowait()
147
+ except queue.Empty:
148
+ return
149
+ while True:
150
+ self.handle(*item)
151
+ try:
152
+ item = self.events.get_nowait()
153
+ except queue.Empty:
154
+ return
155
+
156
+ def handle(self, kind, run_id, event):
157
+ if kind == "lost":
158
+ self.note(str(event), indent="")
159
+ if run_id == self.run_id:
160
+ self.run_id = None
161
+ return
162
+ if kind == "end":
163
+ if run_id not in self.finished:
164
+ try:
165
+ self.finished[run_id] = self.client.run(run_id)
166
+ except (ServiceError, NoService):
167
+ self.finished[run_id] = {"status": "error"}
168
+ if run_id == self.run_id:
169
+ self.run_id, self.pending, self.paused = None, None, False
170
+ self.give_back()
171
+ return
172
+ name = event.get("event")
173
+ renderer = self.renderer(run_id)
174
+ if name == "step" and isinstance(event.get("text"), str):
175
+ # Mobster's agent says each step in a sentence ("Typed “Running late” in Message").
176
+ self.say(" " + self.paint("●", "green") + " " + event["text"])
177
+ return
178
+ if name == "plan" and isinstance(event.get("line"), str) and event["line"]:
179
+ self.say(f" {event['line']}", "faint")
180
+ return
181
+ if name in ("result", "run_finished"):
182
+ summary = event if name == "result" else (event.get("summary") if isinstance(event.get("summary"),
183
+ dict) else {})
184
+ if run_id not in self.outcomes:
185
+ self.outcomes.add(run_id)
186
+ renderer._flush_steps()
187
+ self.outcome({**summary, "status": summary.get("status") or event.get("status") or "error"})
188
+ return
189
+ if name not in ("approval_requested", "approval_resolved", "user_message", "steer_applied", "steer_unread"):
190
+ renderer(event) # the run's events, one line per step (console.py)
191
+ return
192
+ renderer.narrator.feed(event)
193
+ if name == "approval_requested":
194
+ self.ask(run_id, event)
195
+ elif name == "approval_resolved":
196
+ self.pending = None
197
+ decision = str(event.get("decision") or "")
198
+ text = DECISIONS.get(decision) or ("Answered" if decision.startswith("choice:") else decision)
199
+ self.say(f" ⎿ {text}", "faint")
200
+ self.give_back()
201
+ elif name == "steer_applied":
202
+ self.say(" – Mobster read your message", "faint")
203
+ elif name == "steer_unread":
204
+ self.note("Mobster finished before reading your message. Send it again to run it as a new task.")
205
+ elif name == "user_message" and event.get("source") not in ("cli",):
206
+ where = {"app": "in Mobster", "voice": "by voice", "tui": "in the terminal UI", "mcp": "from your agent"}
207
+ self.say(f" ↳ You wrote {where.get(event.get('source'), 'elsewhere')}: “{event.get('text')}”")
208
+
209
+ def outcome(self, summary):
210
+ """The task's end: its status, time and cost, then its answer (or why it stopped)."""
211
+ from ..narrate import completion_message, seconds, status_glyph, status_label, status_tone
212
+ status = summary.get("status") or "error"
213
+ tone = status_tone(status)
214
+ role = {"success": "green", "error": "red", "warning": "amber", "review": "accent"}.get(tone, "muted")
215
+ glyph = status_glyph(tone)
216
+ facts = [seconds(summary.get("elapsed_ms"))] if summary.get("elapsed_ms") else []
217
+ cost = summary.get("costUsd")
218
+ if isinstance(cost, (int, float)) and cost > 0:
219
+ facts.append(f"${cost:.4f}" if cost < 0.1 else f"${cost:.2f}")
220
+ self.say(self.paint(glyph, role, "bold") + " " + self.paint(status_label(status), "bold")
221
+ + (self.paint(" " + ", ".join(facts), "faint") if facts else ""))
222
+ data = summary.get("data")
223
+ if isinstance(data, (dict, list)):
224
+ body = json.dumps(data, indent=2, ensure_ascii=False)
225
+ else:
226
+ body = data if isinstance(data, str) and data.strip() else summary.get("answer")
227
+ if body and status not in ("approval_denied", "stopped"):
228
+ for line in str(body).splitlines():
229
+ self.say(" " + line)
230
+ else:
231
+ message = completion_message(status, summary.get("reason"), summary.get("data_status"), summary)
232
+ if message:
233
+ self.say(" " + message)
234
+ if self.term.prompting:
235
+ self.say("") # a conversation: one blank line between tasks
236
+
237
+ def ask(self, run_id, event):
238
+ try:
239
+ approval = self.client.run(run_id).get("approval")
240
+ except (ServiceError, NoService):
241
+ approval = None
242
+ if not approval or approval.get("id") != event.get("approval_id"):
243
+ return
244
+ self.pending = dict(approval, runId=run_id)
245
+ if approval.get("kind") == "clarify":
246
+ self.note(self.paint(str(approval.get("question") or approval.get("label") or "Mobster has a question"),
247
+ "bold"), glyph="?", role="accent", indent="")
248
+ for number, choice in enumerate(approval.get("choices") or (), 1):
249
+ self.say(" " + self.keys((str(number), str(choice.get("label")))))
250
+ self.say(" Type your answer" + (", or its number" if approval.get("choices") else "")
251
+ + ". Mobster waits 10 minutes, then stops.", "faint")
252
+ return
253
+ # A line half typed when the approval came (a message meant for the task) waits until it's answered, so its
254
+ # letters never become the answer.
255
+ with self.term.lock:
256
+ if self.term.buffer:
257
+ self.held, self.term.buffer = self.held or self.term.buffer, ""
258
+ self.show_approval(approval)
259
+ if self.interactive:
260
+ yes, no = verbs(approval)
261
+ choices = [c for c in approval.get("choices") or () if isinstance(c, dict) and c.get("id")]
262
+ if self.client.app_session():
263
+ # On its own line: the keys below it never wrap into it on a narrow terminal.
264
+ self.say(f" Choose {yes} or {no} in Mobster.")
265
+ self.say(" " + self.keys(("n", no), ("x", "Stop the task")))
266
+ else:
267
+ self.say(" " + self.keys(("y", yes), ("n", no), ("x", "Stop the task")))
268
+ for number, choice in enumerate(choices, 1):
269
+ self.say(" " + self.keys((str(number), str(choice.get("label")))))
270
+ self.say(" Type the " + ("letter or number" if choices and not self.client._app_session else "letter")
271
+ + ", then Return.", "faint")
272
+ # Only a key pressed after the exact text is on screen answers it: anything typed before is dropped.
273
+ self.term.discard_typeahead()
274
+ self.escape = None
275
+
276
+ def give_back(self):
277
+ """The line you were typing when an approval came, back at the prompt once nothing waits."""
278
+ if self.held and not self.pending:
279
+ with self.term.lock:
280
+ self.term.buffer, self.held = self.held + self.term.buffer, ""
281
+ self.term.show()
282
+
283
+ def show_approval(self, approval):
284
+ self.note(self.paint(approval_title(approval), "bold"), glyph="?", indent="")
285
+ if approval.get("text"):
286
+ self.say(f" “{approval['text']}”")
287
+
288
+ # -- something to remember
289
+
290
+ def offer(self, proposals):
291
+ """A message that only asked Mobster to remember something: no task runs, and each suggestion shows here."""
292
+ proposals = [p for p in proposals or () if isinstance(p, dict) and p.get("id") and p.get("text")]
293
+ if not proposals:
294
+ self.say("Mobster already has this.")
295
+ return
296
+ if not self.interactive:
297
+ for proposal in proposals:
298
+ self.show_offer(proposal, ask=False)
299
+ self.say(" " + where_to_remember(proposal), "faint")
300
+ return
301
+ asking = bool(self.offers)
302
+ self.offers.extend(proposals)
303
+ if not asking:
304
+ self.show_offer(self.offers[0])
305
+
306
+ def show_offer(self, proposal, ask=True):
307
+ self.note(self.paint("Remember this?", "bold") + f" “{proposal['text']}”", glyph="?", role="accent", indent="")
308
+ scope = proposal.get("scope") or "global"
309
+ if scope == "global":
310
+ use = ("It goes with every task (pinned)." if proposal.get("pin") else
311
+ "Mobster will use it in tasks it fits.")
312
+ else:
313
+ where = ("tasks for this saved profile and workflow" if scope.startswith("profile:") and
314
+ "/workflow:" in scope else "tasks for this saved profile" if scope.startswith("profile:")
315
+ else "tasks in this workflow" if scope.startswith("workflow:") else "eligible tasks")
316
+ use = (f"Pinned: it goes with {where}." if proposal.get("pin") else
317
+ f"Mobster will use it in {where} when it fits.")
318
+ self.say(f" {use} It stays on this Mac.", "faint")
319
+ if ask:
320
+ self.say(" " + self.keys(("y", "Remember"), ("n", "Not now")))
321
+ self.say(" Type the letter, then Return.", "faint")
322
+ # As with an approval, only a key pressed after the words are on screen answers.
323
+ self.term.discard_typeahead()
324
+ self.escape = None
325
+
326
+ def answer_offer(self, line):
327
+ """A line typed while a suggestion waits: exactly y or n answers it, a /command runs, and other words are
328
+ neither sent nor taken as an answer."""
329
+ word = line.strip().lower()
330
+ if word.startswith("/"):
331
+ return False
332
+ if word not in ("y", "n"):
333
+ self.say(" Type y or n first.")
334
+ return True
335
+ from urllib.parse import quote
336
+ proposal = self.offers.pop(0)
337
+ try:
338
+ self.client.call("POST", f"/api/memory/proposals/{quote(str(proposal['id']), safe='')}",
339
+ {"accept": word == "y"})
340
+ except ServiceError as error:
341
+ self.note(str(error)) # answered in the Mac app meanwhile, or expired: the service's sentence
342
+ except NoService:
343
+ self.note(NOT_RUNNING)
344
+ else:
345
+ self.say(" ⎿ " + ("Mobster will remember this." if word == "y" else "Not remembered."), "faint")
346
+ if self.offers:
347
+ self.show_offer(self.offers[0])
348
+ return True
349
+
350
+ # -- your input
351
+
352
+ def decide(self, line):
353
+ """A line typed while an approval or a question waits: True when it answered (or stopped)."""
354
+ pending = self.pending
355
+ if not pending:
356
+ return self.answer_offer(line) if self.offers else False
357
+ run_id, word = pending["runId"], line.strip().lower()
358
+ if pending.get("kind") == "clarify":
359
+ choices = pending.get("choices") or []
360
+ if word.isdigit() and 1 <= int(word) <= len(choices):
361
+ return self.answer(run_id, {"id": pending["id"], "approve": True,
362
+ "choice": choices[int(word) - 1]["id"]})
363
+ return False
364
+ choices = [c for c in pending.get("choices") or () if isinstance(c, dict) and c.get("id")]
365
+ if word == "y" or (word.isdigit() and 1 <= int(word) <= len(choices)):
366
+ if self.client.app_session():
367
+ self.note(f"Choose {verbs(pending)[0]} in Mobster: the app holds approvals while it's open.")
368
+ return True
369
+ if word != "y":
370
+ return self.answer(run_id, {"id": pending["id"], "approve": True,
371
+ "choice": choices[int(word) - 1]["id"]})
372
+ return self.answer(run_id, {"id": pending["id"], "approve": True})
373
+ if word == "n":
374
+ return self.answer(run_id, {"id": pending["id"], "approve": False})
375
+ if word == "x":
376
+ self.stop()
377
+ return True
378
+ return False
379
+
380
+ def answer(self, run_id, body):
381
+ try:
382
+ self.client.call("POST", f"/api/runs/{run_id}/approval", body)
383
+ except ServiceError as error:
384
+ if error.code == "app_only":
385
+ self.client._app_session = True
386
+ self.note(f"Choose {verbs(self.pending or {})[0]} in Mobster: the app holds approvals while it's open.")
387
+ else:
388
+ self.note(str(error))
389
+ except NoService:
390
+ self.note(NOT_RUNNING)
391
+ return True
392
+
393
+ def stop(self):
394
+ if not self.run_id:
395
+ self.say("Nothing is running.", "faint")
396
+ return
397
+ try:
398
+ self.client.call("POST", f"/api/runs/{self.run_id}/stop", {})
399
+ self.say(" Stopping…", "faint")
400
+ except (ServiceError, NoService) as error:
401
+ self.note(str(error))
402
+
403
+ def send(self, text):
404
+ """A message: a new task, a steer, or an answer, as Mobster routes it. Returns the response, or None."""
405
+ if self.pending and self.pending.get("kind") != "clarify":
406
+ yes, no = verbs(self.pending)
407
+ hint = (f"Choose {yes} or {no} in Mobster, or type n or x here."
408
+ if self.client._app_session else "Type y, n or x first.")
409
+ self.note("A typed message never approves anything.")
410
+ self.say(f" {hint}") # its own line: a narrow terminal never wraps it back to column 0
411
+ return None
412
+ body = {"text": text}
413
+ if self.device:
414
+ body["device"] = self.device
415
+ if self.attachments:
416
+ body["attachmentIds"] = [ident for ident, _name in self.attachments]
417
+ try:
418
+ self.thread_id, response = self.client.send(self.thread_id, body)
419
+ except ServiceError as error:
420
+ if error.status == 404 and error.code == "thread_not_found":
421
+ self.note("That conversation is gone here. Your next message starts a new one.")
422
+ self.thread_id = None
423
+ else:
424
+ self.note(str(error))
425
+ return None
426
+ except NoService:
427
+ self.note(NOT_RUNNING)
428
+ return None
429
+ remember_thread(self.client.url, self.thread_id)
430
+ self.attachments = []
431
+ routed = routed_as(response)
432
+ if routed == "steer":
433
+ self.say(" ↳ Sent while Mobster was working", "faint")
434
+ run_id = (response.get("item") or {}).get("runId")
435
+ if run_id and run_id != self.run_id:
436
+ self.follow(run_id)
437
+ elif routed == "answer":
438
+ self.pending = None
439
+ elif routed == "remember":
440
+ self.offer(proposals_of(self.client, self.thread_id, response))
441
+ else:
442
+ run = response.get("run") or {}
443
+ if run.get("id"):
444
+ where = run.get("deviceName")
445
+ if not self.term.prompting: # one message: its first line (a conversation shows what you typed)
446
+ self.renderer(run["id"]).start(text, f"on {where}" if where else "")
447
+ elif where:
448
+ self.say(f" on {where}", "faint")
449
+ self.follow(run["id"])
450
+ return response
451
+
452
+ def command(self, line):
453
+ name, _, rest = line.partition(" ")
454
+ rest = rest.strip()
455
+ if name in ("/quit", "/exit"):
456
+ self.quit = True
457
+ elif name == "/help":
458
+ for row in HELP.splitlines():
459
+ self.say(row)
460
+ elif name == "/new":
461
+ self.thread_id = None
462
+ self.say("New conversation: your next message starts it.", "faint")
463
+ elif name == "/threads":
464
+ self.list_threads()
465
+ elif name == "/open":
466
+ self.open(rest)
467
+ elif name == "/stop":
468
+ self.stop()
469
+ elif name == "/pause":
470
+ self.pause()
471
+ elif name == "/attach":
472
+ self.attach(rest)
473
+ else:
474
+ self.note(f"Unknown command {name}. /help lists them.", indent="")
475
+
476
+ def list_threads(self):
477
+ try:
478
+ threads = self.client.call("GET", "/api/threads?limit=10")["threads"]
479
+ except (ServiceError, NoService) as error:
480
+ self.note(str(error), indent="")
481
+ return
482
+ if not threads:
483
+ self.say("No conversations yet.", "faint")
484
+ for thread in threads:
485
+ mark = "●" if thread.get("status") in ("running", "waiting") else " "
486
+ here = " (this one)" if thread["id"] == self.thread_id else ""
487
+ self.say(f"{mark} {self.paint(thread['id'], 'faint')} {ago(thread.get('updatedAt')):>8} "
488
+ f"{thread.get('title')}{here}")
489
+
490
+ def open(self, thread_id):
491
+ if not thread_id:
492
+ self.note("Open which? /threads lists them.", indent="")
493
+ return
494
+ try:
495
+ answer = self.client.thread(thread_id)
496
+ except ServiceError as error:
497
+ self.note(str(error), indent="")
498
+ return
499
+ except NoService:
500
+ self.note(NOT_RUNNING, indent="")
501
+ return
502
+ thread = answer["thread"]
503
+ self.thread_id = thread["id"]
504
+ remember_thread(self.client.url, self.thread_id)
505
+ self.say(f"Conversation “{thread.get('title')}”" + (f" on {thread['deviceName']}"
506
+ if thread.get("deviceName") else ""), "bold")
507
+ for line in transcript(answer.get("items") or [], limit=6):
508
+ self.say(line, "faint")
509
+ running = thread.get("lastRunId")
510
+ run = (answer.get("runs") or {}).get(running) if running else None
511
+ if run and run.get("finishedAt") is None:
512
+ self.follow(running)
513
+
514
+ def pause(self):
515
+ if not self.run_id:
516
+ self.say("Nothing is running.", "faint")
517
+ return
518
+ try:
519
+ answer = self.client.call("POST", f"/api/runs/{self.run_id}/pause", {"paused": not self.paused})
520
+ self.paused = bool(answer.get("paused", not self.paused))
521
+ self.say(" Paused. /pause again to continue." if self.paused else " Continuing.", "faint")
522
+ except ServiceError as error:
523
+ # 404: the running Mobster has no pause yet (an older app, or `serve` without the harness).
524
+ self.note(NO_PAUSE if error.status == 404 else str(error))
525
+ except NoService:
526
+ self.note(NOT_RUNNING)
527
+
528
+ def attach(self, path):
529
+ if not path:
530
+ self.note("Attach which file? /attach PATH", indent="")
531
+ return
532
+ file = Path(path).expanduser()
533
+ try:
534
+ size = file.stat().st_size
535
+ if size > ATTACH_MAX:
536
+ self.note("A file can be at most 25 MB.", indent="")
537
+ return
538
+ data = file.read_bytes()
539
+ except OSError:
540
+ self.note(f"Can't read {path}.", indent="")
541
+ return
542
+ from urllib.parse import quote
543
+ try:
544
+ answer = self.client.call("POST", f"/api/attachments?name={quote(file.name)}", raw=data,
545
+ content_type="application/octet-stream", timeout=120)
546
+ except ServiceError as error:
547
+ self.note(NO_FILES if error.status == 404 else str(error), indent="")
548
+ return
549
+ except NoService:
550
+ self.note(NOT_RUNNING, indent="")
551
+ return
552
+ attachment = answer.get("attachment") or {}
553
+ if attachment.get("id"):
554
+ self.attachments.append((attachment["id"], attachment.get("name") or file.name))
555
+ self.say(f"Attached {attachment.get('name') or file.name}. It goes with your next message.", "faint")
556
+
557
+ # -- the loop
558
+
559
+ def repl(self):
560
+ term = self.term
561
+ self.say(self.paint("Mobster", "accent", "bold") + self.paint(" conversation in Terminal /help for "
562
+ "commands ctrl+d to leave", "faint"))
563
+ if self.thread_id:
564
+ self.open(self.thread_id)
565
+ term.show()
566
+ while not self.quit:
567
+ try:
568
+ self.pump()
569
+ data = term.keys(0.1)
570
+ if data is None:
571
+ break
572
+ self.feed(data)
573
+ except KeyboardInterrupt:
574
+ if term.buffer:
575
+ term.buffer = ""
576
+ term.show()
577
+ elif self.run_id:
578
+ self.stop()
579
+ else:
580
+ break
581
+ term.clear()
582
+ return EXIT_DONE
583
+
584
+ def feed(self, data):
585
+ """What one read of the terminal brought, key by key."""
586
+ for ch in data:
587
+ self.type(ch)
588
+ if self.quit:
589
+ break
590
+ if self.escape == "esc":
591
+ self.escape = None # Escape on its own, not the start of an arrow key's sequence
592
+
593
+ def type(self, ch):
594
+ """One key typed on the terminal (whole lines elsewhere)."""
595
+ term = self.term
596
+ if self.escape is not None:
597
+ # The rest of a key's escape sequence (an arrow is ESC [ A, Option-arrow ESC [ 1 ; 3 D, F1 ESC O P):
598
+ # dropped whole, so it never ends up in a message as "[A".
599
+ if self.escape == "esc":
600
+ self.escape = "csi" if ch == "[" else "ss3" if ch == "O" else None
601
+ elif self.escape == "ss3" or "\x40" <= ch <= "\x7e":
602
+ self.escape = None
603
+ return
604
+ if ch == "\x1b":
605
+ self.escape = "esc"
606
+ return
607
+ if ch in ("\r", "\n"):
608
+ line, term.buffer = term.buffer.strip(), ""
609
+ if term.tty:
610
+ term.clear()
611
+ if line:
612
+ term.write(self.paint("› ", "accent") + line + "\n")
613
+ self.submit(line)
614
+ term.show()
615
+ elif ch in ("\x7f", "\b"):
616
+ term.buffer = term.buffer[:-1]
617
+ term.show()
618
+ elif ch == "\x15": # ctrl+u
619
+ term.buffer = ""
620
+ term.show()
621
+ elif ch == "\x04": # ctrl+d
622
+ if not term.buffer:
623
+ self.quit = True
624
+ elif ch < " " and ch != "\t":
625
+ pass # other control keys: ignored
626
+ else:
627
+ term.buffer += ch
628
+ if term.tty:
629
+ term.write(ch)
630
+ term.shown = True
631
+
632
+ def submit(self, line):
633
+ if not line:
634
+ return
635
+ if self.decide(line):
636
+ return
637
+ if line.startswith("/"):
638
+ self.command(line)
639
+ else:
640
+ self.send(line)
641
+
642
+ def ago(ms):
643
+ if not ms:
644
+ return ""
645
+ seconds = max(0, time.time() - ms / 1000)
646
+ for unit, size in (("d", 86400), ("h", 3600), ("min", 60)):
647
+ if seconds >= size:
648
+ return f"{int(seconds // size)} {unit} ago"
649
+ return "just now"
650
+
651
+
652
+ def transcript(items, limit=6):
653
+ """The last exchanges of a thread, one line each, for /open."""
654
+ lines = []
655
+ for item in items:
656
+ if item.get("kind") == "user":
657
+ mark = {"steer": "↳ ", "answer": "↳ ", "unread": "↳ (not read) "}.get(item.get("routed"), "› ")
658
+ lines.append(f"{mark}{item.get('text')}")
659
+ elif item.get("kind") == "run" and item.get("result"):
660
+ from ..narrate import status_label
661
+ result = item["result"]
662
+ lines.append(f" {status_label(result.get('status'))}" + (f": {result['answer']}" if result.get("answer")
663
+ else ""))
664
+ elif item.get("kind") == "clarify":
665
+ lines.append(f" ? {item.get('question')}" + (f" → {item['answer']}" if item.get("answer") else ""))
666
+ return lines[-limit:]
667
+
668
+
669
+ def run(args, stdin=None, stdout=None, stderr=None, client=None):
670
+ stdin, stdout, stderr = stdin or sys.stdin, stdout or sys.stdout, stderr or sys.stderr
671
+ as_json = bool(args.json)
672
+
673
+ def fail(message, code="couldnt_run"):
674
+ if as_json:
675
+ print(json.dumps({"status": "couldnt_run", "error": message, "code": code}), file=stdout, flush=True)
676
+ else:
677
+ print(message, file=stderr, flush=True)
678
+ return EXIT_COULDNT
679
+
680
+ if client is None:
681
+ try:
682
+ client = Client.connect(args.url, origin="cli")
683
+ except NoService as error:
684
+ return fail(str(error), "not_running")
685
+ except ServiceError as error:
686
+ return fail(str(error), error.code or "refused")
687
+ if not client.has_threads:
688
+ return fail(NO_THREADS, "no_threads")
689
+ text = " ".join(args.message).strip()
690
+ workflow = getattr(args, "workflow", None)
691
+ if workflow and text:
692
+ return fail("Give a message or --workflow, not both.", "usage")
693
+ if getattr(args, "set", None) and not workflow:
694
+ return fail("--set fills a workflow's blanks: use it with --workflow.", "usage")
695
+ if getattr(args, "keep_waiting", False) and not as_json:
696
+ return fail("--keep-waiting goes with --json.", "usage")
697
+ wait = 300.0 if args.wait is None else args.wait
698
+ if not text and not workflow and not isatty(stdin):
699
+ text = stdin.read().strip()
700
+ if not text:
701
+ return fail("Say what Mobster should do: mobster chat \"What's on my calendar tomorrow?\"", "usage")
702
+ thread_id = None
703
+ if args.thread:
704
+ thread_id = args.thread.strip().lower()
705
+ try:
706
+ client.thread(thread_id)
707
+ except ServiceError as error:
708
+ return fail(str(error), error.code or "thread_not_found")
709
+ except NoService as error:
710
+ return fail(str(error), "not_running")
711
+ elif not args.new and not workflow:
712
+ remembered = last_thread(client.url)
713
+ if remembered:
714
+ try:
715
+ thread = client.thread(remembered)["thread"]
716
+ thread_id = None if thread.get("archived") else remembered
717
+ except (ServiceError, NoService):
718
+ thread_id = None
719
+ if text and len(text) > 4000:
720
+ return fail("A message can be at most 4,000 characters.", "too_long")
721
+ term = Term(stdin, stdout if not as_json else stderr)
722
+ term.prompting = not text and not workflow
723
+ with term:
724
+ chat = Chat(client, term, thread_id=thread_id, device=args.device, interactive=term.tty and not as_json)
725
+ if workflow:
726
+ try:
727
+ return workflow_once(client, chat, args, as_json=as_json, wait=wait, out=stdout, fail=fail)
728
+ except NoService as error:
729
+ return fail(str(error), "not_running")
730
+ if text:
731
+ if thread_id and not as_json:
732
+ chat.say(f"Continuing your last conversation ({thread_id}); --new starts another.", "faint")
733
+ try:
734
+ return once(client, chat, text, as_json=as_json, wait=wait, out=stdout,
735
+ keep=bool(getattr(args, "keep_waiting", False)))
736
+ except NoService as error:
737
+ return fail(str(error), "not_running")
738
+ if not term.tty:
739
+ return fail("Say what Mobster should do: mobster chat \"What's on my calendar tomorrow?\"", "usage")
740
+ return chat.repl()