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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
"""The pack framework: what a pack declares, and the code that recognizes screens, navigates and reads with it."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from .. import harness_api, hints
|
|
10
|
+
from . import pins
|
|
11
|
+
|
|
12
|
+
# The confidence at which a pack is sure which screen it is on; below it the general agent takes the step.
|
|
13
|
+
SURE = hints.SURE # one threshold for every source of hints (hints.py)
|
|
14
|
+
# A tie closer than this between the two best screens is no answer.
|
|
15
|
+
MARGIN = .1
|
|
16
|
+
SETTLE_SECONDS = .6
|
|
17
|
+
BACK_LABEL = re.compile(r"^\s*(back|close|dismiss|cancel|‹|<|chevron[ .]?left|navigate up)\s*$", re.I)
|
|
18
|
+
# An act's own control, by its whole label: never tapped by a pack, whatever the step says.
|
|
19
|
+
ACT_LABEL = re.compile(r"^(like|liked|unlike|follow|follow back|following|unfollow|requested|send|send message|share|"
|
|
20
|
+
r"repost|post|post comment|reply|save|save post|delete|remove|block|report|mute|restrict|"
|
|
21
|
+
r"subscribe|buy|pay|order|book|accept|decline|approve|unsend|add to story|go live|"
|
|
22
|
+
r"share to story|done|submit|confirm)$", re.I)
|
|
23
|
+
# Words a pack never taps in a control it doesn't know only opens a screen.
|
|
24
|
+
NEVER = re.compile(r"\b(like|likes|liked|unlike|follow|unfollow|send|share|repost|post|delete|remove|block|report|"
|
|
25
|
+
r"restrict|mute|subscribe|buy|pay|order|book|accept|decline|approve|unsend|save post|"
|
|
26
|
+
r"add to story|go live)\b", re.I)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Lost(Exception):
|
|
30
|
+
"""Navigation or a read couldn't go on: the message says where it stopped, in plain words."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PinnedAside(Lost):
|
|
34
|
+
"""The team pinned this pack to another version, or turned it off (pins.py): the pack steps aside."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Refused(Lost):
|
|
38
|
+
"""A tap or a keystroke the pack may not make (it names an act, or the caller's check said no)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Signal:
|
|
43
|
+
pattern: re.Pattern
|
|
44
|
+
role: Optional[str] = None # the element's role ("Button"), or any
|
|
45
|
+
region: Optional[tuple] = None # (top, bottom): screen fractions the element's centre lies between
|
|
46
|
+
selected: Optional[bool] = None # True: the element is the current tab (its Selected trait or value "1")
|
|
47
|
+
weight: float = 1.0
|
|
48
|
+
must: bool = False # the screen is never this one without it
|
|
49
|
+
never: bool = False # the screen is never this one with it
|
|
50
|
+
field: str = "label" # "label", "value", or "text" (any line of the screen's text)
|
|
51
|
+
column: Optional[tuple] = None # (left, right): screen fractions the element's centre lies between
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sig(pattern, role=None, *, region=None, selected=None, weight=1.0, must=False, never=False, field="label",
|
|
55
|
+
column=None, flags=re.I):
|
|
56
|
+
"""A ``Signal``; ``pattern`` is a regex string, searched."""
|
|
57
|
+
return Signal(re.compile(pattern, flags), role, region, selected, weight, must, never, field, column)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ScreenSpec:
|
|
62
|
+
name: str # "dm_inbox"
|
|
63
|
+
title: str # "DM inbox": how a person names it
|
|
64
|
+
signals: tuple
|
|
65
|
+
evidence: str = "inferred" # "recorded": checked on a tree recorded from the real app
|
|
66
|
+
# The same screen as an image-only connection reads it (iPhone Mirroring, USB video: perception.py): only the
|
|
67
|
+
# words drawn on screen, named icons as Buttons, the current tab when the pixels show it. No identifiers, no
|
|
68
|
+
# accessibility names of icons that aren't drawn. A screen without them is never named on such a read.
|
|
69
|
+
ocr: tuple = ()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class Seen:
|
|
74
|
+
screen: Optional[str] # the screen's name, or None when the pack isn't sure
|
|
75
|
+
confidence: float
|
|
76
|
+
title: str = ""
|
|
77
|
+
app: str = "" # the bundle id in front
|
|
78
|
+
matched: tuple = () # the signals that matched, as their patterns (tests, logs)
|
|
79
|
+
runner_up: Optional[str] = None
|
|
80
|
+
evidence: str = "" # the matched spec's: "recorded", "inferred" or "learned" (heal.py)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def sure(self):
|
|
84
|
+
return self.screen is not None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_selected(element):
|
|
88
|
+
return bool(getattr(element, "selected", False)) or str(getattr(element, "value", "") or "").strip() == "1"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def centre_y(element):
|
|
92
|
+
x, y, w, h = element.rect
|
|
93
|
+
return y + h / 2
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Connections that read the screen from pictures (perception.py): their elements' locators start "ocr:" or "icon:".
|
|
97
|
+
IMAGE_SOURCES = frozenset({"mirroring", "usb_video", "usb_avf"})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def image_only(snapshot):
|
|
101
|
+
"""Whether ``snapshot`` was read from a picture, with no accessibility tree."""
|
|
102
|
+
if getattr(snapshot, "source", "") in IMAGE_SOURCES:
|
|
103
|
+
return True
|
|
104
|
+
elements = getattr(snapshot, "elements", ()) or ()
|
|
105
|
+
return bool(elements) and all(str(e.locator).startswith(("ocr:", "icon:")) for e in elements)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def hits(signal, snapshot):
|
|
109
|
+
"""The elements (or text lines) ``signal`` matches on ``snapshot``."""
|
|
110
|
+
if signal.field == "text":
|
|
111
|
+
return [line for line in str(getattr(snapshot, "text", "") or "").split("\n") if signal.pattern.search(line)]
|
|
112
|
+
out = []
|
|
113
|
+
for element in getattr(snapshot, "elements", ()) or ():
|
|
114
|
+
if signal.role and element.role != signal.role:
|
|
115
|
+
continue
|
|
116
|
+
if signal.region and not signal.region[0] <= centre_y(element) <= signal.region[1]:
|
|
117
|
+
continue
|
|
118
|
+
if signal.column and not signal.column[0] <= element.rect[0] + element.rect[2] / 2 <= signal.column[1]:
|
|
119
|
+
continue
|
|
120
|
+
if signal.selected is not None and is_selected(element) != signal.selected:
|
|
121
|
+
continue
|
|
122
|
+
words = element.value if signal.field == "value" else element.label
|
|
123
|
+
if words and signal.pattern.search(words):
|
|
124
|
+
out.append(element)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def score(spec, snapshot, signals=None):
|
|
129
|
+
"""(confidence 0..1, matched patterns) of ``spec`` on ``snapshot``: its tree signals, or its ``ocr`` ones on a
|
|
130
|
+
picture-read screen (``image_only``)."""
|
|
131
|
+
if signals is None:
|
|
132
|
+
signals = spec.ocr if image_only(snapshot) else spec.signals
|
|
133
|
+
total = got = 0.0
|
|
134
|
+
matched = []
|
|
135
|
+
for signal in signals:
|
|
136
|
+
found = bool(hits(signal, snapshot))
|
|
137
|
+
if signal.never:
|
|
138
|
+
if found:
|
|
139
|
+
return 0.0, ()
|
|
140
|
+
continue
|
|
141
|
+
if signal.must and not found:
|
|
142
|
+
return 0.0, ()
|
|
143
|
+
total += signal.weight
|
|
144
|
+
if found:
|
|
145
|
+
got += signal.weight
|
|
146
|
+
matched.append(signal.pattern.pattern)
|
|
147
|
+
return (got / total if total else 0.0), tuple(matched)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def recognize(pack, snapshot):
|
|
151
|
+
"""Which of ``pack``'s screens ``snapshot`` shows (``Seen``)."""
|
|
152
|
+
app = str(getattr(snapshot, "bundle_id", "") or "")
|
|
153
|
+
if app and app != pack.bundle:
|
|
154
|
+
return Seen(None, 0.0, app=app)
|
|
155
|
+
pictured = image_only(snapshot)
|
|
156
|
+
specs = [spec for spec in (*pack.screens, *pack.learned_specs()) if spec.ocr or not pictured]
|
|
157
|
+
best_of = {}
|
|
158
|
+
for spec in specs:
|
|
159
|
+
(value, matched) = score(spec, snapshot)
|
|
160
|
+
if spec.name not in best_of or value > best_of[spec.name][0]:
|
|
161
|
+
best_of[spec.name] = (value, matched, spec)
|
|
162
|
+
ranked = sorted(best_of.values(), key=lambda entry: -entry[0])
|
|
163
|
+
if not ranked:
|
|
164
|
+
return Seen(None, 0.0, app=app)
|
|
165
|
+
best, matched, spec = ranked[0]
|
|
166
|
+
second = ranked[1][0] if len(ranked) > 1 else 0.0
|
|
167
|
+
runner = ranked[1][2].name if len(ranked) > 1 and second > 0 else None
|
|
168
|
+
if best < SURE or best - second < MARGIN and second >= SURE:
|
|
169
|
+
return Seen(None, round(best, 2), app=app, matched=matched, runner_up=spec.name)
|
|
170
|
+
return Seen(spec.name, round(best, 2), spec.title, app, matched, runner, spec.evidence)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def find_all(snapshot, pattern, role=None, region=None, selected=None, roles=None, field_="label"):
|
|
174
|
+
"""Every element whose label (or value) matches ``pattern`` (a regex or a string, searched case-insensitively)."""
|
|
175
|
+
rx = pattern if isinstance(pattern, re.Pattern) else re.compile(pattern, re.I)
|
|
176
|
+
out = []
|
|
177
|
+
for element in getattr(snapshot, "elements", ()) or ():
|
|
178
|
+
if role and element.role != role or roles and element.role not in roles:
|
|
179
|
+
continue
|
|
180
|
+
if region and not region[0] <= centre_y(element) <= region[1]:
|
|
181
|
+
continue
|
|
182
|
+
if selected is not None and is_selected(element) != selected:
|
|
183
|
+
continue
|
|
184
|
+
words = element.value if field_ == "value" else element.label
|
|
185
|
+
if words and rx.search(words):
|
|
186
|
+
out.append(element)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def find(snapshot, pattern, **options):
|
|
191
|
+
found = find_all(snapshot, pattern, **options)
|
|
192
|
+
return found[0] if found else None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def plain(text):
|
|
196
|
+
return " ".join(str(text or "").split())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def rows(snapshot, gap=.022):
|
|
200
|
+
"""What a reader walks: the elements, or on a picture-read screen its lines joined into rows (a list row's name and
|
|
201
|
+
its preview line, read as two lines, become one element labelled "name, preview, time")."""
|
|
202
|
+
if not image_only(snapshot):
|
|
203
|
+
return list(getattr(snapshot, "elements", ()) or ())
|
|
204
|
+
from ..state import Element
|
|
205
|
+
lines = sorted((e for e in snapshot.elements if e.role != "Button" or not str(e.locator).startswith("icon:")),
|
|
206
|
+
key=lambda e: (e.rect[1], e.rect[0]))
|
|
207
|
+
groups = []
|
|
208
|
+
for e in lines:
|
|
209
|
+
last = groups[-1] if groups else None
|
|
210
|
+
if last and e.rect[1] - (last[-1].rect[1] + last[-1].rect[3]) <= gap and abs(e.rect[0] - last[0].rect[0]) < .08:
|
|
211
|
+
last.append(e)
|
|
212
|
+
else:
|
|
213
|
+
groups.append([e])
|
|
214
|
+
out = []
|
|
215
|
+
for index, group in enumerate(groups):
|
|
216
|
+
x = min(e.rect[0] for e in group)
|
|
217
|
+
y = min(e.rect[1] for e in group)
|
|
218
|
+
right = max(e.rect[0] + e.rect[2] for e in group)
|
|
219
|
+
bottom = max(e.rect[1] + e.rect[3] for e in group)
|
|
220
|
+
label = ", ".join(re.sub(r"\s*[·•]\s*", ", ", plain(e.label)) for e in group)[:500]
|
|
221
|
+
out.append(Element(f"r{index}", label, "Cell", (x, y, max(right - x, 1e-3), max(bottom - y, 1e-3)),
|
|
222
|
+
locator=group[0].locator))
|
|
223
|
+
return out
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class Tap:
|
|
228
|
+
"""Tap the first control matching ``pattern``."""
|
|
229
|
+
pattern: str
|
|
230
|
+
what: str
|
|
231
|
+
role: Optional[str] = None
|
|
232
|
+
region: Optional[tuple] = None
|
|
233
|
+
opens: bool = False
|
|
234
|
+
optional: bool = False
|
|
235
|
+
wait: float = 0.0 # extra seconds a slow screen takes to fill after the tap
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass(frozen=True)
|
|
239
|
+
class TypeIn:
|
|
240
|
+
"""Type the route's argument into the field matching ``pattern`` (a search field only)."""
|
|
241
|
+
pattern: str
|
|
242
|
+
what: str
|
|
243
|
+
submit: bool = False
|
|
244
|
+
text: Optional[str] = None # fixed words to type instead of the route's argument ("About")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass(frozen=True)
|
|
248
|
+
class PickRow:
|
|
249
|
+
"""Tap the row whose words hold the route's argument (a DM thread by name, a search result by handle)."""
|
|
250
|
+
what: str
|
|
251
|
+
roles: tuple = ("Cell", "Button", "StaticText", "Other", "Link")
|
|
252
|
+
region: tuple = (.1, .9)
|
|
253
|
+
exact_handle: bool = False # the argument is a handle: match it as a whole word
|
|
254
|
+
template: object = None # a regex with {arg} (escaped) a row's words must match instead, or a
|
|
255
|
+
# tuple of them, tried in order
|
|
256
|
+
scrolls: int = 0 # swipes up to look further down the list when the row isn't showing
|
|
257
|
+
text: Optional[str] = None # fixed words the row holds instead of the route's argument
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True)
|
|
261
|
+
class Control:
|
|
262
|
+
"""An act's control (``Pack.controls``): the route that stops at it, the pattern that finds it on the screen (with
|
|
263
|
+
its ``role`` and ``region`` when the pattern alone is loose) and the ``title`` the tools call it."""
|
|
264
|
+
route: str
|
|
265
|
+
control: str
|
|
266
|
+
title: str
|
|
267
|
+
role: Optional[str] = None
|
|
268
|
+
region: Optional[tuple] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class Reader:
|
|
273
|
+
"""What one ``Pack.readers`` entry reads: ``read(pack, io, snapshot, n, arg)`` -> ``ReadResult``, the ``screens`` it
|
|
274
|
+
reads, and the ``route`` that gets there from elsewhere (None: only from where the person is)."""
|
|
275
|
+
read: object
|
|
276
|
+
screens: tuple
|
|
277
|
+
route: Optional[str] = None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True)
|
|
281
|
+
class Route:
|
|
282
|
+
name: str
|
|
283
|
+
title: str # "your DMs"
|
|
284
|
+
lands: tuple # the screens that mean it arrived
|
|
285
|
+
steps: tuple = ()
|
|
286
|
+
link: Optional[str] = None # a deep link template ({arg}); tried first when the pack trusts its links
|
|
287
|
+
from_tabs: bool = True # the steps start on a screen with the app's tab bar
|
|
288
|
+
arg: Optional[str] = None # "user", "name", "query" or "url": the argument it needs
|
|
289
|
+
then_control: Optional[str] = None # an act's control to name once there ("like")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class Landing:
|
|
294
|
+
ok: bool
|
|
295
|
+
route: str
|
|
296
|
+
seen: Seen
|
|
297
|
+
how: str = "" # "already" | "link" | "code" | ""
|
|
298
|
+
steps: int = 0
|
|
299
|
+
why: str = ""
|
|
300
|
+
snapshot: object = None
|
|
301
|
+
seconds: float = 0.0
|
|
302
|
+
control: object = None # the act's control, when the route names one and it is on screen
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class PackIO:
|
|
306
|
+
"""The phone, as a pack uses it: reads, settles, and the only taps and keystrokes it makes."""
|
|
307
|
+
|
|
308
|
+
def __init__(self, driver, pack, *, permit=None, cancelled=None, allows_app=None, clock=time.monotonic,
|
|
309
|
+
sleep=time.sleep, deadline=60.0):
|
|
310
|
+
self.driver, self.pack = driver, pack
|
|
311
|
+
self.permit = permit # (op, element, snapshot, text) -> None, or raise Refused
|
|
312
|
+
self.cancelled = cancelled or (lambda: False)
|
|
313
|
+
self.allows_app = allows_app or (lambda bundle: True)
|
|
314
|
+
self.clock, self.sleep = clock, sleep
|
|
315
|
+
self.started = clock()
|
|
316
|
+
self.deadline = deadline
|
|
317
|
+
self.taps = 0
|
|
318
|
+
self.screens = [] # every screen read, in order (the run's contract sees them)
|
|
319
|
+
|
|
320
|
+
def left(self):
|
|
321
|
+
return self.deadline - (self.clock() - self.started)
|
|
322
|
+
|
|
323
|
+
def check_time(self):
|
|
324
|
+
if self.cancelled():
|
|
325
|
+
raise Lost("the task was stopped")
|
|
326
|
+
if self.left() <= 0:
|
|
327
|
+
raise Lost("it ran out of time")
|
|
328
|
+
|
|
329
|
+
def observe(self):
|
|
330
|
+
snapshot = self.driver.observe(timeout=10)
|
|
331
|
+
self.screens.append(snapshot)
|
|
332
|
+
return snapshot
|
|
333
|
+
|
|
334
|
+
def settle(self, before, extra=0.0):
|
|
335
|
+
"""The screen after an action: the driver's own wait when it has one, else a pause and a read."""
|
|
336
|
+
native = getattr(self.driver, "wait_for_change", None)
|
|
337
|
+
if callable(native):
|
|
338
|
+
try:
|
|
339
|
+
after = native(before, wait_seconds=SETTLE_SECONDS + extra, timeout=SETTLE_SECONDS + extra + 1.5)
|
|
340
|
+
self.screens.append(after)
|
|
341
|
+
return after
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
self.sleep(SETTLE_SECONDS + extra)
|
|
345
|
+
return self.observe()
|
|
346
|
+
|
|
347
|
+
def may_tap(self, label, opens=False, pattern=None):
|
|
348
|
+
"""Whether the pack may tap a control with ``label``: never an act's own control or one of the app's ``never``
|
|
349
|
+
words; a control the step knows only ``opens`` a screen, matched in full, may name an act's noun ("Comment")."""
|
|
350
|
+
text = plain(label)
|
|
351
|
+
if not text or ACT_LABEL.match(text) or self.pack.never.search(text):
|
|
352
|
+
return False
|
|
353
|
+
if opens and pattern and re.fullmatch(pattern, text, re.I):
|
|
354
|
+
return True
|
|
355
|
+
if NEVER.search(text):
|
|
356
|
+
return False
|
|
357
|
+
return harness_api.commit_act("TAP", text, None, self.pack.bundle) is None
|
|
358
|
+
|
|
359
|
+
def tap(self, element, snapshot, *, opens=False, pattern=None, check=None, extra=0.0):
|
|
360
|
+
"""Tap ``element`` (a navigation control); the settled screen after."""
|
|
361
|
+
self.check_time()
|
|
362
|
+
if not self.may_tap(check if check is not None else element.label, opens, pattern):
|
|
363
|
+
raise Refused(f"it won't tap “{plain(element.label)[:60]}”: that control may act")
|
|
364
|
+
if self.permit is not None:
|
|
365
|
+
self.permit("TAP", element, snapshot, None)
|
|
366
|
+
self.driver.execute("TAP", element, snapshot, timeout=10)
|
|
367
|
+
self.taps += 1
|
|
368
|
+
return self.settle(snapshot, extra)
|
|
369
|
+
|
|
370
|
+
def type(self, element, snapshot, text, submit=False):
|
|
371
|
+
"""Type into a search field (the only field a pack types in)."""
|
|
372
|
+
self.check_time()
|
|
373
|
+
words = plain(f"{element.label} {getattr(element, 'placeholder', '')}")
|
|
374
|
+
if not element.editable or not (element.role == "SearchField" or re.search(r"\bsearch\b", words, re.I)):
|
|
375
|
+
raise Refused("it types only into a search field")
|
|
376
|
+
if self.permit is not None:
|
|
377
|
+
self.permit("TYPE", element, snapshot, text)
|
|
378
|
+
self.driver.execute("TYPE", element, snapshot, text=text, timeout=20)
|
|
379
|
+
after = self.settle(snapshot)
|
|
380
|
+
if submit:
|
|
381
|
+
field_ = find(after, re.escape(element.label), role=element.role) or element
|
|
382
|
+
if self.permit is not None:
|
|
383
|
+
self.permit("SUBMIT", field_, after, None)
|
|
384
|
+
if "SUBMIT" in field_.actions:
|
|
385
|
+
self.driver.execute("SUBMIT", field_, after, timeout=10)
|
|
386
|
+
after = self.settle(after)
|
|
387
|
+
return after
|
|
388
|
+
|
|
389
|
+
def swipe(self, snapshot, direction="up"):
|
|
390
|
+
self.check_time()
|
|
391
|
+
op = {"up": "SWIPE_UP", "down": "SWIPE_DOWN"}[direction]
|
|
392
|
+
self.driver.execute(op, None, snapshot, timeout=10)
|
|
393
|
+
return self.settle(snapshot)
|
|
394
|
+
|
|
395
|
+
def back(self, snapshot):
|
|
396
|
+
"""Tap the screen's Back (or Close) control; the screen after, or None when it has none."""
|
|
397
|
+
control = next((e for e in snapshot.elements if e.role == "Button" and BACK_LABEL.match(e.label or "")), None)
|
|
398
|
+
if control is None:
|
|
399
|
+
control = self.pack.back_control(snapshot)
|
|
400
|
+
if control is None:
|
|
401
|
+
return None
|
|
402
|
+
return self.tap(control, snapshot)
|
|
403
|
+
|
|
404
|
+
def launch(self, snapshot):
|
|
405
|
+
self.check_time()
|
|
406
|
+
if not self.allows_app(self.pack.bundle):
|
|
407
|
+
raise Refused(f"{self.pack.name} isn't allowed in this task")
|
|
408
|
+
if self.permit is not None:
|
|
409
|
+
self.permit("LAUNCH_APP", None, snapshot, None)
|
|
410
|
+
self.driver.execute("LAUNCH_APP", self.pack.bundle, snapshot, timeout=20)
|
|
411
|
+
return self.settle(snapshot, extra=.6)
|
|
412
|
+
|
|
413
|
+
def open_link(self, url, snapshot):
|
|
414
|
+
from ..deeplinks import open_link
|
|
415
|
+
self.check_time()
|
|
416
|
+
if not self.allows_app(self.pack.bundle):
|
|
417
|
+
raise Refused(f"{self.pack.name} isn't allowed in this task")
|
|
418
|
+
if self.permit is not None:
|
|
419
|
+
self.permit("OPEN_URL", None, snapshot, url)
|
|
420
|
+
open_link(self.driver, url, bundle=self.pack.bundle, clock=self.clock, sleep=self.sleep)
|
|
421
|
+
return self.settle(snapshot, extra=.8)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _on_tabs(pack, snapshot):
|
|
425
|
+
return bool(pack.tabs is not None and hits(pack.tabs, snapshot))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def to_tabs(pack, io, snapshot):
|
|
429
|
+
"""A screen with the app's tab bar: launch the app when another is in front, then Back (or Close)
|
|
430
|
+
up to three times."""
|
|
431
|
+
if str(getattr(snapshot, "bundle_id", "") or "") not in ("", pack.bundle):
|
|
432
|
+
snapshot = io.launch(snapshot)
|
|
433
|
+
for _ in range(3):
|
|
434
|
+
if _on_tabs(pack, snapshot):
|
|
435
|
+
return snapshot
|
|
436
|
+
after = io.back(snapshot)
|
|
437
|
+
if after is None:
|
|
438
|
+
break
|
|
439
|
+
snapshot = after
|
|
440
|
+
if _on_tabs(pack, snapshot):
|
|
441
|
+
return snapshot
|
|
442
|
+
raise Lost(f"it couldn't get back to {pack.name}'s tab bar")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _run_step(pack, io, step, snapshot, arg):
|
|
446
|
+
if isinstance(step, Tap):
|
|
447
|
+
control = find(snapshot, step.pattern, role=step.role, region=step.region)
|
|
448
|
+
if control is None:
|
|
449
|
+
if step.optional:
|
|
450
|
+
return snapshot
|
|
451
|
+
raise Lost(f"{step.what} isn't on the screen")
|
|
452
|
+
return io.tap(control, snapshot, opens=step.opens, pattern=step.pattern, extra=step.wait)
|
|
453
|
+
if isinstance(step, TypeIn):
|
|
454
|
+
control = next((e for e in find_all(snapshot, step.pattern) if e.editable), None)
|
|
455
|
+
if control is None:
|
|
456
|
+
raise Lost(f"{step.what} isn't on the screen")
|
|
457
|
+
return io.type(control, snapshot, step.text or arg, submit=step.submit)
|
|
458
|
+
if isinstance(step, PickRow):
|
|
459
|
+
arg = step.text or arg
|
|
460
|
+
row = pick_row(snapshot, arg, step, pack)
|
|
461
|
+
for _ in range(step.scrolls if row is None else 0):
|
|
462
|
+
after = io.swipe(snapshot, "up")
|
|
463
|
+
if after.content_fingerprint == snapshot.content_fingerprint:
|
|
464
|
+
break
|
|
465
|
+
snapshot = after
|
|
466
|
+
row = pick_row(snapshot, arg, step, pack)
|
|
467
|
+
if row is not None:
|
|
468
|
+
break
|
|
469
|
+
if row is None:
|
|
470
|
+
raise Lost(f"no {step.what} names “{plain(arg)[:40]}”")
|
|
471
|
+
# A row is checked by the name it was picked by: its other words are the app's (a last message, "Delete").
|
|
472
|
+
return io.tap(row, snapshot, opens=True, pattern=r".*", check=plain(arg))
|
|
473
|
+
raise ValueError(f"unknown step {step!r}")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _names_an_act(text, pack):
|
|
477
|
+
return bool(ACT_LABEL.match(text) or NEVER.search(text) or (pack is not None and pack.never.search(text)))
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def pick_row(snapshot, arg, step, pack=None):
|
|
481
|
+
"""The row ``arg`` names: a whole-word match first, then any match; the largest row wins (a cell over its text)."""
|
|
482
|
+
want = plain(arg).lstrip("@").casefold()
|
|
483
|
+
if not want:
|
|
484
|
+
return None
|
|
485
|
+
candidates = [e for e in snapshot.elements if e.role in step.roles and e.label
|
|
486
|
+
and step.region[0] <= centre_y(e) <= step.region[1]
|
|
487
|
+
and (e.role == "Cell" or not _names_an_act(plain(e.label), pack))]
|
|
488
|
+
if step.template:
|
|
489
|
+
for template in (step.template,) if isinstance(step.template, str) else step.template:
|
|
490
|
+
rx = re.compile(template.replace("{arg}", re.escape(want)), re.I)
|
|
491
|
+
found = [e for e in candidates if rx.search(plain(e.label))]
|
|
492
|
+
if found:
|
|
493
|
+
return found[0]
|
|
494
|
+
return None
|
|
495
|
+
word = re.compile(r"(?<![\w.])@?" + re.escape(want) + r"(?![\w])", re.I)
|
|
496
|
+
whole = [e for e in candidates if word.search(e.label)]
|
|
497
|
+
loose = [] if step.exact_handle else [e for e in candidates if want in e.label.casefold()]
|
|
498
|
+
for group in (whole, loose):
|
|
499
|
+
if group:
|
|
500
|
+
return max(group, key=lambda e: e.rect[2] * e.rect[3])
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def navigate(pack, io, name, arg=None):
|
|
505
|
+
"""Go to ``pack``'s route ``name`` (with its argument): ``Landing``."""
|
|
506
|
+
started = io.clock()
|
|
507
|
+
route = pack.routes.get(name)
|
|
508
|
+
if route is None:
|
|
509
|
+
raise ValueError(f"{pack.name} has no route {name!r}")
|
|
510
|
+
if route.arg and not plain(arg):
|
|
511
|
+
return Landing(False, name, Seen(None, 0.0), why=f"it needs a {route.arg}", seconds=0.0)
|
|
512
|
+
snapshot, how, steps = None, "", 0
|
|
513
|
+
try:
|
|
514
|
+
snapshot = io.observe()
|
|
515
|
+
seen = recognize(pack, snapshot)
|
|
516
|
+
if seen.screen in route.lands and not route.arg:
|
|
517
|
+
return _landed(pack, route, seen, snapshot, "already", 0, io, started, arg)
|
|
518
|
+
if route.link and pack.links_on and callable(getattr(io.driver, "call", None)):
|
|
519
|
+
# A connection with no link opener (iPhone Mirroring) goes straight to the steps.
|
|
520
|
+
url = link_for(pack, route, arg)
|
|
521
|
+
if url:
|
|
522
|
+
try:
|
|
523
|
+
snapshot = io.open_link(url, snapshot)
|
|
524
|
+
except Refused:
|
|
525
|
+
raise
|
|
526
|
+
except Exception: # noqa: BLE001 -- a link that won't open is a route by taps, never a failed step
|
|
527
|
+
snapshot = io.observe()
|
|
528
|
+
steps += 1
|
|
529
|
+
seen = recognize(pack, snapshot)
|
|
530
|
+
if seen.screen in route.lands:
|
|
531
|
+
return _landed(pack, route, seen, snapshot, "link", steps, io, started, arg)
|
|
532
|
+
if route.from_tabs:
|
|
533
|
+
snapshot = to_tabs(pack, io, snapshot)
|
|
534
|
+
for step in route.steps:
|
|
535
|
+
before = io.taps
|
|
536
|
+
snapshot = _run_step(pack, io, step, snapshot, arg)
|
|
537
|
+
steps += max(1, io.taps - before)
|
|
538
|
+
seen = recognize(pack, snapshot)
|
|
539
|
+
if seen.screen in route.lands:
|
|
540
|
+
return _landed(pack, route, seen, snapshot, "code", steps, io, started, arg)
|
|
541
|
+
return Landing(False, name, seen, "code", steps, _unsure(pack, seen, route), snapshot,
|
|
542
|
+
round(io.clock() - started, 2))
|
|
543
|
+
except Lost as lost:
|
|
544
|
+
seen = recognize(pack, snapshot) if snapshot is not None else Seen(None, 0.0)
|
|
545
|
+
return Landing(False, name, seen, how, steps, str(lost), snapshot, round(io.clock() - started, 2))
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def link_for(pack, route, arg):
|
|
549
|
+
"""The route's deep link with its argument, or None."""
|
|
550
|
+
if route.arg == "url":
|
|
551
|
+
url = plain(arg)
|
|
552
|
+
host = re.match(r"^https://([^/?#]+)/", url + "/")
|
|
553
|
+
if not (host and host.group(1).casefold() in pack.sites and len(url) < 500):
|
|
554
|
+
return None
|
|
555
|
+
return url if pack.link_paths.match(urlsplit(url).path) else None
|
|
556
|
+
from ..deeplinks import Link
|
|
557
|
+
try:
|
|
558
|
+
return Link("pack", pack.name, pack.bundle, route.title, route.link).fill(arg=(arg or "").lstrip("@"))
|
|
559
|
+
except KeyError:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _landed(pack, route, seen, snapshot, how, steps, io, started, arg=None):
|
|
564
|
+
why = pack.confirm(route.name, arg, snapshot)
|
|
565
|
+
if why:
|
|
566
|
+
# On the right kind of screen but not the one asked for (another conversation): never name an act's control.
|
|
567
|
+
return Landing(False, route.name, seen, how, steps, why, snapshot, round(io.clock() - started, 2))
|
|
568
|
+
landing = Landing(True, route.name, seen, how, steps, "", snapshot, round(io.clock() - started, 2))
|
|
569
|
+
if route.then_control:
|
|
570
|
+
landing.control = control_for(pack, route.then_control, snapshot)
|
|
571
|
+
return landing
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _unsure(pack, seen, route):
|
|
575
|
+
wanted = " or ".join(next((s.title for s in pack.screens if s.name == name), name) for name in route.lands)
|
|
576
|
+
if seen.screen:
|
|
577
|
+
return f"it reached {seen.title}, not {wanted}"
|
|
578
|
+
return f"it couldn't tell it reached {wanted}"
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def control_for(pack, act, snapshot):
|
|
582
|
+
"""The control for ``act`` on ``snapshot`` (``Pack.controls``), or None."""
|
|
583
|
+
spec = pack.controls.get(act)
|
|
584
|
+
if spec is None:
|
|
585
|
+
return None
|
|
586
|
+
return find(snapshot, spec.control, role=spec.role, region=spec.region)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@dataclass
|
|
590
|
+
class ReadResult:
|
|
591
|
+
kind: str # "dms", "reels" ...
|
|
592
|
+
title: str # "Your 10 newest DMs"
|
|
593
|
+
columns: tuple # ((key, label, type), ...) for the table card
|
|
594
|
+
rows: list = field(default_factory=list)
|
|
595
|
+
stopped: str = "" # why it read fewer than asked ("end of the list"), or ""
|
|
596
|
+
block: str = "table" # "table" or "media_list"
|
|
597
|
+
screen: Optional[str] = None # the screen it read on
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def collect(io, snapshot, parse, key, n, *, max_swipes=8, direction="up"):
|
|
601
|
+
"""Rows ``parse(snapshot)`` gives, across scrolls, de-duplicated by ``key(row)``, until ``n`` or the end."""
|
|
602
|
+
rows, keys = [], set()
|
|
603
|
+
for swipe in range(max_swipes + 1):
|
|
604
|
+
for row in parse(snapshot):
|
|
605
|
+
k = key(row)
|
|
606
|
+
if k in keys:
|
|
607
|
+
continue
|
|
608
|
+
keys.add(k)
|
|
609
|
+
rows.append(row)
|
|
610
|
+
if len(rows) >= n:
|
|
611
|
+
return rows[:n], "", snapshot
|
|
612
|
+
if swipe == max_swipes:
|
|
613
|
+
return rows, "it scrolled as far as it reads in one go", snapshot
|
|
614
|
+
try:
|
|
615
|
+
after = io.swipe(snapshot, direction)
|
|
616
|
+
except Lost as lost:
|
|
617
|
+
return rows, str(lost), snapshot
|
|
618
|
+
if after.content_fingerprint == snapshot.content_fingerprint:
|
|
619
|
+
return rows, "it reached the end of the list", after
|
|
620
|
+
snapshot = after
|
|
621
|
+
return rows, "", snapshot
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@dataclass(frozen=True)
|
|
625
|
+
class Verified:
|
|
626
|
+
version: str # the app version the recognizers were checked on ("412.0.0")
|
|
627
|
+
date: str # "2026-10-14"
|
|
628
|
+
device: str = "" # "iPhone 15 Pro, iOS 26.0"
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class Pack:
|
|
632
|
+
"""One app's pack. Subclasses (instagram.py) fill in the class attributes and the readers."""
|
|
633
|
+
|
|
634
|
+
name = "" # "Instagram"
|
|
635
|
+
key = "" # "instagram": the MCP tools' prefix
|
|
636
|
+
prefix = "" # "IG": the agent tools' prefix (IG_OPEN, IG_READ)
|
|
637
|
+
bundle = ""
|
|
638
|
+
verified: Optional[Verified] = None
|
|
639
|
+
screens: tuple = ()
|
|
640
|
+
routes: dict = {}
|
|
641
|
+
controls: dict = {} # act -> Control
|
|
642
|
+
readers: dict = {} # what -> Reader
|
|
643
|
+
tabs: Optional[Signal] = None # the tab bar's signal (to_tabs)
|
|
644
|
+
never = re.compile(r"(?!x)x") # the app's own words never tapped (matches nothing by default)
|
|
645
|
+
links_on = True # try a route's deep link before its steps
|
|
646
|
+
sites: tuple = () # hosts whose https links open in the app ("www.instagram.com")
|
|
647
|
+
link_paths = re.compile(r"") # the paths on those hosts a route may open (a post's, not the home feed's)
|
|
648
|
+
route_with_arg: dict = {} # a route that takes an argument when one is given ("profile" -> "user")
|
|
649
|
+
mentions = re.compile(r"(?!x)x") # words in a task that make it about this app
|
|
650
|
+
photos_notice = re.compile(r"(?!x)x") # the label of the app's notice that it sees only some photos
|
|
651
|
+
# The tools' words (tools.py, mcp.py): each tool's one-line prompt, the words OPEN and READ take, and how a read's
|
|
652
|
+
# rows are named.
|
|
653
|
+
prompts: dict = {} # "OPEN" | "READ" -> "- IG_OPEN ..."
|
|
654
|
+
open_aliases: dict = {} # a word the model may write -> a route or an act
|
|
655
|
+
read_aliases: dict = {}
|
|
656
|
+
read_default: dict = {} # what -> n when the model gives none
|
|
657
|
+
read_titles: dict = {} # what -> "DMs"
|
|
658
|
+
required: dict = {} # what -> the column keys every row has: a row without one says so
|
|
659
|
+
nouns: dict = {} # what -> "Reel": one item, for a card's title
|
|
660
|
+
mcp_open_hint = ""
|
|
661
|
+
mcp_read_hint = ""
|
|
662
|
+
|
|
663
|
+
hidden_routes: tuple = () # routes the tools reach only through another word ("user" by "profile @x")
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def open_words(self):
|
|
667
|
+
return [*(r for r in self.routes if r not in self.hidden_routes),
|
|
668
|
+
*(c for c in self.controls if c not in self.routes)]
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def read_words(self):
|
|
672
|
+
return list(self.readers)
|
|
673
|
+
|
|
674
|
+
def noun(self, what):
|
|
675
|
+
return self.nouns.get(what, "Post")
|
|
676
|
+
|
|
677
|
+
def recognize(self, snapshot):
|
|
678
|
+
return recognize(self, snapshot)
|
|
679
|
+
|
|
680
|
+
def limits_photos(self, snapshot):
|
|
681
|
+
"""Whether ``snapshot`` is this app showing its limited-photos notice (a Button or text whose whole label is
|
|
682
|
+
``photos_notice``): the app can't see most of the library, so a post's media may not be found in its picker."""
|
|
683
|
+
if str(getattr(snapshot, "bundle_id", "") or "") != self.bundle:
|
|
684
|
+
return False
|
|
685
|
+
elements = getattr(snapshot, "elements", ()) or ()
|
|
686
|
+
return any(e.role in ("StaticText", "Button", "Other") and e.label and len(e.label) <= 160
|
|
687
|
+
and self.photos_notice.search(" ".join(e.label.split())) for e in elements)
|
|
688
|
+
|
|
689
|
+
learned = None # heal.Learned: variants this Mac learned and checked, or None
|
|
690
|
+
|
|
691
|
+
def learned_specs(self):
|
|
692
|
+
"""The trusted variants heal.py learned for the app version on the phone (never for an act's control)."""
|
|
693
|
+
store = self.learned
|
|
694
|
+
return store.specs(self) if store is not None else ()
|
|
695
|
+
|
|
696
|
+
def back_control(self, snapshot):
|
|
697
|
+
"""The app's own Back control when its label isn't "Back" (iOS names it after the screen it goes back to:
|
|
698
|
+
"Settings", "Search"), or None."""
|
|
699
|
+
return None
|
|
700
|
+
|
|
701
|
+
def confirm(self, route, arg, snapshot):
|
|
702
|
+
"""None when ``snapshot`` is the screen ``route`` with ``arg`` asked for; else why not, in plain words (a
|
|
703
|
+
messaging pack checks the conversation on screen names the person asked for before it names Send)."""
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
def offered_to(self, ctx):
|
|
707
|
+
"""Whether this pack's tools are offered to the run ``ctx``: a Smart run that starts in the app or whose task
|
|
708
|
+
names it."""
|
|
709
|
+
if getattr(ctx, "engine", None) != "smart" or pins.aside(self):
|
|
710
|
+
return False
|
|
711
|
+
return (getattr(ctx, "app_bundle", None) == self.bundle
|
|
712
|
+
or bool(self.mentions.search(str(getattr(ctx, "goal", "") or ""))))
|
|
713
|
+
|
|
714
|
+
def screen_title(self, name):
|
|
715
|
+
return next((s.title for s in self.screens if s.name == name), name or "")
|
|
716
|
+
|
|
717
|
+
def read(self, io, what, n=10, arg=None):
|
|
718
|
+
"""``ReadResult`` for ``what``."""
|
|
719
|
+
reader = self.readers[what]
|
|
720
|
+
route = self.route_with_arg.get(reader.route, reader.route) if arg else reader.route
|
|
721
|
+
snapshot = io.observe()
|
|
722
|
+
seen = recognize(self, snapshot)
|
|
723
|
+
goes_to_arg = bool(arg) and route in self.routes and bool(self.routes[route].arg)
|
|
724
|
+
if goes_to_arg or seen.screen not in reader.screens:
|
|
725
|
+
if reader.route is None:
|
|
726
|
+
raise Lost(f"this isn't {' or '.join(self.screen_title(s) for s in reader.screens)}")
|
|
727
|
+
landing = navigate(self, io, route, arg)
|
|
728
|
+
if not landing.ok:
|
|
729
|
+
raise Lost(landing.why)
|
|
730
|
+
snapshot, seen = landing.snapshot, landing.seen
|
|
731
|
+
result = reader.read(self, io, snapshot, n, arg)
|
|
732
|
+
result.screen = result.screen or seen.screen
|
|
733
|
+
return result
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def number(text):
|
|
737
|
+
""""2.7M" -> 2700000, "16,767" -> 16767, "1.1K" -> 1100; None when it isn't a count."""
|
|
738
|
+
match = re.fullmatch(r"\s*(\d[\d,]*(?:\.\d+)?)\s*([KkMmBb])?\s*", str(text or ""))
|
|
739
|
+
if not match:
|
|
740
|
+
return None
|
|
741
|
+
value = float(match.group(1).replace(",", "")) * {"": 1, "K": 1e3, "M": 1e6, "B": 1e9}[
|
|
742
|
+
(match.group(2) or "").upper()]
|
|
743
|
+
return int(round(value))
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def app_version(runtime, bundle):
|
|
747
|
+
"""The version of ``bundle`` installed on the phone (installed_apps' last listing), or None."""
|
|
748
|
+
installed = getattr(runtime, "installed_apps", None)
|
|
749
|
+
try:
|
|
750
|
+
inventory = installed.snapshot() if installed is not None else None
|
|
751
|
+
except Exception:
|
|
752
|
+
return None
|
|
753
|
+
for app in getattr(inventory, "user", ()) or ():
|
|
754
|
+
if isinstance(app, dict) and app.get("bundleId") == bundle:
|
|
755
|
+
return app.get("version") or None
|
|
756
|
+
return None
|
|
757
|
+
|