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,132 @@
|
|
|
1
|
+
"""A check's ``stubs:``: fixed answers to named requests, served by the run's proxy (netproxy.py)."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import json
|
|
5
|
+
import mimetypes
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .assertions import CheckError
|
|
9
|
+
from .conditions import _file
|
|
10
|
+
|
|
11
|
+
KEYS = ("match", "respond")
|
|
12
|
+
MATCH_KEYS = ("host", "path_prefix", "method")
|
|
13
|
+
RESPOND_KEYS = ("status", "json", "file", "delay_ms")
|
|
14
|
+
METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
|
|
15
|
+
STUB_LIMIT = 50
|
|
16
|
+
DELAY_LIMIT = 30000 # ms
|
|
17
|
+
BODY_LIMIT = 8 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _keys(value, allowed, where):
|
|
21
|
+
for key in value:
|
|
22
|
+
if key not in allowed:
|
|
23
|
+
close = difflib.get_close_matches(str(key), allowed, n=1)
|
|
24
|
+
raise CheckError(f"{where}.{key} isn't a setting; " + (f"did you mean {close[0]}? " if close else "")
|
|
25
|
+
+ f"Use {', '.join(allowed)}.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _host(value, where):
|
|
29
|
+
text = value.strip().lower() if isinstance(value, str) else ""
|
|
30
|
+
bare = text[2:] if text.startswith("*.") else text
|
|
31
|
+
if not bare or any(c in bare for c in "/:*? ") or bare.startswith(".") or bare.endswith("."):
|
|
32
|
+
raise CheckError(f"{where} must be a host name such as api.example.com or *.example.com, without a scheme, "
|
|
33
|
+
"port or path.")
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse(value, base_dir=None):
|
|
38
|
+
"""The normalized ``stubs`` list of a check file: () when absent. Raises CheckError."""
|
|
39
|
+
if value is None:
|
|
40
|
+
return ()
|
|
41
|
+
if not isinstance(value, list) or not value:
|
|
42
|
+
raise CheckError("stubs must list requests to answer, such as [{match: {host: api.example.com}, "
|
|
43
|
+
"respond: {status: 200, json: {items: []}}}].")
|
|
44
|
+
if len(value) > STUB_LIMIT:
|
|
45
|
+
raise CheckError(f"stubs has more than {STUB_LIMIT} entries. Keep the ones the check needs.")
|
|
46
|
+
out = []
|
|
47
|
+
for index, item in enumerate(value):
|
|
48
|
+
where = f"stubs[{index}]"
|
|
49
|
+
if not isinstance(item, dict) or set(item) != set(KEYS):
|
|
50
|
+
raise CheckError(f"{where} needs match and respond, such as {{match: {{host: api.example.com}}, "
|
|
51
|
+
"respond: {status: 200, json: {}}}.")
|
|
52
|
+
match, respond = item["match"], item["respond"]
|
|
53
|
+
if not isinstance(match, dict) or not isinstance(respond, dict):
|
|
54
|
+
raise CheckError(f"{where}.match and {where}.respond must each be a mapping.")
|
|
55
|
+
_keys(match, MATCH_KEYS, f"{where}.match")
|
|
56
|
+
_keys(respond, RESPOND_KEYS, f"{where}.respond")
|
|
57
|
+
if "host" not in match:
|
|
58
|
+
raise CheckError(f"{where}.match needs a host.")
|
|
59
|
+
prefix = match.get("path_prefix", "/")
|
|
60
|
+
if not isinstance(prefix, str) or not prefix.startswith("/"):
|
|
61
|
+
raise CheckError(f"{where}.match.path_prefix must start with /, such as /v1/items.")
|
|
62
|
+
method = match.get("method")
|
|
63
|
+
if method is not None and (not isinstance(method, str) or method.upper() not in METHODS):
|
|
64
|
+
raise CheckError(f"{where}.match.method must be one of {', '.join(METHODS)}.")
|
|
65
|
+
status = respond.get("status", 200)
|
|
66
|
+
if isinstance(status, bool) or not isinstance(status, int) or not 100 <= status <= 599:
|
|
67
|
+
raise CheckError(f"{where}.respond.status must be a whole number from 100 to 599.")
|
|
68
|
+
delay = respond.get("delay_ms", 0)
|
|
69
|
+
if isinstance(delay, bool) or not isinstance(delay, int) or not 0 <= delay <= DELAY_LIMIT:
|
|
70
|
+
raise CheckError(f"{where}.respond.delay_ms must be a whole number of milliseconds from 0 to "
|
|
71
|
+
f"{DELAY_LIMIT}.")
|
|
72
|
+
if "json" in respond and "file" in respond:
|
|
73
|
+
raise CheckError(f"{where}.respond has both json and file. Keep one.")
|
|
74
|
+
stub = {"host": _host(match["host"], f"{where}.match.host"), "path_prefix": prefix,
|
|
75
|
+
"method": method.upper() if method else None, "status": status, "delay_ms": delay,
|
|
76
|
+
"json": None, "file": None}
|
|
77
|
+
if "json" in respond:
|
|
78
|
+
try:
|
|
79
|
+
json.dumps(respond["json"])
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
raise CheckError(f"{where}.respond.json must be plain JSON: text, numbers, lists and "
|
|
82
|
+
"objects.") from None
|
|
83
|
+
stub["json"] = respond["json"]
|
|
84
|
+
elif "file" in respond:
|
|
85
|
+
stub["file"] = _file(respond["file"], f"{where}.respond.file", base_dir)
|
|
86
|
+
if Path(stub["file"]).stat().st_size > BODY_LIMIT:
|
|
87
|
+
raise CheckError(f"{where}.respond.file is larger than {BODY_LIMIT // 1024 // 1024} MiB. Trim it.")
|
|
88
|
+
out.append(stub)
|
|
89
|
+
return tuple(out)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def to_dict(stubs, project_root=None):
|
|
93
|
+
"""The file shape of ``stubs``, with paths relative to ``project_root`` where they sit inside it."""
|
|
94
|
+
def relative(path):
|
|
95
|
+
try:
|
|
96
|
+
return str(Path(path).relative_to(Path(project_root).resolve()))
|
|
97
|
+
except (ValueError, TypeError):
|
|
98
|
+
return path
|
|
99
|
+
out = []
|
|
100
|
+
for stub in stubs:
|
|
101
|
+
match = {"host": stub["host"], "path_prefix": stub["path_prefix"]}
|
|
102
|
+
if stub["method"]:
|
|
103
|
+
match["method"] = stub["method"]
|
|
104
|
+
respond = {"status": stub["status"]}
|
|
105
|
+
if stub["json"] is not None:
|
|
106
|
+
respond["json"] = json.loads(json.dumps(stub["json"]))
|
|
107
|
+
if stub["file"]:
|
|
108
|
+
respond["file"] = relative(stub["file"])
|
|
109
|
+
if stub["delay_ms"]:
|
|
110
|
+
respond["delay_ms"] = stub["delay_ms"]
|
|
111
|
+
out.append({"match": match, "respond": respond})
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def host_matches(pattern, host):
|
|
116
|
+
host = host.lower()
|
|
117
|
+
return host.endswith(pattern[1:]) if pattern.startswith("*.") else host == pattern
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def matches(stub, host, method, path):
|
|
121
|
+
return (host_matches(stub["host"], host) and (not stub["method"] or stub["method"] == method.upper())
|
|
122
|
+
and path.startswith(stub["path_prefix"]))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def answer(stub):
|
|
126
|
+
"""(status, content type, body bytes) of a stub's response."""
|
|
127
|
+
if stub["file"]:
|
|
128
|
+
kind = mimetypes.guess_type(stub["file"])[0] or "application/octet-stream"
|
|
129
|
+
return stub["status"], kind, Path(stub["file"]).read_bytes()
|
|
130
|
+
if stub["json"] is not None:
|
|
131
|
+
return stub["status"], "application/json", json.dumps(stub["json"]).encode()
|
|
132
|
+
return stub["status"], "text/plain", b""
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .. import brand as tokens
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _between(light, dark):
|
|
7
|
+
return tuple(round((a + b) / 2) for a, b in zip(tokens.rgb(light), tokens.rgb(dark)))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# The brand's light green and coral hold white text at 4.5:1; outlines sit halfway between light and dark tones.
|
|
11
|
+
GREEN, RED = tokens.rgb(tokens.LIGHT["green"]), tokens.rgb(tokens.LIGHT["coral"])
|
|
12
|
+
GREEN_LINE = _between(tokens.LIGHT["green"], tokens.DARK["green"])
|
|
13
|
+
RED_LINE = _between(tokens.LIGHT["coral"], tokens.DARK["coral"])
|
|
14
|
+
SHOWN = tokens.rgb(tokens.LIGHT["violet"]) # violet: every element Mobster read
|
|
15
|
+
SHOWN_ALPHA = round(255 * .3)
|
|
16
|
+
TINT_ALPHA = round(255 * .10)
|
|
17
|
+
FONT_CANDIDATES = ("/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Helvetica.ttc",
|
|
18
|
+
"/Library/Fonts/Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
|
|
19
|
+
FRAME_CSS_WIDTH = 280 # CSS px: the verify report's frame; the suite report's is 264
|
|
20
|
+
MAX_PILLS = 6
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _font(size):
|
|
24
|
+
from PIL import ImageFont
|
|
25
|
+
for path in FONT_CANDIDATES:
|
|
26
|
+
try:
|
|
27
|
+
font = ImageFont.truetype(path, size)
|
|
28
|
+
except OSError:
|
|
29
|
+
continue
|
|
30
|
+
try:
|
|
31
|
+
font.set_variation_by_name("Bold") # SF is a variable font; others keep their one weight
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return font
|
|
35
|
+
try:
|
|
36
|
+
return ImageFont.load_default(size=size)
|
|
37
|
+
except TypeError:
|
|
38
|
+
return ImageFont.load_default()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pill_numbers(items):
|
|
42
|
+
"""Which assertions get a pill: ``items`` is [(ok, has_node)] in the check's order."""
|
|
43
|
+
ranked = sorted((index for index, (ok, has_node) in enumerate(items) if has_node),
|
|
44
|
+
key=lambda index: (bool(items[index][0]), index))
|
|
45
|
+
return set(ranked[:MAX_PILLS])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def draw_overlay(frame_path, tree, asserted, out_path):
|
|
49
|
+
"""The accessibility overlay: ``asserted`` is [(AssertionResult, [node paths])] in the check's order."""
|
|
50
|
+
from PIL import Image, ImageDraw, ImageFilter
|
|
51
|
+
with Image.open(frame_path) as source:
|
|
52
|
+
base = source.convert("RGBA")
|
|
53
|
+
width, height = base.size
|
|
54
|
+
scale = width / (tree.size[0] or 1)
|
|
55
|
+
k = width / FRAME_CSS_WIDTH # image px per CSS px at the report's size
|
|
56
|
+
layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
|
|
57
|
+
draw = ImageDraw.Draw(layer)
|
|
58
|
+
|
|
59
|
+
def box(rect, grow=0):
|
|
60
|
+
x, y, w, h = rect
|
|
61
|
+
left, top = max(0, x * scale - grow), max(0, y * scale - grow)
|
|
62
|
+
right, bottom = min(width - 1, (x + w) * scale + grow), min(height - 1, (y + h) * scale + grow)
|
|
63
|
+
return (left, top, right, bottom) if right > left and bottom > top else None
|
|
64
|
+
|
|
65
|
+
def rounded(area, radius, **style):
|
|
66
|
+
radius = max(0, min(radius, (area[2] - area[0]) / 2, (area[3] - area[1]) / 2))
|
|
67
|
+
draw.rounded_rectangle(area, radius=radius, **style)
|
|
68
|
+
|
|
69
|
+
shown = tree.shown()
|
|
70
|
+
for node in shown:
|
|
71
|
+
area = box(node.rect)
|
|
72
|
+
# Not the containers that fill the screen: their edges would only rule lines across it.
|
|
73
|
+
if area and _area(area) < .2 * width * height:
|
|
74
|
+
rounded(area, 3 * k, outline=SHOWN + (SHOWN_ALPHA,), width=max(1, round(.75 * k)))
|
|
75
|
+
grow, stroke = round(2.5 * k), max(2, round(1.5 * k))
|
|
76
|
+
items = []
|
|
77
|
+
for result, paths in asserted:
|
|
78
|
+
nodes = [node for node in (tree.by_path(path) for path in paths) if node]
|
|
79
|
+
areas = sorted({area for area in (box(node.rect, grow) for node in nodes) if area},
|
|
80
|
+
key=lambda area: (area[1], area[0]))
|
|
81
|
+
# An element inside another the same assertion matched (a switch's own label) is outlined once, outside.
|
|
82
|
+
areas = [area for area in areas if not any(other != area and _contains(other, area) for other in areas)]
|
|
83
|
+
items.append((result, areas))
|
|
84
|
+
# Held first, failed last, so a red outline is never under a green one.
|
|
85
|
+
for result, areas in sorted(items, key=lambda item: item[0].ok, reverse=True):
|
|
86
|
+
line = (GREEN_LINE if result.ok else RED_LINE) + (255,)
|
|
87
|
+
tint = (GREEN_LINE if result.ok else RED_LINE) + (TINT_ALPHA,)
|
|
88
|
+
for area in areas:
|
|
89
|
+
rounded(area, 7 * k, fill=tint, outline=line, width=stroke)
|
|
90
|
+
numbered = sorted(pill_numbers([(result.ok, bool(areas)) for result, areas in items]),
|
|
91
|
+
key=lambda index: (items[index][0].ok, index))
|
|
92
|
+
asserted_areas = [area for _, areas in items for area in areas]
|
|
93
|
+
edges = base.convert("L").filter(ImageFilter.FIND_EDGES)
|
|
94
|
+
font = _font(max(10, round(12 * k)))
|
|
95
|
+
placed = []
|
|
96
|
+
jobs = [(index, 0) for index in numbered] + [(index, n) for index in numbered if not items[index][0].ok
|
|
97
|
+
for n in range(1, min(3, len(items[index][1])))]
|
|
98
|
+
for index, n in jobs:
|
|
99
|
+
if n and len(placed) >= MAX_PILLS + 2:
|
|
100
|
+
break
|
|
101
|
+
result, areas = items[index]
|
|
102
|
+
_pill(draw, font, index + 1, result.ok, areas[n], (width, height), k, placed,
|
|
103
|
+
[area for area in asserted_areas if area != areas[n]], edges)
|
|
104
|
+
Image.alpha_composite(base, layer).convert("RGB").save(out_path, "JPEG", quality=85)
|
|
105
|
+
return Path(out_path)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _overlap(a, b):
|
|
109
|
+
return max(0, min(a[2], b[2]) - max(a[0], b[0])) * max(0, min(a[3], b[3]) - max(a[1], b[1]))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _area(a):
|
|
113
|
+
return max(0, a[2] - a[0]) * max(0, a[3] - a[1])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _contains(outer, inner):
|
|
117
|
+
return outer[0] <= inner[0] and outer[1] <= inner[1] and outer[2] >= inner[2] and outer[3] >= inner[3]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pill(draw, font, number, ok, area, size, k, placed, others, edges):
|
|
121
|
+
from PIL import ImageStat
|
|
122
|
+
text = str(number)
|
|
123
|
+
left, top, right, bottom = draw.textbbox((0, 0), text, font=font, anchor="ls")
|
|
124
|
+
pill_h = round(18 * k)
|
|
125
|
+
pad, mark, gap, ring = round(5.5 * k), round(7.5 * k), round(3 * k), max(1, round(k))
|
|
126
|
+
pill_w = pad + mark + gap + (right - left) + pad
|
|
127
|
+
space = round(1.5 * k) # outside, the pill touches the outline, so it reads as the outline's own label
|
|
128
|
+
x0, y0, x1, y1 = area
|
|
129
|
+
middle = (y0 + y1) / 2 - pill_h / 2 if y1 - y0 < 2 * pill_h else y0
|
|
130
|
+
inset = round(8 * k)
|
|
131
|
+
candidates = [(x0, y0 - space - pill_h, 0), (x0 - space - pill_w, middle, 0), (x1 + space, middle, 0),
|
|
132
|
+
(x0, y1 + space, 0), (x1 - pill_w, y0 - space - pill_h, 0), (x1 - pill_w, y1 + space, 0),
|
|
133
|
+
(x0 + inset, y0 - pill_h / 2, 2), (x1 - inset - pill_w, y0 - pill_h / 2, 2),
|
|
134
|
+
(x0 + space, y0 + space, 4), (x1 - space - pill_w, y0 + space, 4)]
|
|
135
|
+
best = None
|
|
136
|
+
for order, (x, y, own_cost) in enumerate(candidates):
|
|
137
|
+
if x < ring or y < ring or x + pill_w > size[0] - ring or y + pill_h > size[1] - ring:
|
|
138
|
+
continue
|
|
139
|
+
rect = (x - ring, y - ring, x + pill_w + ring, y + pill_h + ring)
|
|
140
|
+
if any(_overlap(rect, other) for other in placed):
|
|
141
|
+
continue
|
|
142
|
+
whole = _area(rect)
|
|
143
|
+
busy = ImageStat.Stat(edges.crop(tuple(round(v) for v in rect))).mean[0]
|
|
144
|
+
cost = (busy + 60 * sum(_overlap(rect, other) for other in others) / whole
|
|
145
|
+
+ own_cost * _overlap(rect, area) / whole + .3 * order)
|
|
146
|
+
if best is None or cost < best[0]:
|
|
147
|
+
best = (cost, x, y)
|
|
148
|
+
if busy < 3 and not any(_overlap(rect, other) for other in others) and own_cost == 0:
|
|
149
|
+
break
|
|
150
|
+
if best is None: # nowhere free: the top corner inside, clamped to the frame
|
|
151
|
+
x = min(max(ring, x0 + space), size[0] - pill_w - ring)
|
|
152
|
+
y = min(max(ring, y0 + space), size[1] - pill_h - ring)
|
|
153
|
+
for _ in range(12):
|
|
154
|
+
rect = (x - ring, y - ring, x + pill_w + ring, y + pill_h + ring)
|
|
155
|
+
if not any(_overlap(rect, other) for other in placed):
|
|
156
|
+
break
|
|
157
|
+
y = min(y + pill_h + 2 * ring, size[1] - pill_h - ring)
|
|
158
|
+
else:
|
|
159
|
+
_, x, y = best
|
|
160
|
+
placed.append((x - ring, y - ring, x + pill_w + ring, y + pill_h + ring))
|
|
161
|
+
radius = pill_h / 2
|
|
162
|
+
draw.rounded_rectangle((x - ring, y - ring, x + pill_w + ring, y + pill_h + ring), radius=radius + ring,
|
|
163
|
+
fill=(255, 255, 255, 235))
|
|
164
|
+
draw.rounded_rectangle((x, y, x + pill_w, y + pill_h), radius=radius, fill=(GREEN if ok else RED) + (255,))
|
|
165
|
+
white, line = (255, 255, 255, 255), max(2, round(1.5 * k))
|
|
166
|
+
gx, gy = x + pad, y + (pill_h - mark) / 2
|
|
167
|
+
if ok:
|
|
168
|
+
draw.line([(gx + .1 * mark, gy + .55 * mark), (gx + .4 * mark, gy + .82 * mark),
|
|
169
|
+
(gx + .92 * mark, gy + .2 * mark)], fill=white, width=line, joint="curve")
|
|
170
|
+
else:
|
|
171
|
+
draw.line([(gx + .12 * mark, gy + .12 * mark), (gx + .88 * mark, gy + .88 * mark)], fill=white, width=line)
|
|
172
|
+
draw.line([(gx + .88 * mark, gy + .12 * mark), (gx + .12 * mark, gy + .88 * mark)], fill=white, width=line)
|
|
173
|
+
draw.text((gx + mark + gap - left, y + pill_h / 2 - (top + bottom) / 2), text, font=font, fill=white,
|
|
174
|
+
anchor="ls")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""What a check measures: launch time, the app's memory, and hangs (docs/testing.mdx, "What a check measures").
|
|
2
|
+
|
|
3
|
+
Everything here is cheap enough to run on every check: one `ps` every 500 ms, the accessibility reads the run already
|
|
4
|
+
makes, and the app's own log. A number Mobster could not measure is None and is named in ``notMeasured``; a budget on
|
|
5
|
+
a number that is None never passes (see ``judge``)."""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
SAMPLE_EVERY = .5
|
|
15
|
+
HANG_MS = 250
|
|
16
|
+
BUDGET_KEYS = ("launch_ms", "memory_mb", "hangs")
|
|
17
|
+
# The app's runloop monitor: "Hang detected: 0.52s (debugger attached, not reporting)".
|
|
18
|
+
HANG_LINE = re.compile(r"Hang detected: (?P<seconds>\d+(?:\.\d+)?)\s*s\b")
|
|
19
|
+
PS = "/bin/ps"
|
|
20
|
+
NOT_MEASURED = {
|
|
21
|
+
"firstFrame": "A simulator logs no line for the app's first frame, so only the first stable screen is timed.",
|
|
22
|
+
"droppedFrames": "A simulator exposes no Core Animation commit times, so dropped frames are not counted.",
|
|
23
|
+
"iphoneMemory": ("On an iPhone, memory needs `pymobiledevice3 developer dvt sysmon`, which Mobster does not "
|
|
24
|
+
"call yet."),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_ps(text):
|
|
29
|
+
"""(rss_mb, cpu_percent) from `ps -o rss=,pcpu=` for one process; None when the line is not that."""
|
|
30
|
+
parts = (text or "").split()
|
|
31
|
+
if len(parts) != 2:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
rss_kb, cpu = int(parts[0]), float(parts[1])
|
|
35
|
+
except ValueError:
|
|
36
|
+
return None
|
|
37
|
+
if rss_kb <= 0 or cpu < 0 or not math.isfinite(cpu):
|
|
38
|
+
return None
|
|
39
|
+
return rss_kb / 1024, cpu
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ps_sample(pid, *, run=subprocess.run):
|
|
43
|
+
"""One reading of a simulator app's process (a simulator's processes are the Mac's own); None once it has gone."""
|
|
44
|
+
try:
|
|
45
|
+
done = run([PS, "-o", "rss=,pcpu=", "-p", str(int(pid))], capture_output=True, text=True, timeout=2,
|
|
46
|
+
stdin=subprocess.DEVNULL)
|
|
47
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
48
|
+
return None
|
|
49
|
+
return parse_ps(done.stdout) if done.returncode == 0 else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def percentile(values, share):
|
|
53
|
+
"""The nearest-rank percentile of ``values`` (0 < share <= 1)."""
|
|
54
|
+
ordered = sorted(values)
|
|
55
|
+
return ordered[max(0, math.ceil(share * len(ordered)) - 1)]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def memory_summary(samples):
|
|
59
|
+
"""{peakMb, p95Mb, cpuPeakPct, samples} over (rss_mb, cpu) samples; None when there are none."""
|
|
60
|
+
if not samples:
|
|
61
|
+
return None
|
|
62
|
+
memory = [mb for mb, _ in samples]
|
|
63
|
+
return {"peakMb": round(max(memory), 1), "p95Mb": round(percentile(memory, .95), 1),
|
|
64
|
+
"cpuPeakPct": round(max(cpu for _, cpu in samples), 1), "samples": len(samples)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Sampler:
|
|
68
|
+
"""Reads the app's process when it starts, every SAMPLE_EVERY seconds on a thread of its own while the steps run,
|
|
69
|
+
and once more when it stops: a launch-only check that settles in a second still has its readings (26 Oct 2026,
|
|
70
|
+
iOS 26.4 simulator: runs of about 1.3 s got 2, 2, 0 and 1 samples with the start and the thread alone)."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, pid_of, *, sample=None, every=SAMPLE_EVERY):
|
|
73
|
+
self._pid_of, self._sample, self._every = pid_of, sample or ps_sample, every
|
|
74
|
+
self.samples = []
|
|
75
|
+
self._stop = threading.Event()
|
|
76
|
+
self._thread = None
|
|
77
|
+
|
|
78
|
+
def start(self):
|
|
79
|
+
"""The first reading is taken here, so a run that ends at once still has one."""
|
|
80
|
+
self._take()
|
|
81
|
+
self._thread = threading.Thread(target=self._run, name="mobster-perf-sampler", daemon=True)
|
|
82
|
+
self._thread.start()
|
|
83
|
+
|
|
84
|
+
def _take(self):
|
|
85
|
+
try:
|
|
86
|
+
pid = self._pid_of()
|
|
87
|
+
reading = self._sample(pid) if pid else None
|
|
88
|
+
except Exception: # a sample never costs a run
|
|
89
|
+
reading = None
|
|
90
|
+
if reading is not None:
|
|
91
|
+
self.samples.append(reading)
|
|
92
|
+
|
|
93
|
+
def _run(self):
|
|
94
|
+
while not self._stop.wait(self._every):
|
|
95
|
+
self._take()
|
|
96
|
+
|
|
97
|
+
def stop(self):
|
|
98
|
+
self._stop.set()
|
|
99
|
+
if self._thread is not None:
|
|
100
|
+
self._thread.join(2)
|
|
101
|
+
self._take()
|
|
102
|
+
return memory_summary(list(self.samples))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def hang_summary(texts, *, threshold_ms=HANG_MS):
|
|
106
|
+
"""{count, longestMs, thresholdMs} over the app's log lines: the hang reports of ``threshold_ms`` or longer."""
|
|
107
|
+
found = []
|
|
108
|
+
for text in texts:
|
|
109
|
+
match = HANG_LINE.search(text or "")
|
|
110
|
+
if match and float(match.group("seconds")) * 1000 >= threshold_ms:
|
|
111
|
+
found.append(round(float(match.group("seconds")) * 1000))
|
|
112
|
+
return {"count": len(found), "longestMs": max(found, default=0), "thresholdMs": threshold_ms}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build(*, launch, sampler_summary, hangs, simulator):
|
|
116
|
+
"""The result's ``perf``: the four numbers, the Mac's 1-minute load when they were taken, and what isn't
|
|
117
|
+
measured."""
|
|
118
|
+
missing = {"firstFrame": NOT_MEASURED["firstFrame"]} if simulator else {}
|
|
119
|
+
if simulator:
|
|
120
|
+
missing["droppedFrames"] = NOT_MEASURED["droppedFrames"]
|
|
121
|
+
else:
|
|
122
|
+
missing["droppedFrames"] = "Dropped frames are not measured on an iPhone."
|
|
123
|
+
missing["iphoneMemory"] = NOT_MEASURED["iphoneMemory"]
|
|
124
|
+
try:
|
|
125
|
+
load = round(os.getloadavg()[0], 1)
|
|
126
|
+
except (OSError, AttributeError):
|
|
127
|
+
load = None
|
|
128
|
+
return {"launch": launch, "memory": sampler_summary, "hangs": hangs, "loadAverage": load,
|
|
129
|
+
"notMeasured": missing}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class Breach:
|
|
134
|
+
key: str
|
|
135
|
+
message: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def judge(budget, perf):
|
|
139
|
+
"""(breaches, unmeasured): which of a check's ``budget`` limits the run broke, and which it could not judge.
|
|
140
|
+
|
|
141
|
+
A limit on a number that was not measured is unmeasured, never met: a run that cannot show it stayed inside the
|
|
142
|
+
budget does not pass it."""
|
|
143
|
+
perf = perf or {}
|
|
144
|
+
values = {
|
|
145
|
+
"launch_ms": ((perf.get("launch") or {}).get("interactiveMs"), "Cold launch to the first stable screen",
|
|
146
|
+
"{:,.0f} ms", "launch time"),
|
|
147
|
+
"memory_mb": ((perf.get("memory") or {}).get("peakMb"), "Peak memory", "{:,.0f} MB", "memory"),
|
|
148
|
+
"hangs": (None if perf.get("hangs") is None else perf["hangs"]["count"], "Hangs of "
|
|
149
|
+
f"{HANG_MS} ms or longer", "{:,.0f}", "hangs"),
|
|
150
|
+
}
|
|
151
|
+
breaches, unmeasured = [], []
|
|
152
|
+
for key in BUDGET_KEYS:
|
|
153
|
+
limit = (budget or {}).get(key)
|
|
154
|
+
if limit is None:
|
|
155
|
+
continue
|
|
156
|
+
value, what, form, short = values[key]
|
|
157
|
+
if value is None:
|
|
158
|
+
verb = "were" if short == "hangs" else "was"
|
|
159
|
+
unmeasured.append(Breach(key, f"{short.capitalize()} {verb} not measured, so budget.{key} could not be "
|
|
160
|
+
"checked."))
|
|
161
|
+
elif value > limit:
|
|
162
|
+
breaches.append(Breach(key, f"{what} was {form.format(value)}, over budget.{key} of {form.format(limit)}."))
|
|
163
|
+
return breaches, unmeasured
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# What history keeps of a run's perf, and the unit each is shown in.
|
|
167
|
+
METRICS = {"launchMs": "ms", "memoryMb": "MB", "hangs": ""}
|
|
168
|
+
REGRESS_AT, BASELINE_RUNS, BASELINE_MIN = .20, 10, 3
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def clean(found):
|
|
172
|
+
"""``found`` ({launchMs, memoryMb, hangs}) with only the finite, non-negative numbers; the boundary for history."""
|
|
173
|
+
found = found if isinstance(found, dict) else {}
|
|
174
|
+
return {key: found[key] for key in METRICS
|
|
175
|
+
if isinstance(found.get(key), (int, float)) and not isinstance(found[key], bool)
|
|
176
|
+
and math.isfinite(found[key]) and found[key] >= 0}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def numbers(perf):
|
|
180
|
+
"""{launchMs, memoryMb, hangs} of a result's ``perf``: only the ones that were measured."""
|
|
181
|
+
perf = perf if isinstance(perf, dict) else {}
|
|
182
|
+
return clean({"launchMs": (perf.get("launch") or {}).get("interactiveMs"),
|
|
183
|
+
"memoryMb": (perf.get("memory") or {}).get("peakMb"),
|
|
184
|
+
"hangs": (perf.get("hangs") or {}).get("count")})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compare(metric, latest, before):
|
|
188
|
+
"""(arrow, change) of ``latest`` against the median of ``before`` (a list of earlier passes): ↑ when it is 20 %
|
|
189
|
+
more (a hang count: any more), ↓ when 20 % less, else →; None with fewer than BASELINE_MIN earlier values."""
|
|
190
|
+
before = [value for value in before if value is not None]
|
|
191
|
+
if latest is None or len(before) < BASELINE_MIN:
|
|
192
|
+
return None
|
|
193
|
+
ordered = sorted(before)
|
|
194
|
+
middle = len(ordered) // 2
|
|
195
|
+
base = ordered[middle] if len(ordered) % 2 else (ordered[middle - 1] + ordered[middle]) / 2
|
|
196
|
+
if metric == "hangs":
|
|
197
|
+
arrow = "↑" if latest > base else "↓" if latest < base else "→"
|
|
198
|
+
elif base <= 0:
|
|
199
|
+
return None
|
|
200
|
+
else:
|
|
201
|
+
change = (latest - base) / base
|
|
202
|
+
arrow = "↑" if change > REGRESS_AT else "↓" if change < -REGRESS_AT else "→"
|
|
203
|
+
return arrow, {"latest": latest, "baseline": round(base, 1), "runs": len(before)}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def trend(runs):
|
|
207
|
+
"""{metric: {arrow, latest, baseline, runs}} for the newest of ``runs`` (history's, oldest first) against the
|
|
208
|
+
median of the last BASELINE_RUNS passes before it. ↑ is a regression: more time, memory or hangs."""
|
|
209
|
+
if not runs:
|
|
210
|
+
return {}
|
|
211
|
+
*earlier, latest = runs
|
|
212
|
+
passes = [run for run in earlier if run["status"] == "passed"][-BASELINE_RUNS:]
|
|
213
|
+
out = {}
|
|
214
|
+
for metric in METRICS:
|
|
215
|
+
found = compare(metric, (latest.get("perf") or {}).get(metric),
|
|
216
|
+
[(run.get("perf") or {}).get(metric) for run in passes])
|
|
217
|
+
if found is not None:
|
|
218
|
+
out[metric] = {"arrow": found[0], **found[1]}
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def line(found, trends=None):
|
|
223
|
+
"""One line for a report: "launch 812 ms ↑, memory 214 MB →, hangs 0"; "" when nothing was measured."""
|
|
224
|
+
values, trends = clean(found), trends or {}
|
|
225
|
+
parts = []
|
|
226
|
+
for metric, word in (("launchMs", "launch"), ("memoryMb", "memory"), ("hangs", "hangs")):
|
|
227
|
+
if metric in values:
|
|
228
|
+
value = values[metric]
|
|
229
|
+
unit = METRICS[metric]
|
|
230
|
+
text = f"{word} {value:,.0f}" + (f" {unit}" if unit else "")
|
|
231
|
+
arrow = (trends.get(metric) or {}).get("arrow")
|
|
232
|
+
parts.append(text + (f" {arrow}" if arrow else ""))
|
|
233
|
+
return ", ".join(parts)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def last_run(row):
|
|
237
|
+
"""`mobster test --history`'s second line for a check, its last run's numbers and markers, or nothing."""
|
|
238
|
+
measured = line(row.get("perfLatest") or {}, row.get("perf"))
|
|
239
|
+
return f"\n Last run: {measured}." if measured else ""
|