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,646 @@
|
|
|
1
|
+
"""FrameClock: the phone's MJPEG stream as a clock for "the action took effect"
|
|
2
|
+
and "the screen is still".
|
|
3
|
+
|
|
4
|
+
WDA streams the screen as MJPEG at ~30 fps (``video.WdaVideo``). Reading
|
|
5
|
+
that stream costs almost nothing, while every AX read costs 130-290 ms and
|
|
6
|
+
proving a screen settled from AX alone needs a 0.5 s quiet period. This module
|
|
7
|
+
decodes each frame at reduced size in greyscale (JPEG DCT draft mode, ~0.25 ms
|
|
8
|
+
a frame), compares 16-px tiles, and answers the questions the WDA driver
|
|
9
|
+
asks around an action:
|
|
10
|
+
|
|
11
|
+
* ``mark()`` before dispatch, then ``wait_effect(token)``: the first frame that
|
|
12
|
+
differs from the pre-action frame outside regions that were already busy.
|
|
13
|
+
* ``quiet_between(token, start, end)`` / ``still_for()``: no changed tile
|
|
14
|
+
(outside constantly busy tiles: video, spinners) in a window, or since a time
|
|
15
|
+
(the settle rules in WDA.wait_for_change / observe_ready); ``wait_stable`` is
|
|
16
|
+
the blocking form.
|
|
17
|
+
* ``displacement(a, b)``: vertical/horizontal content shift between two
|
|
18
|
+
frames (scroll odometry), by row/column-profile matching.
|
|
19
|
+
|
|
20
|
+
It never decides anything on its own: the driver still takes an AX read and
|
|
21
|
+
compares structure before it accepts a screen. A stale stream cannot prove a
|
|
22
|
+
screen settled, so the caller falls back to AX polling.
|
|
23
|
+
|
|
24
|
+
A driver uses it whenever one is attached: on since the live validation of
|
|
25
|
+
23 Sep 2026 (0/32 premature settles vs 0/32 for AX alone, AX read latency
|
|
26
|
+
unchanged with the stream up).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import io
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import math
|
|
33
|
+
import os
|
|
34
|
+
import threading
|
|
35
|
+
import time
|
|
36
|
+
from collections import deque
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
|
|
39
|
+
Image = ImageChops = ImageStat = None
|
|
40
|
+
_pil_loaded = False
|
|
41
|
+
|
|
42
|
+
log = logging.getLogger("mobster.frameclock")
|
|
43
|
+
|
|
44
|
+
# Decoded frame height. JPEG draft mode scales by 1/2, 1/4 or 1/8 inside the
|
|
45
|
+
# decoder; the factor is chosen so frames land at >= this many rows whatever
|
|
46
|
+
# the MJPEG scaling (medium: 590x1278 -> 1/4 -> 148x319).
|
|
47
|
+
TARGET_ROWS = 300
|
|
48
|
+
TILE = 16 # tile side in decoded pixels (~43 pt on an iPhone 15 Pro)
|
|
49
|
+
PIXEL_THRESHOLD = 16 # grey levels; identical screens re-encode to identical bytes
|
|
50
|
+
TILE_MIN = 2 # tile mean of the 0/255 change map: >= 2 changed pixels of 256
|
|
51
|
+
# The status bar (clock, battery, Live Activities) is not the app's and changes
|
|
52
|
+
# on its own; it is cropped before any comparison. So is the right edge, where
|
|
53
|
+
# the vertical scroll indicator fades out ~1.1 s after every scroll (measured:
|
|
54
|
+
# 6 tiles changing long after the content stopped).
|
|
55
|
+
STATUS_BAR_FRACTION = .05
|
|
56
|
+
SCROLL_INDICATOR_FRACTION = .03
|
|
57
|
+
EFFECT_TILES = 2 # changed tiles (outside busy regions) that count as an effect
|
|
58
|
+
STABLE_SECONDS = .18 # no change this long after the effect = at rest
|
|
59
|
+
STABLE_MIN_FRAMES = 3 # ...seen over at least this many frames
|
|
60
|
+
BASELINE_SECONDS = 1.5 # activity before mark() that defines busy regions
|
|
61
|
+
QUIET_BEFORE_MARK = .2 # the screen must be still this long before dispatch
|
|
62
|
+
ACTIVE_RATIO = .2 # changed in more than this share of baseline frames = masked
|
|
63
|
+
PERIODIC_CHANGES = 2 # changed at least this often in the baseline = ignored for effects
|
|
64
|
+
RECENT_SECONDS = .6 # ...and still changing this recently (a caret blinks every ~0.5 s)
|
|
65
|
+
MAX_MASKED_FRACTION = .25 # more of the screen busy than this: the clock declines
|
|
66
|
+
STALE_SECONDS = .3 # newest frame older than this: unhealthy
|
|
67
|
+
MIN_FPS = 10 # frames in the last second (measured: ~28 fps, p99 gap 127 ms)
|
|
68
|
+
GAP_SECONDS = 1.0 # a longer pause between frames is logged (the settle is blind meanwhile)
|
|
69
|
+
# A pause this long is a stall: WDA could not take its screenshot ("Cannot take a screenshot within 20000 ms",
|
|
70
|
+
# Maps and Spotify on the phone, B4). Counted once per stall in ``stalls``; WDA.next_step reads it.
|
|
71
|
+
STALL_SECONDS = 3.0
|
|
72
|
+
# MJPEG arrival lags the screen by at most ~55 ms (measured against /screenshot
|
|
73
|
+
# captures during scroll momentum, iPhone 15 Pro over USB).
|
|
74
|
+
CAPTURE_LAG = .06
|
|
75
|
+
WATCH_SECONDS = 12 # a token's watch expires after this long
|
|
76
|
+
MAX_WATCHES = 8
|
|
77
|
+
HISTORY = 96 # (time, changed tiles) per frame, ~3 s; no images
|
|
78
|
+
PROFILE_STRIPS = 32 # columns kept for scroll odometry
|
|
79
|
+
MIN_SHIFT_CONFIDENCE = .3 # below this an axis reports no shift
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _pil():
|
|
83
|
+
"""Load Pillow on first use, so a start that never reads a frame doesn't import it; without it the clock reports
|
|
84
|
+
itself unavailable."""
|
|
85
|
+
global Image, ImageChops, ImageStat, _pil_loaded
|
|
86
|
+
if not _pil_loaded:
|
|
87
|
+
_pil_loaded = True
|
|
88
|
+
try:
|
|
89
|
+
from PIL import Image, ImageChops, ImageStat
|
|
90
|
+
except ImportError: # pragma: no cover - exercised only in stripped installs
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def available():
|
|
95
|
+
_pil()
|
|
96
|
+
return Image is not None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_CHANGE_LUT = [0 if value <= PIXEL_THRESHOLD else 255 for value in range(256)]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def decode(data):
|
|
103
|
+
"""JPEG bytes -> small greyscale image with the status bar cropped off."""
|
|
104
|
+
_pil()
|
|
105
|
+
image = Image.open(io.BytesIO(data))
|
|
106
|
+
if image.format != "JPEG":
|
|
107
|
+
raise ValueError("FrameClock decodes JPEG frames only")
|
|
108
|
+
width, height = image.size
|
|
109
|
+
factor = 1
|
|
110
|
+
while factor < 8 and height // (factor * 2) >= TARGET_ROWS:
|
|
111
|
+
factor *= 2
|
|
112
|
+
image.draft("L", (max(1, width // factor), max(1, height // factor)))
|
|
113
|
+
image = image.convert("L")
|
|
114
|
+
top = int(image.size[1] * STATUS_BAR_FRACTION)
|
|
115
|
+
right = image.size[0] - max(1, round(image.size[0] * SCROLL_INDICATOR_FRACTION))
|
|
116
|
+
return image.crop((0, top, right, image.size[1]))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def tile_grid(size):
|
|
120
|
+
return math.ceil(size[0] / TILE), math.ceil(size[1] / TILE)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def changed_tiles(a, b):
|
|
124
|
+
"""Indexes (row-major) of tiles where ``b`` differs from ``a``; () when equal, every tile when the sizes differ."""
|
|
125
|
+
if a is b:
|
|
126
|
+
return ()
|
|
127
|
+
if a.size != b.size:
|
|
128
|
+
columns, rows = tile_grid(b.size)
|
|
129
|
+
return tuple(range(columns * rows))
|
|
130
|
+
_pil()
|
|
131
|
+
change = ImageChops.difference(a, b).point(_CHANGE_LUT)
|
|
132
|
+
if change.getbbox() is None:
|
|
133
|
+
return ()
|
|
134
|
+
grid = change.reduce(TILE).tobytes()
|
|
135
|
+
return tuple(index for index, value in enumerate(grid) if value >= TILE_MIN)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _profile(image, strips, axis):
|
|
139
|
+
"""Mean intensity per row (axis 0) or column (axis 1), in ``strips`` bands."""
|
|
140
|
+
_pil()
|
|
141
|
+
width, height = image.size
|
|
142
|
+
return image.resize((strips, height) if axis == 0 else (width, strips), Image.BOX)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _shift_cost(a, b, shift, length, axis):
|
|
146
|
+
"""Mean absolute difference when ``b`` shows ``a`` moved by ``shift`` (toward 0)."""
|
|
147
|
+
def cut(image, start, end):
|
|
148
|
+
width, height = image.size
|
|
149
|
+
return image.crop((0, start, width, end)) if axis == 0 else image.crop((start, 0, end, height))
|
|
150
|
+
if shift >= 0:
|
|
151
|
+
x, y = cut(a, shift, length), cut(b, 0, length - shift)
|
|
152
|
+
else:
|
|
153
|
+
x, y = cut(a, 0, length + shift), cut(b, -shift, length)
|
|
154
|
+
_pil()
|
|
155
|
+
return sum(ImageStat.Stat(ImageChops.difference(x, y)).mean)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _axis_shift(a, b, axis, max_fraction=.9):
|
|
159
|
+
"""Content shift of ``b`` against ``a`` along one axis, in decoded pixels."""
|
|
160
|
+
_pil()
|
|
161
|
+
strips = PROFILE_STRIPS
|
|
162
|
+
pa, pb = _profile(a, strips, axis), _profile(b, strips, axis)
|
|
163
|
+
length = pa.size[1] if axis == 0 else pa.size[0]
|
|
164
|
+
flat = (1, length) if axis == 0 else (length, 1)
|
|
165
|
+
rows = ImageChops.difference(pa, pb).resize(flat, Image.BOX).tobytes()
|
|
166
|
+
start, end = 0, length
|
|
167
|
+
while start < end and rows[start] <= 1:
|
|
168
|
+
start += 1
|
|
169
|
+
while end > start and rows[end - 1] <= 1:
|
|
170
|
+
end -= 1
|
|
171
|
+
if end - start < 8:
|
|
172
|
+
return 0, 1.0 if end - start == 0 else 0.0
|
|
173
|
+
if axis == 0:
|
|
174
|
+
pa, pb = pa.crop((0, start, strips, end)), pb.crop((0, start, strips, end))
|
|
175
|
+
else:
|
|
176
|
+
pa, pb = pa.crop((start, 0, end, strips)), pb.crop((start, 0, end, strips))
|
|
177
|
+
span = end - start
|
|
178
|
+
limit = int(span * max_fraction)
|
|
179
|
+
overlap = max(8, int(span * .3))
|
|
180
|
+
# Exhaustive at this resolution: a coarse pass aliases on periodic lists
|
|
181
|
+
# (Settings rows repeat every ~16 decoded pixels). ~10 ms, once per swipe.
|
|
182
|
+
costs = {shift: _shift_cost(pa, pb, shift, span, axis)
|
|
183
|
+
for shift in range(-limit, limit + 1) if span - abs(shift) >= overlap}
|
|
184
|
+
if not costs:
|
|
185
|
+
return 0, 0.0
|
|
186
|
+
best = min(costs, key=costs.get)
|
|
187
|
+
ordered = sorted(costs.values())
|
|
188
|
+
typical = ordered[len(ordered) // 2] or 1e-9
|
|
189
|
+
return best, max(0.0, min(1.0, 1 - costs[best] / typical))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def displacement(a, b, axes="xy"):
|
|
193
|
+
"""Content shift from frame ``a`` to frame ``b`` as screen fractions."""
|
|
194
|
+
a = getattr(a, "image", a)
|
|
195
|
+
b = getattr(b, "image", b)
|
|
196
|
+
if a is None or b is None or a.size != b.size:
|
|
197
|
+
return None
|
|
198
|
+
if not changed_tiles(a, b):
|
|
199
|
+
return {"dy": 0.0, "dx": 0.0, "confidence": 1.0}
|
|
200
|
+
dy, vertical = _axis_shift(a, b, 0) if "y" in axes else (0, 0.0)
|
|
201
|
+
dx, horizontal = _axis_shift(a, b, 1) if "x" in axes else (0, 0.0)
|
|
202
|
+
# An axis without a clear match reports no shift: a vertical scroll changes
|
|
203
|
+
# every column profile, and the best horizontal "match" is then noise.
|
|
204
|
+
if vertical < MIN_SHIFT_CONFIDENCE:
|
|
205
|
+
dy = 0
|
|
206
|
+
if horizontal < MIN_SHIFT_CONFIDENCE:
|
|
207
|
+
dx = 0
|
|
208
|
+
width, height = a.size
|
|
209
|
+
# The status bar was cropped off; express shifts against the full screen.
|
|
210
|
+
full_height = height / (1 - STATUS_BAR_FRACTION)
|
|
211
|
+
return {"dy": round(dy / full_height, 4), "dx": round(dx / width, 4),
|
|
212
|
+
"confidence": round(max(vertical, horizontal), 3)}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class Frame:
|
|
217
|
+
seq: int
|
|
218
|
+
t: float # arrival on this Mac (monotonic)
|
|
219
|
+
image: object
|
|
220
|
+
changed: tuple = () # tiles changed against the previous frame
|
|
221
|
+
same_bytes: bool = False # byte-identical to the previous frame
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class Watch:
|
|
226
|
+
"""Everything the worker learns about one action, frame by frame."""
|
|
227
|
+
t: float
|
|
228
|
+
ref: Frame
|
|
229
|
+
stable_mask: frozenset
|
|
230
|
+
effect_mask: frozenset
|
|
231
|
+
usable: bool
|
|
232
|
+
reason: str = ""
|
|
233
|
+
effect: Frame | None = None
|
|
234
|
+
effect_tiles: int = 0
|
|
235
|
+
anchor: Frame | None = None
|
|
236
|
+
latest: Frame | None = None
|
|
237
|
+
frames_since_anchor: int = 0
|
|
238
|
+
stable_at: float | None = None # first time the at-rest condition held
|
|
239
|
+
moves: deque = field(default_factory=lambda: deque(maxlen=400)) # (t, moved) after the effect
|
|
240
|
+
# (t, bytes changed) for every frame after mark(). A static screen re-encodes
|
|
241
|
+
# to identical JPEG bytes (measured: 29/29 frames), so this sees sub-tile
|
|
242
|
+
# motion such as iOS 26's glass morph. Too strict to settle on (measured:
|
|
243
|
+
# bytes keep changing ~1-1.2 s after a Settings push, long after AX is
|
|
244
|
+
# stable), it is kept for diagnostics.
|
|
245
|
+
exact: deque = field(default_factory=lambda: deque(maxlen=400))
|
|
246
|
+
closed: bool = False
|
|
247
|
+
release_t: float | None = None # dispatch returned (+ capture lag)
|
|
248
|
+
release_frame: Frame | None = None # first frame at or after release_t
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class FrameClock:
|
|
252
|
+
"""Consumes JPEG frames from a ``WdaVideo`` relay, or its own MJPEG connection.
|
|
253
|
+
|
|
254
|
+
Thread-safe. Frames are processed on a private worker thread; the relay is
|
|
255
|
+
never blocked (it only notifies a condition). Memory is bounded: the latest
|
|
256
|
+
frame, one anchor, and per-watch reference frames, plus a short history of
|
|
257
|
+
changed-tile sets without images.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, video=None, *, wda_url=None, session=None, mjpeg_url=None,
|
|
261
|
+
clock=time.monotonic, log_path=None, start=True):
|
|
262
|
+
if not available():
|
|
263
|
+
raise RuntimeError("FrameClock needs Pillow")
|
|
264
|
+
self.clock = clock
|
|
265
|
+
self.owns_video = video is None
|
|
266
|
+
if video is None:
|
|
267
|
+
if not wda_url:
|
|
268
|
+
raise ValueError("FrameClock needs a WdaVideo relay or a WDA URL")
|
|
269
|
+
from .video import WdaVideo, default_mjpeg_url
|
|
270
|
+
mjpeg_url = mjpeg_url or default_mjpeg_url(wda_url)
|
|
271
|
+
if not mjpeg_url:
|
|
272
|
+
# A WDA port with no stream paired (8080): never another device's; the driver settles by time.
|
|
273
|
+
raise ValueError("No MJPEG stream is paired with this WDA port")
|
|
274
|
+
if isinstance(session, str):
|
|
275
|
+
session = (lambda value: lambda: value)(session)
|
|
276
|
+
video = WdaVideo(wda_url, mjpeg_url=mjpeg_url, session=session, screenshot_fallback=False)
|
|
277
|
+
self.video = video
|
|
278
|
+
self.log_path = log_path
|
|
279
|
+
self.condition = threading.Condition()
|
|
280
|
+
self.closed = False
|
|
281
|
+
self.viewer = None
|
|
282
|
+
self.worker = None
|
|
283
|
+
self.latest = None # Frame
|
|
284
|
+
self._data = None # bytes of the latest frame (identical-bytes shortcut)
|
|
285
|
+
self.anchor = None # global still-tracker anchor (no mask)
|
|
286
|
+
self.still_since = None # arrival of the last frame with a changed tile
|
|
287
|
+
self.exact_since = None # arrival of the last frame whose bytes changed
|
|
288
|
+
self.history = deque(maxlen=HISTORY) # (t, changed tiles)
|
|
289
|
+
self.arrivals = deque(maxlen=128)
|
|
290
|
+
self.decode_ms = deque(maxlen=256)
|
|
291
|
+
self.watches = []
|
|
292
|
+
self.records = deque(maxlen=256)
|
|
293
|
+
self.frames = 0
|
|
294
|
+
self.skipped = 0
|
|
295
|
+
self.errors = 0
|
|
296
|
+
self.last_seq = 0
|
|
297
|
+
self.resets = 0
|
|
298
|
+
self.stalls = 0
|
|
299
|
+
self._stalled_after = None # the seq of the last frame before a counted stall
|
|
300
|
+
self._log_lock = threading.Lock()
|
|
301
|
+
if start:
|
|
302
|
+
self.start()
|
|
303
|
+
|
|
304
|
+
def start(self):
|
|
305
|
+
with self.condition:
|
|
306
|
+
if self.worker is not None or self.closed:
|
|
307
|
+
return
|
|
308
|
+
self.viewer = self.video.subscribe()
|
|
309
|
+
self.worker = threading.Thread(target=self._run, name="mobster-frame-clock", daemon=True)
|
|
310
|
+
self.worker.start()
|
|
311
|
+
|
|
312
|
+
def close(self, timeout=2):
|
|
313
|
+
with self.condition:
|
|
314
|
+
if self.closed:
|
|
315
|
+
return
|
|
316
|
+
self.closed = True
|
|
317
|
+
viewer, self.viewer = self.viewer, None
|
|
318
|
+
self.watches.clear()
|
|
319
|
+
self.condition.notify_all()
|
|
320
|
+
if viewer is not None:
|
|
321
|
+
try:
|
|
322
|
+
self.video.unsubscribe(viewer) # wakes the worker's next_frame
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
if self.owns_video:
|
|
326
|
+
try:
|
|
327
|
+
self.video.close()
|
|
328
|
+
except Exception:
|
|
329
|
+
pass
|
|
330
|
+
worker = self.worker
|
|
331
|
+
if worker is not None and worker is not threading.current_thread():
|
|
332
|
+
worker.join(timeout)
|
|
333
|
+
|
|
334
|
+
def __enter__(self):
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
def __exit__(self, *exc):
|
|
338
|
+
self.close()
|
|
339
|
+
|
|
340
|
+
def _run(self):
|
|
341
|
+
sequence = 0
|
|
342
|
+
while True:
|
|
343
|
+
with self.condition:
|
|
344
|
+
if self.closed:
|
|
345
|
+
return
|
|
346
|
+
viewer = self.viewer
|
|
347
|
+
try:
|
|
348
|
+
frame = self.video.next_frame(viewer, sequence, timeout=.5)
|
|
349
|
+
except Exception:
|
|
350
|
+
frame = None
|
|
351
|
+
if frame is None:
|
|
352
|
+
if getattr(self.video, "closed", False):
|
|
353
|
+
return
|
|
354
|
+
self.check_stall()
|
|
355
|
+
continue
|
|
356
|
+
sequence = frame[0]
|
|
357
|
+
source = frame[5] if len(frame) > 5 else "wda_mjpeg"
|
|
358
|
+
arrival = frame[4] if len(frame) > 4 else self.clock()
|
|
359
|
+
if frame[1] != "image/jpeg" or source != "wda_mjpeg":
|
|
360
|
+
continue # Screenshot fallback frames are too slow to be a clock.
|
|
361
|
+
last = self.latest
|
|
362
|
+
if last is not None and arrival - last.t >= GAP_SECONDS:
|
|
363
|
+
self.record({"kind": "gap", "ms": round((arrival - last.t) * 1000)})
|
|
364
|
+
self.check_stall(arrival)
|
|
365
|
+
self.ingest(frame[2], arrival, sequence)
|
|
366
|
+
|
|
367
|
+
def check_stall(self, now=None):
|
|
368
|
+
"""Count a stall (no frame for STALL_SECONDS) once; True when this call counted one."""
|
|
369
|
+
with self.condition:
|
|
370
|
+
last = self.latest
|
|
371
|
+
now = self.clock() if now is None else now
|
|
372
|
+
if last is None or now - last.t < STALL_SECONDS or self._stalled_after == last.seq:
|
|
373
|
+
return False
|
|
374
|
+
self._stalled_after = last.seq
|
|
375
|
+
self.stalls += 1
|
|
376
|
+
self.record({"kind": "stall", "ms": round((now - last.t) * 1000)})
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
def ingest(self, data, arrival=None, seq=None):
|
|
380
|
+
"""Process one JPEG frame. Public so tests (and offline replays) can feed frames."""
|
|
381
|
+
arrival = self.clock() if arrival is None else arrival
|
|
382
|
+
started = time.perf_counter()
|
|
383
|
+
with self.condition:
|
|
384
|
+
previous, previous_data = self.latest, self._data
|
|
385
|
+
if seq is not None and self.last_seq and seq > self.last_seq + 1:
|
|
386
|
+
self.skipped += seq - self.last_seq - 1
|
|
387
|
+
same = previous is not None and data == previous_data
|
|
388
|
+
if same:
|
|
389
|
+
image, changed = previous.image, ()
|
|
390
|
+
else:
|
|
391
|
+
try:
|
|
392
|
+
image = decode(data)
|
|
393
|
+
except Exception:
|
|
394
|
+
self.errors += 1
|
|
395
|
+
return None
|
|
396
|
+
changed = changed_tiles(previous.image, image) if previous is not None else ()
|
|
397
|
+
current = Frame(seq if seq is not None else self.frames + 1, arrival, image, changed, same)
|
|
398
|
+
with self.condition:
|
|
399
|
+
if self.closed:
|
|
400
|
+
return None
|
|
401
|
+
if previous is not None and previous.image.size != image.size:
|
|
402
|
+
self._reset_locked()
|
|
403
|
+
self.latest, self._data = current, data
|
|
404
|
+
self.last_seq = current.seq
|
|
405
|
+
self.frames += 1
|
|
406
|
+
self.arrivals.append(arrival)
|
|
407
|
+
self.history.append((arrival, changed))
|
|
408
|
+
# Global stillness: the last frame with a changed tile (no mask) ...
|
|
409
|
+
if self.anchor is None:
|
|
410
|
+
self.anchor, self.still_since = current, arrival
|
|
411
|
+
elif changed or (not same and changed_tiles(self.anchor.image, image)):
|
|
412
|
+
self.anchor, self.still_since = current, arrival
|
|
413
|
+
# ... and, exactly, the last frame whose bytes changed.
|
|
414
|
+
if self.exact_since is None or not same:
|
|
415
|
+
self.exact_since = arrival
|
|
416
|
+
now = self.clock()
|
|
417
|
+
self.watches = [w for w in self.watches if not w.closed and now - w.t <= WATCH_SECONDS]
|
|
418
|
+
for watch in self.watches:
|
|
419
|
+
self._update_watch(watch, current)
|
|
420
|
+
self.decode_ms.append((time.perf_counter() - started) * 1000)
|
|
421
|
+
self.condition.notify_all()
|
|
422
|
+
return current
|
|
423
|
+
|
|
424
|
+
def _reset_locked(self):
|
|
425
|
+
"""Geometry changed mid-stream: every reference is void."""
|
|
426
|
+
self.resets += 1
|
|
427
|
+
self.anchor = self.still_since = None
|
|
428
|
+
self.history.clear()
|
|
429
|
+
for watch in self.watches:
|
|
430
|
+
watch.usable, watch.reason, watch.closed = False, "geometry_changed", True
|
|
431
|
+
|
|
432
|
+
def _update_watch(self, watch, frame):
|
|
433
|
+
if not watch.usable or frame.t <= watch.t:
|
|
434
|
+
return
|
|
435
|
+
watch.latest = frame
|
|
436
|
+
watch.exact.append((frame.t, not frame.same_bytes))
|
|
437
|
+
if watch.release_t is not None and watch.release_frame is None and frame.t >= watch.release_t:
|
|
438
|
+
watch.release_frame = frame
|
|
439
|
+
if watch.effect is None:
|
|
440
|
+
tiles = [t for t in changed_tiles(watch.ref.image, frame.image) if t not in watch.effect_mask]
|
|
441
|
+
if len(tiles) >= EFFECT_TILES:
|
|
442
|
+
watch.effect, watch.effect_tiles = frame, len(tiles)
|
|
443
|
+
watch.anchor, watch.frames_since_anchor = frame, 0
|
|
444
|
+
watch.moves.append((frame.t, True))
|
|
445
|
+
return
|
|
446
|
+
moved = any(t not in watch.stable_mask for t in frame.changed) or any(
|
|
447
|
+
t not in watch.stable_mask for t in changed_tiles(watch.anchor.image, frame.image))
|
|
448
|
+
watch.moves.append((frame.t, moved))
|
|
449
|
+
if moved:
|
|
450
|
+
watch.anchor, watch.frames_since_anchor = frame, 0
|
|
451
|
+
else:
|
|
452
|
+
watch.frames_since_anchor += 1
|
|
453
|
+
if (watch.stable_at is None and frame.t - watch.anchor.t >= STABLE_SECONDS
|
|
454
|
+
and watch.frames_since_anchor >= STABLE_MIN_FRAMES):
|
|
455
|
+
watch.stable_at = frame.t
|
|
456
|
+
|
|
457
|
+
def healthy(self):
|
|
458
|
+
"""Fresh frames at a usable rate. Everything else answers None when not."""
|
|
459
|
+
with self.condition:
|
|
460
|
+
return self._healthy_locked()
|
|
461
|
+
|
|
462
|
+
def _healthy_locked(self):
|
|
463
|
+
if self.closed or self.latest is None:
|
|
464
|
+
return False
|
|
465
|
+
now = self.clock()
|
|
466
|
+
if now - self.latest.t > STALE_SECONDS:
|
|
467
|
+
return False
|
|
468
|
+
return self._recent_frames_locked(now) >= MIN_FPS
|
|
469
|
+
|
|
470
|
+
def _recent_frames_locked(self, now):
|
|
471
|
+
"""Frames that arrived in the last second."""
|
|
472
|
+
return sum(1 for t in self.arrivals if now - t <= 1.0)
|
|
473
|
+
|
|
474
|
+
def stats(self):
|
|
475
|
+
with self.condition:
|
|
476
|
+
timings = sorted(self.decode_ms)
|
|
477
|
+
now = self.clock()
|
|
478
|
+
return {"healthy": self._healthy_locked(), "frames": self.frames, "skipped": self.skipped,
|
|
479
|
+
"errors": self.errors, "resets": self.resets, "stalls": self.stalls,
|
|
480
|
+
"fps": self._recent_frames_locked(now),
|
|
481
|
+
"frame_age_ms": None if self.latest is None else round((now - self.latest.t) * 1000, 1),
|
|
482
|
+
"process_ms_p50": round(timings[len(timings) // 2], 3) if timings else None,
|
|
483
|
+
"process_ms_p99": round(timings[int(len(timings) * .99)], 3) if timings else None,
|
|
484
|
+
"watches": len(self.watches)}
|
|
485
|
+
|
|
486
|
+
def _baseline_locked(self, end):
|
|
487
|
+
"""Busy tiles in the window ending at ``end``; only ongoing activity counts (a past scroll masks nothing)."""
|
|
488
|
+
counts, recent, frames = {}, set(), 0
|
|
489
|
+
for t, changed in self.history:
|
|
490
|
+
if end - BASELINE_SECONDS <= t <= end:
|
|
491
|
+
frames += 1
|
|
492
|
+
for tile in changed:
|
|
493
|
+
counts[tile] = counts.get(tile, 0) + 1
|
|
494
|
+
if t >= end - RECENT_SECONDS:
|
|
495
|
+
recent.update(changed)
|
|
496
|
+
active = frozenset(t for t, n in counts.items()
|
|
497
|
+
if frames >= 10 and n > frames * ACTIVE_RATIO and t in recent)
|
|
498
|
+
periodic = frozenset(t for t, n in counts.items() if n >= PERIODIC_CHANGES and t in recent) | active
|
|
499
|
+
return active, periodic
|
|
500
|
+
|
|
501
|
+
def mark(self):
|
|
502
|
+
"""A token for "now" taken right before dispatch; usable on a healthy stream still for QUIET_BEFORE_MARK."""
|
|
503
|
+
with self.condition:
|
|
504
|
+
now = self.clock()
|
|
505
|
+
latest = self.latest
|
|
506
|
+
if not self._healthy_locked():
|
|
507
|
+
return Watch(now, latest, frozenset(), frozenset(), False, "unhealthy", closed=True)
|
|
508
|
+
active, periodic = self._baseline_locked(latest.t)
|
|
509
|
+
columns, rows = tile_grid(latest.image.size)
|
|
510
|
+
watch = Watch(now, latest, active, periodic, True)
|
|
511
|
+
if len(periodic) > MAX_MASKED_FRACTION * columns * rows:
|
|
512
|
+
watch.usable, watch.reason, watch.closed = False, "busy_screen", True
|
|
513
|
+
elif any(t >= latest.t - QUIET_BEFORE_MARK and any(tile not in active for tile in changed)
|
|
514
|
+
for t, changed in self.history):
|
|
515
|
+
watch.usable, watch.reason, watch.closed = False, "not_still_before_action", True
|
|
516
|
+
else:
|
|
517
|
+
if len(self.watches) >= MAX_WATCHES:
|
|
518
|
+
self.watches.pop(0).closed = True
|
|
519
|
+
self.watches.append(watch)
|
|
520
|
+
return watch
|
|
521
|
+
|
|
522
|
+
def set_release(self, watch, t):
|
|
523
|
+
"""The action's request returned at ``t``."""
|
|
524
|
+
with self.condition:
|
|
525
|
+
if watch is not None and watch.release_t is None:
|
|
526
|
+
watch.release_t = t + CAPTURE_LAG
|
|
527
|
+
|
|
528
|
+
def release(self, watch):
|
|
529
|
+
if watch is None:
|
|
530
|
+
return
|
|
531
|
+
with self.condition:
|
|
532
|
+
watch.closed = True
|
|
533
|
+
if watch in self.watches:
|
|
534
|
+
self.watches.remove(watch)
|
|
535
|
+
self.condition.notify_all()
|
|
536
|
+
|
|
537
|
+
def _watch_usable_locked(self, watch):
|
|
538
|
+
return (watch is not None and watch.usable and not watch.closed
|
|
539
|
+
and self.clock() - watch.t <= WATCH_SECONDS and self._healthy_locked())
|
|
540
|
+
|
|
541
|
+
def _wait(self, watch, predicate, timeout):
|
|
542
|
+
"""Wait for ``predicate()``; None when the watch expires, closes or loses its stream."""
|
|
543
|
+
deadline = self.clock() + max(0.0, timeout)
|
|
544
|
+
with self.condition:
|
|
545
|
+
while self._watch_usable_locked(watch):
|
|
546
|
+
value = predicate()
|
|
547
|
+
if value:
|
|
548
|
+
return value
|
|
549
|
+
remaining = deadline - self.clock()
|
|
550
|
+
if remaining <= 0:
|
|
551
|
+
return None
|
|
552
|
+
self.condition.wait(min(remaining, .05))
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
def wait_effect(self, watch, timeout):
|
|
556
|
+
"""The first frame after ``mark()`` with EFFECT_TILES changed tiles outside busy regions, else None."""
|
|
557
|
+
return self._wait(watch, lambda: watch.effect, timeout)
|
|
558
|
+
|
|
559
|
+
def wait_stable(self, watch, timeout, duration=STABLE_SECONDS):
|
|
560
|
+
"""When nothing outside busy tiles changed for ``duration`` after the effect; None on timeout."""
|
|
561
|
+
def settled():
|
|
562
|
+
if watch.effect is None or watch.latest is None or watch.anchor is None:
|
|
563
|
+
return None
|
|
564
|
+
if (watch.latest.t - watch.anchor.t >= duration
|
|
565
|
+
and watch.frames_since_anchor >= STABLE_MIN_FRAMES):
|
|
566
|
+
return watch.anchor
|
|
567
|
+
return None
|
|
568
|
+
return self._wait(watch, settled, timeout)
|
|
569
|
+
|
|
570
|
+
def quiet_between(self, watch, start, end, min_frames=STABLE_MIN_FRAMES, exact=False):
|
|
571
|
+
"""After the effect, frames arrived in (start, end] (at least ``min_frames``) and none of them changed."""
|
|
572
|
+
with self.condition:
|
|
573
|
+
if not self._watch_usable_locked(watch) or watch.effect is None or watch.effect.t > start:
|
|
574
|
+
return False
|
|
575
|
+
source = watch.exact if exact else watch.moves
|
|
576
|
+
window = [moved for t, moved in source if start < t <= end]
|
|
577
|
+
return len(window) >= min_frames and not any(window)
|
|
578
|
+
|
|
579
|
+
def last_motion(self, watch):
|
|
580
|
+
with self.condition:
|
|
581
|
+
return max((t for t, moved in watch.moves if moved), default=None)
|
|
582
|
+
|
|
583
|
+
def still_for(self, exact=True):
|
|
584
|
+
"""Seconds since the stream's bytes last changed (``exact``: nothing on
|
|
585
|
+
screen changed at all, sub-tile motion included), or since a tile last
|
|
586
|
+
changed anywhere (status bar aside, no mask) when ``exact`` is False;
|
|
587
|
+
None when unhealthy. Tile stillness misses iOS 26's glass morph, which
|
|
588
|
+
changes AX (measured), so callers that skip an AX read use the default."""
|
|
589
|
+
with self.condition:
|
|
590
|
+
since = self.exact_since if exact else self.still_since
|
|
591
|
+
if not self._healthy_locked() or since is None:
|
|
592
|
+
return None
|
|
593
|
+
return self.clock() - since
|
|
594
|
+
|
|
595
|
+
def latest_frame(self):
|
|
596
|
+
with self.condition:
|
|
597
|
+
return self.latest
|
|
598
|
+
|
|
599
|
+
def record(self, entry):
|
|
600
|
+
"""Keep a settle comparison in memory and append it to the JSONL log."""
|
|
601
|
+
entry = {"at": round(time.time(), 3), **entry}
|
|
602
|
+
with self.condition:
|
|
603
|
+
self.records.append(entry)
|
|
604
|
+
log.info("frameclock %s", entry)
|
|
605
|
+
if not self.log_path:
|
|
606
|
+
return
|
|
607
|
+
try:
|
|
608
|
+
line = json.dumps(entry, sort_keys=True, default=str) + "\n"
|
|
609
|
+
with self._log_lock:
|
|
610
|
+
if os.path.exists(self.log_path) and os.path.getsize(self.log_path) > 5_000_000:
|
|
611
|
+
os.replace(self.log_path, self.log_path + ".1")
|
|
612
|
+
with open(self.log_path, "a", encoding="utf-8") as handle:
|
|
613
|
+
handle.write(line)
|
|
614
|
+
except OSError:
|
|
615
|
+
pass
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def attach_frame_clock(driver, video=None, *, wda_url=None, session=None, log_path=None, log_dir=None,
|
|
619
|
+
mjpeg_url=None):
|
|
620
|
+
"""Give a WDA driver a FrameClock for its lifetime.
|
|
621
|
+
|
|
622
|
+
``video`` is a running ``WdaVideo`` relay to subscribe to (the server's);
|
|
623
|
+
without one the clock opens its own MJPEG connection (CLI, evals). The
|
|
624
|
+
driver's ``close()`` releases it. Settle comparisons go to
|
|
625
|
+
``MOBSTER_FRAME_CLOCK_LOG``, else ``log_path``, else
|
|
626
|
+
``<log_dir>/frameclock.jsonl``. Returns the clock, or None when off or
|
|
627
|
+
unavailable; failures never prevent the run.
|
|
628
|
+
"""
|
|
629
|
+
if not available() or not hasattr(driver, "frame_clock"):
|
|
630
|
+
return None
|
|
631
|
+
log_path = (os.environ.get("MOBSTER_FRAME_CLOCK_LOG") or log_path
|
|
632
|
+
or (os.path.join(str(log_dir), "frameclock.jsonl") if log_dir else None))
|
|
633
|
+
try:
|
|
634
|
+
if video is not None:
|
|
635
|
+
if not all(hasattr(video, name) for name in ("subscribe", "unsubscribe", "next_frame")):
|
|
636
|
+
return None
|
|
637
|
+
clock = FrameClock(video, log_path=log_path)
|
|
638
|
+
else:
|
|
639
|
+
if not wda_url:
|
|
640
|
+
return None
|
|
641
|
+
clock = FrameClock(wda_url=wda_url, session=session, mjpeg_url=mjpeg_url, log_path=log_path)
|
|
642
|
+
except Exception as error:
|
|
643
|
+
log.info("frameclock unavailable: %s", type(error).__name__)
|
|
644
|
+
return None
|
|
645
|
+
driver.frame_clock = clock
|
|
646
|
+
return clock
|