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,436 @@
|
|
|
1
|
+
"""Choosing a project's folder, and reading what is in one.
|
|
2
|
+
|
|
3
|
+
``choose(text)`` turns what the person picked or typed into the folder a project is, or refuses with a sentence.
|
|
4
|
+
``summary(path)`` is what the Projects list shows: the folder's state, its apps, how many checks and workflow files
|
|
5
|
+
its `.mobster/` holds, its simulator matrix and its branch. ``detail(path)`` adds what costs more to read: the
|
|
6
|
+
checks by name and whether the branch has changes. Both read the folder each time and never write to it.
|
|
7
|
+
|
|
8
|
+
Every git call runs with no terminal prompt, no optional locks (so Mobster never takes the index lock from under
|
|
9
|
+
your own git) and no fsmonitor hook (so a folder's git config can't make Mobster run a program), and gives up after
|
|
10
|
+
3 seconds.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from ..api_errors import APIError
|
|
22
|
+
|
|
23
|
+
MAX_PATH = 1024
|
|
24
|
+
MAX_APPS = 8
|
|
25
|
+
MAX_LISTED_CHECKS = 200
|
|
26
|
+
GIT_TIMEOUT = 3
|
|
27
|
+
PBXPROJ_LIMIT = 8_000_000
|
|
28
|
+
# Folders an app's sources don't live in: never searched for an Xcode project.
|
|
29
|
+
NOT_SOURCES = frozenset({"node_modules", "Pods", "Carthage", "DerivedData", "build", "dist", "vendor"})
|
|
30
|
+
# Folders that hold everything rather than one app: refused as a project.
|
|
31
|
+
BROAD = ("Desktop", "Documents", "Downloads", "Library", "Movies", "Music", "Pictures", "Public", "Applications")
|
|
32
|
+
SYSTEM = ("/", "/Users", "/System", "/Library", "/Applications", "/Volumes", "/private", "/usr", "/opt", "/tmp",
|
|
33
|
+
"/var", "/bin", "/sbin", "/etc", "/cores")
|
|
34
|
+
WORKFLOW_SUFFIXES = (".yaml", ".yml")
|
|
35
|
+
JSON_LIMIT = 1_000_000
|
|
36
|
+
ICON_DEPTH = 4
|
|
37
|
+
ICON_FOLDERS = 2000
|
|
38
|
+
ICON_LIMIT = 4_000_000
|
|
39
|
+
NOT_IPHONE = ("mac", "watch", "watch-marketing", "car", "tv", "vision", "ipad")
|
|
40
|
+
BUNDLE = re.compile(r'PRODUCT_BUNDLE_IDENTIFIER = "?([^";\s]+)"?;')
|
|
41
|
+
GITHUB = re.compile(r"(?:https://(?:[^@/]+@)?github\.com/|ssh://git@github\.com/|git@github\.com:)"
|
|
42
|
+
r"([A-Za-z0-9-]{1,39})/([A-Za-z0-9._-]{1,100}?)(?:\.git)?/?$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def shown(path, home=None):
|
|
46
|
+
"""``path`` as a sentence shows it: ~/… inside the home folder."""
|
|
47
|
+
home = Path(home or Path.home())
|
|
48
|
+
path = Path(path)
|
|
49
|
+
try:
|
|
50
|
+
return "~/" + path.relative_to(home).as_posix() if path != home else "~"
|
|
51
|
+
except ValueError:
|
|
52
|
+
return str(path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def choose(text, *, home=None, data_dir=None):
|
|
56
|
+
"""The folder ``text`` names, resolved: an absolute path (~ allowed) to a folder Mobster can read that holds one
|
|
57
|
+
project. A `.mobster` folder means the folder holding it. APIError with what to do otherwise."""
|
|
58
|
+
if not isinstance(text, str) or not text.strip() or len(text) > MAX_PATH or "\0" in text:
|
|
59
|
+
raise APIError("Give the folder as a full path, such as ~/Developer/YourApp.", 400, "invalid_path")
|
|
60
|
+
home = Path(home or Path.home()).resolve()
|
|
61
|
+
given = Path(text.strip()).expanduser()
|
|
62
|
+
if not given.is_absolute():
|
|
63
|
+
raise APIError("Give the folder as a full path, such as ~/Developer/YourApp.", 400, "invalid_path")
|
|
64
|
+
try:
|
|
65
|
+
path = given.resolve(strict=True)
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
raise APIError(f"There's no folder at {shown(given, home)}. Check the path, or choose the folder again.",
|
|
68
|
+
404, "missing") from None
|
|
69
|
+
except (OSError, RuntimeError):
|
|
70
|
+
raise APIError(f"Mobster can't read {shown(given, home)}. Check the path, or choose the folder again.",
|
|
71
|
+
403, "unreadable") from None
|
|
72
|
+
if path.name == ".mobster":
|
|
73
|
+
path = path.parent
|
|
74
|
+
if not path.is_dir():
|
|
75
|
+
raise APIError(f"{shown(path, home)} is a file. Choose the folder that holds your app.", 400, "not_folder")
|
|
76
|
+
system = {Path(p).resolve() for p in SYSTEM}
|
|
77
|
+
if path in system or path == home or path.parent == home and path.name in BROAD:
|
|
78
|
+
raise APIError(f"{shown(path, home)} holds more than one project. Choose your app's own folder inside it.",
|
|
79
|
+
400, "too_broad")
|
|
80
|
+
if data_dir is not None:
|
|
81
|
+
mine = Path(data_dir).resolve()
|
|
82
|
+
if path == mine or mine in path.parents:
|
|
83
|
+
raise APIError("That's Mobster's own data folder. Choose your app's folder.", 400, "mobster_data")
|
|
84
|
+
unreadable = (f"Mobster can't read {shown(path, home)}. Allow Mobster in System Settings › Privacy & "
|
|
85
|
+
"Security › Files and Folders, then add it again.")
|
|
86
|
+
if not os.access(path, os.R_OK | os.X_OK):
|
|
87
|
+
raise APIError(unreadable, 403, "unreadable")
|
|
88
|
+
try:
|
|
89
|
+
next(path.iterdir(), None)
|
|
90
|
+
except OSError:
|
|
91
|
+
raise APIError(unreadable, 403, "unreadable") from None
|
|
92
|
+
repos = _repositories_in(path)
|
|
93
|
+
if repos >= 2:
|
|
94
|
+
raise APIError(f"{shown(path, home)} holds {repos} repositories. Choose the one with your app.", 400,
|
|
95
|
+
"too_broad")
|
|
96
|
+
return path
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _repositories_in(path, limit=200):
|
|
100
|
+
"""How many folders directly in ``path`` are repositories or projects, when ``path`` is neither itself and has
|
|
101
|
+
no app at its root; else 0."""
|
|
102
|
+
try:
|
|
103
|
+
if any((path / name).exists() for name in (".git", ".mobster", "package.json")) \
|
|
104
|
+
or any(path.glob("*.xcodeproj")) or any(path.glob("*.xcworkspace")):
|
|
105
|
+
return 0
|
|
106
|
+
count = 0
|
|
107
|
+
for index, child in enumerate(path.iterdir()):
|
|
108
|
+
if index >= limit:
|
|
109
|
+
break
|
|
110
|
+
if child.is_dir() and ((child / ".git").exists() or (child / ".mobster").is_dir()):
|
|
111
|
+
count += 1
|
|
112
|
+
return count
|
|
113
|
+
except OSError:
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# -- reading a folder -------------------------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def state(path, home=None):
|
|
120
|
+
"""("ready" | "missing" | "unreadable", a sentence or None)."""
|
|
121
|
+
path = Path(path)
|
|
122
|
+
try:
|
|
123
|
+
if not path.is_dir():
|
|
124
|
+
return "missing", (f"The folder {shown(path, home)} isn't there any more. Move it back, or remove the "
|
|
125
|
+
"project and add the folder where it is now.")
|
|
126
|
+
next(path.iterdir(), None)
|
|
127
|
+
except PermissionError:
|
|
128
|
+
return "unreadable", (f"Mobster can't read {shown(path, home)}. Allow Mobster in System Settings › Privacy & "
|
|
129
|
+
"Security › Files and Folders.")
|
|
130
|
+
except OSError:
|
|
131
|
+
return "unreadable", f"Mobster can't read {shown(path, home)}. Check the folder is still there."
|
|
132
|
+
return "ready", None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _bundle(xcodeproj):
|
|
136
|
+
"""The app's bundle id from the project file: the shortest PRODUCT_BUNDLE_IDENTIFIER that names no build
|
|
137
|
+
setting and no test target. None when there's none."""
|
|
138
|
+
try:
|
|
139
|
+
with open(Path(xcodeproj) / "project.pbxproj", encoding="utf-8", errors="replace") as handle:
|
|
140
|
+
text = handle.read(PBXPROJ_LIMIT)
|
|
141
|
+
except OSError:
|
|
142
|
+
return None
|
|
143
|
+
found = {value for value in BUNDLE.findall(text) if "$" not in value and "test" not in value.lower()}
|
|
144
|
+
return min(found, key=lambda value: (len(value), value)) if found else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _no_node(argv, cwd, *args, **kwargs):
|
|
148
|
+
return 1, "" # reading a folder never starts Node: devkit/expo.py falls back to app.json
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def apps(root):
|
|
152
|
+
"""The apps ``root`` builds, at most MAX_APPS: an Expo or React Native app at its root, then each Xcode workspace
|
|
153
|
+
(else project) at the root or one folder down. Each is {kind, name, path (relative), bundle}."""
|
|
154
|
+
from ..devkit import expo
|
|
155
|
+
root = Path(root)
|
|
156
|
+
out = []
|
|
157
|
+
native = None
|
|
158
|
+
try:
|
|
159
|
+
found = expo.detect(root, runner=_no_node)
|
|
160
|
+
except (OSError, ValueError):
|
|
161
|
+
found = None
|
|
162
|
+
if found is not None:
|
|
163
|
+
out.append({"kind": "expo" if found.expo else "react-native", "name": found.name, "path": ".",
|
|
164
|
+
"bundle": found.bundle})
|
|
165
|
+
native = root / "ios"
|
|
166
|
+
try:
|
|
167
|
+
children = sorted(child for child in root.iterdir() if child.is_dir() and not child.name.startswith(".")
|
|
168
|
+
and child.name not in NOT_SOURCES and child.suffix not in (".xcodeproj", ".xcworkspace"))
|
|
169
|
+
except OSError:
|
|
170
|
+
children = []
|
|
171
|
+
for folder in (root, *children):
|
|
172
|
+
if len(out) >= MAX_APPS:
|
|
173
|
+
break
|
|
174
|
+
if folder == native:
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
workspaces = sorted(p for p in folder.glob("*.xcworkspace") if p.is_dir())
|
|
178
|
+
projects = sorted(p for p in folder.glob("*.xcodeproj") if p.is_dir())
|
|
179
|
+
except OSError:
|
|
180
|
+
continue
|
|
181
|
+
for path in (workspaces or projects)[:MAX_APPS - len(out)]:
|
|
182
|
+
twin = path.with_suffix(".xcodeproj")
|
|
183
|
+
source = path if path.suffix == ".xcodeproj" else twin if twin.is_dir() else \
|
|
184
|
+
(projects[0] if len(projects) == 1 else None)
|
|
185
|
+
out.append({"kind": "xcode", "name": path.stem, "path": path.relative_to(root).as_posix(),
|
|
186
|
+
"bundle": _bundle(source) if source else None})
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _inside(root, path):
|
|
191
|
+
"""``path`` resolved, when it is a file inside ``root`` (a link can't lead out of the project), else None."""
|
|
192
|
+
try:
|
|
193
|
+
resolved = Path(path).resolve(strict=True)
|
|
194
|
+
except (OSError, RuntimeError):
|
|
195
|
+
return None
|
|
196
|
+
return resolved if resolved.is_file() and Path(root).resolve() in resolved.parents else None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _json(path):
|
|
200
|
+
try:
|
|
201
|
+
with open(path, encoding="utf-8") as handle:
|
|
202
|
+
value = json.loads(handle.read(JSON_LIMIT))
|
|
203
|
+
except (OSError, ValueError):
|
|
204
|
+
return None
|
|
205
|
+
return value if isinstance(value, dict) else None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _icon_sets(root):
|
|
209
|
+
"""The Xcode asset catalogs' app icon sets under ``root`` (AppIcon first), searched at most ICON_DEPTH folders down
|
|
210
|
+
and through at most ICON_FOLDERS folders, never in hidden folders or NOT_SOURCES."""
|
|
211
|
+
found, seen, queue = [], 0, [(Path(root), 0)]
|
|
212
|
+
while queue and seen < ICON_FOLDERS:
|
|
213
|
+
folder, depth = queue.pop(0)
|
|
214
|
+
seen += 1
|
|
215
|
+
try:
|
|
216
|
+
children = sorted(child for child in folder.iterdir() if child.is_dir() and not child.is_symlink()
|
|
217
|
+
and not child.name.startswith(".") and child.name not in NOT_SOURCES)
|
|
218
|
+
except OSError:
|
|
219
|
+
continue
|
|
220
|
+
for child in children:
|
|
221
|
+
if child.suffix == ".appiconset" and folder.suffix == ".xcassets":
|
|
222
|
+
found.append(child)
|
|
223
|
+
elif depth < ICON_DEPTH and child.suffix not in (".appiconset", ".xcodeproj", ".xcworkspace"):
|
|
224
|
+
queue.append((child, depth + 1))
|
|
225
|
+
return sorted(found, key=lambda path: (path.stem != "AppIcon", "test" in str(path).lower(), str(path)))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
_icons = {} # project folder -> (when it was looked for, the icon found or None)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def icon(root, clock=time.monotonic):
|
|
232
|
+
"""The app's own icon in the project, a PNG file inside it: Expo's (app.json's ios.icon, else icon), else the
|
|
233
|
+
largest iPhone image of an Xcode asset catalog's app icon set, AppIcon first. None when there is none. What was
|
|
234
|
+
found is kept for a minute, so a list of projects doesn't search every folder each time it is read."""
|
|
235
|
+
now = clock()
|
|
236
|
+
kept = _icons.get(str(root))
|
|
237
|
+
if kept is not None and now - kept[0] < 60 and (kept[1] is None or _inside(root, kept[1]) is not None):
|
|
238
|
+
return kept[1]
|
|
239
|
+
found = _find_icon(Path(root))
|
|
240
|
+
if len(_icons) > 200:
|
|
241
|
+
_icons.clear()
|
|
242
|
+
_icons[str(root)] = (now, found)
|
|
243
|
+
return found
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _find_icon(root):
|
|
247
|
+
config = _json(root / "app.json")
|
|
248
|
+
expo = config.get("expo") if isinstance(config, dict) and isinstance(config.get("expo"), dict) else config
|
|
249
|
+
if isinstance(expo, dict):
|
|
250
|
+
ios = expo.get("ios") if isinstance(expo.get("ios"), dict) else {}
|
|
251
|
+
for value in (ios.get("icon"), expo.get("icon")):
|
|
252
|
+
path = _inside(root, root / value) if isinstance(value, str) and value.lower().endswith(".png") else None
|
|
253
|
+
if path is not None:
|
|
254
|
+
return path
|
|
255
|
+
for folder in _icon_sets(root):
|
|
256
|
+
contents = _json(folder / "Contents.json") or {}
|
|
257
|
+
best = None
|
|
258
|
+
for image in contents.get("images") or ():
|
|
259
|
+
if not isinstance(image, dict) or image.get("appearances") or image.get("idiom") in NOT_IPHONE:
|
|
260
|
+
continue
|
|
261
|
+
name, size, scale = image.get("filename"), str(image.get("size") or ""), str(image.get("scale") or "1x")
|
|
262
|
+
match = re.fullmatch(r"(\d+(?:\.\d+)?)x\1", size)
|
|
263
|
+
path = _inside(root, folder / name) if isinstance(name, str) and name.lower().endswith(".png") else None
|
|
264
|
+
if match and path is not None:
|
|
265
|
+
pixels = float(match.group(1)) * float(scale.rstrip("x") or 1)
|
|
266
|
+
if best is None or pixels > best[0]:
|
|
267
|
+
best = (pixels, path)
|
|
268
|
+
if best is not None:
|
|
269
|
+
return best[1]
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def icon_version(path):
|
|
274
|
+
"""A word that changes when the icon file does (its size and change time), for a cache-busting address."""
|
|
275
|
+
try:
|
|
276
|
+
status = Path(path).stat()
|
|
277
|
+
except OSError:
|
|
278
|
+
return None
|
|
279
|
+
return f"{status.st_mtime_ns:x}{status.st_size:x}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _count(folder, suffixes):
|
|
283
|
+
try:
|
|
284
|
+
return sum(1 for path in Path(folder).iterdir() if path.is_file() and path.suffix.lower() in suffixes
|
|
285
|
+
and not path.name.startswith("."))
|
|
286
|
+
except OSError:
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def checks(root, limit=None):
|
|
291
|
+
"""The check files in ``root``/.mobster/checks, relative to the project, sorted (testkit/discover's walk)."""
|
|
292
|
+
from ..testkit.discover import check_files
|
|
293
|
+
folder = Path(root) / ".mobster" / "checks"
|
|
294
|
+
if not folder.is_dir():
|
|
295
|
+
return []
|
|
296
|
+
try:
|
|
297
|
+
files = check_files(folder)
|
|
298
|
+
except OSError:
|
|
299
|
+
return []
|
|
300
|
+
return [path.relative_to(root).as_posix() for path in files[:limit]]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def check_tags(root):
|
|
304
|
+
"""The tags the project's checks carry, sorted; [] when they can't be read."""
|
|
305
|
+
from ..testkit.discover import DiscoveryError, discover
|
|
306
|
+
try:
|
|
307
|
+
entries = discover([], root)
|
|
308
|
+
except (DiscoveryError, OSError):
|
|
309
|
+
return []
|
|
310
|
+
return sorted({tag for entry in entries for tag in entry.tags})
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def simulators(root):
|
|
314
|
+
"""(the simulators .mobster/matrix.yaml lists, as "TYPE@RUNTIME", or [], and the file's problem or None)."""
|
|
315
|
+
from ..testkit.targets import TargetError, load_matrix
|
|
316
|
+
path = Path(root) / ".mobster" / "matrix.yaml"
|
|
317
|
+
if not path.is_file():
|
|
318
|
+
return [], None
|
|
319
|
+
try:
|
|
320
|
+
return [f"{t.device_type}@{t.runtime}" if t.runtime else t.device_type for t in load_matrix(path)], None
|
|
321
|
+
except TargetError as error:
|
|
322
|
+
return [], str(error).replace(str(path), ".mobster/matrix.yaml")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
_ready = {"at": None, "value": False}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def git_ready(clock=time.monotonic):
|
|
329
|
+
"""Whether git can run without macOS offering to install the developer tools (the /usr/bin/git stub opens that
|
|
330
|
+
window when they're missing): a git outside /usr/bin, or a developer folder xcode-select names. Asked again after
|
|
331
|
+
a minute, so installing the tools needs no restart."""
|
|
332
|
+
now = clock()
|
|
333
|
+
if _ready["at"] is not None and now - _ready["at"] < 60:
|
|
334
|
+
return _ready["value"]
|
|
335
|
+
found = shutil.which("git")
|
|
336
|
+
value = bool(found) and Path(found).resolve().parent != Path("/usr/bin")
|
|
337
|
+
if found and not value:
|
|
338
|
+
try:
|
|
339
|
+
result = subprocess.run(["/usr/bin/xcode-select", "-p"], capture_output=True, timeout=GIT_TIMEOUT,
|
|
340
|
+
stdin=subprocess.DEVNULL)
|
|
341
|
+
folder = result.stdout.decode("utf-8", errors="replace").strip() if result.returncode == 0 else ""
|
|
342
|
+
value = bool(folder) and Path(folder, "usr", "bin", "git").is_file()
|
|
343
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
344
|
+
value = False
|
|
345
|
+
_ready.update(at=now, value=value)
|
|
346
|
+
return value
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _git(argv, cwd):
|
|
350
|
+
"""(exit code, stdout) of a read-only git call in ``cwd``; never raises, never prompts, never locks."""
|
|
351
|
+
if not git_ready():
|
|
352
|
+
return 1, ""
|
|
353
|
+
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_OPTIONAL_LOCKS": "0"}
|
|
354
|
+
try:
|
|
355
|
+
result = subprocess.run(["git", "-c", "core.fsmonitor=false", *argv], cwd=str(cwd), capture_output=True,
|
|
356
|
+
timeout=GIT_TIMEOUT, stdin=subprocess.DEVNULL, env=env)
|
|
357
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
358
|
+
return 1, ""
|
|
359
|
+
return result.returncode, result.stdout.decode("utf-8", errors="replace")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def repo_of(url):
|
|
363
|
+
""""owner/name" for a GitHub remote URL, else None."""
|
|
364
|
+
match = GITHUB.match(url or "")
|
|
365
|
+
return f"{match.group(1)}/{match.group(2)}" if match else None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def clean_url(url):
|
|
369
|
+
"""A remote URL with any user name and password taken out (https://token@github.com/... is never shown)."""
|
|
370
|
+
return re.sub(r"^(https?://)[^@/]+@", r"\1", url or "") or None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def git(root):
|
|
374
|
+
"""{branch, remote, repo} for a git working tree at ``root``, else None. ``branch`` is None when detached."""
|
|
375
|
+
if not (Path(root) / ".git").exists():
|
|
376
|
+
return None
|
|
377
|
+
code, top = _git(["rev-parse", "--show-toplevel"], root)
|
|
378
|
+
if code != 0:
|
|
379
|
+
return None
|
|
380
|
+
code, branch = _git(["symbolic-ref", "--short", "-q", "HEAD"], root)
|
|
381
|
+
_, remote = _git(["config", "--get", "remote.origin.url"], root)
|
|
382
|
+
remote = clean_url(remote.strip())
|
|
383
|
+
return {"branch": (branch.strip() or None) if code == 0 else None, "remote": remote, "repo": repo_of(remote)}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def git_status(root):
|
|
387
|
+
"""{head, upstream, ahead, behind, changed} from `git status --porcelain=v2 --branch` (tracked files only), or
|
|
388
|
+
{} when it can't be read in time."""
|
|
389
|
+
code, out = _git(["status", "--porcelain=v2", "--branch", "--untracked-files=no"], root)
|
|
390
|
+
if code != 0:
|
|
391
|
+
return {}
|
|
392
|
+
status = {"head": None, "upstream": None, "ahead": 0, "behind": 0, "changed": 0}
|
|
393
|
+
for line in out.splitlines():
|
|
394
|
+
if line.startswith("# branch.oid "):
|
|
395
|
+
oid = line[13:].strip()
|
|
396
|
+
status["head"] = oid[:12] if re.fullmatch(r"[0-9a-f]{40,64}", oid) else None
|
|
397
|
+
elif line.startswith("# branch.upstream "):
|
|
398
|
+
status["upstream"] = line[18:].strip() or None
|
|
399
|
+
elif line.startswith("# branch.ab "):
|
|
400
|
+
match = re.fullmatch(r"\+(\d+) -(\d+)", line[12:].strip())
|
|
401
|
+
if match:
|
|
402
|
+
status["ahead"], status["behind"] = int(match.group(1)), int(match.group(2))
|
|
403
|
+
elif line and not line.startswith("#"):
|
|
404
|
+
status["changed"] += 1
|
|
405
|
+
return status
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def summary(path, home=None):
|
|
409
|
+
"""What the Projects list shows for the folder at ``path``."""
|
|
410
|
+
path = Path(path)
|
|
411
|
+
current, problem = state(path, home)
|
|
412
|
+
out = {"state": current, "problem": problem, "mobster": False, "apps": [], "icon": None, "checks": 0,
|
|
413
|
+
"workflows": 0, "simulators": [], "matrixProblem": None, "git": None}
|
|
414
|
+
if current != "ready":
|
|
415
|
+
return out
|
|
416
|
+
out["mobster"] = (path / ".mobster").is_dir()
|
|
417
|
+
out["apps"] = apps(path)
|
|
418
|
+
found = icon(path)
|
|
419
|
+
out["icon"] = icon_version(found) if found else None
|
|
420
|
+
if out["mobster"]:
|
|
421
|
+
out["checks"] = len(checks(path))
|
|
422
|
+
out["workflows"] = _count(path / ".mobster" / "workflows", WORKFLOW_SUFFIXES)
|
|
423
|
+
out["simulators"], out["matrixProblem"] = simulators(path)
|
|
424
|
+
out["git"] = git(path)
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def detail(path, home=None):
|
|
429
|
+
"""``summary`` with the checks by file and their tags and, in a git working tree, the commit, upstream and changed
|
|
430
|
+
files."""
|
|
431
|
+
out = summary(path, home)
|
|
432
|
+
out["checkFiles"] = checks(path, MAX_LISTED_CHECKS) if out["mobster"] else []
|
|
433
|
+
out["checkTags"] = check_tags(path) if out["checkFiles"] else []
|
|
434
|
+
if out["git"] is not None:
|
|
435
|
+
out["git"].update(git_status(path))
|
|
436
|
+
return out
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Import from GitHub: the repositories your GitHub account can open, through the GitHub connector's sign-in, and that
|
|
2
|
+
sign-in for cloning one of them.
|
|
3
|
+
|
|
4
|
+
``repositories(token)`` lists them, the one pushed to last first: owner/name, whether it's private, its language and
|
|
5
|
+
default branch and when it was last pushed. ``clone_env(token)`` is what git needs to clone a private repository from
|
|
6
|
+
github.com with that sign-in: an ``http.https://github.com/.extraheader`` set through git's environment, so the token
|
|
7
|
+
is never in a command line, a URL, a log or the clone's own .git/config, and is sent to github.com only.
|
|
8
|
+
|
|
9
|
+
Mobster never asks for more than the connector's sign-in already allows, and never keeps the list.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
|
|
18
|
+
from ..api_errors import APIError
|
|
19
|
+
|
|
20
|
+
API = "https://api.github.com"
|
|
21
|
+
PRESET = "github"
|
|
22
|
+
PAGES = 3
|
|
23
|
+
PER_PAGE = 100
|
|
24
|
+
TIMEOUT = 15
|
|
25
|
+
SIGN_IN_AGAIN = "GitHub didn't accept Mobster's sign-in. Sign in to GitHub again in Connectors, then try again."
|
|
26
|
+
OFFLINE = "Mobster couldn't reach GitHub. Check your connection, then try again."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def token_of(runtime):
|
|
30
|
+
"""The GitHub connector's sign-in, or None when there is none (or the Keychain won't give it)."""
|
|
31
|
+
from ..connectors import service as connectors
|
|
32
|
+
service = connectors.service_for(runtime)
|
|
33
|
+
if service is None:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
return service.preset_bearer(PRESET)
|
|
37
|
+
except APIError:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def repositories(token, *, opener=urllib.request.urlopen):
|
|
42
|
+
"""Every repository the account can open (owner, collaborator, organization member), newest push first, at most
|
|
43
|
+
PAGES * PER_PAGE of them. APIError with what to do when GitHub refuses or can't be reached."""
|
|
44
|
+
out = []
|
|
45
|
+
for page in range(1, PAGES + 1):
|
|
46
|
+
request = urllib.request.Request(
|
|
47
|
+
f"{API}/user/repos?per_page={PER_PAGE}&page={page}&sort=pushed"
|
|
48
|
+
"&affiliation=owner,collaborator,organization_member",
|
|
49
|
+
headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
|
|
50
|
+
"X-GitHub-Api-Version": "2022-11-28", "User-Agent": "Mobster"})
|
|
51
|
+
try:
|
|
52
|
+
with opener(request, timeout=TIMEOUT) as response:
|
|
53
|
+
rows = json.loads(response.read(8_000_000).decode("utf-8"))
|
|
54
|
+
except urllib.error.HTTPError as error:
|
|
55
|
+
if error.code in (401, 403):
|
|
56
|
+
raise APIError(SIGN_IN_AGAIN, 401, "github_sign_in") from None
|
|
57
|
+
raise APIError(f"GitHub answered {error.code}. Try again in a minute.", 502, "github_error") from None
|
|
58
|
+
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
|
59
|
+
raise APIError(OFFLINE, 503, "offline") from None
|
|
60
|
+
if not isinstance(rows, list):
|
|
61
|
+
raise APIError("GitHub sent back something Mobster didn't expect. Try again.", 502, "github_error")
|
|
62
|
+
out += [repo for repo in (_repo(row) for row in rows) if repo is not None]
|
|
63
|
+
if len(rows) < PER_PAGE:
|
|
64
|
+
break
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pulls(token, repo, *, opener=urllib.request.urlopen):
|
|
69
|
+
"""The repository's pull requests from its own branches, the one updated last first (one page of PER_PAGE), as
|
|
70
|
+
{branch: {number, title, url, state (open, draft, merged or closed)}}: the newest for each branch. A pull request
|
|
71
|
+
from a fork names a branch of another repository, so it is left out. APIError as ``repositories``."""
|
|
72
|
+
request = urllib.request.Request(
|
|
73
|
+
f"{API}/repos/{repo}/pulls?state=all&sort=updated&direction=desc&per_page={PER_PAGE}",
|
|
74
|
+
headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json",
|
|
75
|
+
"X-GitHub-Api-Version": "2022-11-28", "User-Agent": "Mobster"})
|
|
76
|
+
try:
|
|
77
|
+
with opener(request, timeout=TIMEOUT) as response:
|
|
78
|
+
rows = json.loads(response.read(8_000_000).decode("utf-8"))
|
|
79
|
+
except urllib.error.HTTPError as error:
|
|
80
|
+
if error.code in (401, 403):
|
|
81
|
+
raise APIError(SIGN_IN_AGAIN, 401, "github_sign_in") from None
|
|
82
|
+
if error.code == 404: # a repository the sign-in can't see
|
|
83
|
+
return {}
|
|
84
|
+
raise APIError(f"GitHub answered {error.code}. Try again in a minute.", 502, "github_error") from None
|
|
85
|
+
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
|
86
|
+
raise APIError(OFFLINE, 503, "offline") from None
|
|
87
|
+
out = {}
|
|
88
|
+
for row in rows if isinstance(rows, list) else ():
|
|
89
|
+
pull = _pull(row, repo)
|
|
90
|
+
if pull is not None:
|
|
91
|
+
out.setdefault(pull.pop("branch"), pull)
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _pull(row, repo):
|
|
96
|
+
"""A pull request as runs by branch show it, checked at the boundary; None for one that isn't, or a fork's."""
|
|
97
|
+
head = row.get("head") if isinstance(row, dict) else None
|
|
98
|
+
head_repo = head.get("repo") if isinstance(head, dict) else None
|
|
99
|
+
if not (isinstance(head, dict) and isinstance(head.get("ref"), str) and isinstance(row.get("number"), int)
|
|
100
|
+
and isinstance(head_repo, dict) and str(head_repo.get("full_name", "")).lower() == repo.lower()):
|
|
101
|
+
return None
|
|
102
|
+
state = "merged" if row.get("merged_at") else "draft" if row.get("draft") is True and row.get("state") == "open" \
|
|
103
|
+
else "open" if row.get("state") == "open" else "closed"
|
|
104
|
+
title = row["title"][:200] if isinstance(row.get("title"), str) else ""
|
|
105
|
+
return {"branch": head["ref"], "number": row["number"], "title": title,
|
|
106
|
+
"url": f"https://github.com/{repo}/pull/{row['number']}", "state": state}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _repo(row):
|
|
110
|
+
"""A repository as the Add page shows it, checked at the boundary; None for one that isn't."""
|
|
111
|
+
if not isinstance(row, dict) or not isinstance(row.get("full_name"), str) or "/" not in row["full_name"]:
|
|
112
|
+
return None
|
|
113
|
+
owner, name = row["full_name"].split("/", 1)
|
|
114
|
+
text = lambda key, high=200: row[key][:high] if isinstance(row.get(key), str) else None # noqa: E731
|
|
115
|
+
return {"repo": row["full_name"], "owner": owner, "name": name, "private": row.get("private") is True,
|
|
116
|
+
"description": text("description", 300), "language": text("language", 40),
|
|
117
|
+
"defaultBranch": text("default_branch", 200), "pushedAt": text("pushed_at", 40),
|
|
118
|
+
"archived": row.get("archived") is True, "url": f"https://github.com/{row['full_name']}.git"}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clone_env(token, base=None):
|
|
122
|
+
"""git's environment for a clone from github.com with the connector's sign-in: http.extraheader for that host
|
|
123
|
+
only, added after any GIT_CONFIG_* entries ``base`` (os.environ) already has, so none of them is lost."""
|
|
124
|
+
base = os.environ if base is None else base
|
|
125
|
+
try:
|
|
126
|
+
count = max(0, int(base.get("GIT_CONFIG_COUNT", "0")))
|
|
127
|
+
except ValueError:
|
|
128
|
+
count = 0
|
|
129
|
+
basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
|
|
130
|
+
return {"GIT_CONFIG_COUNT": str(count + 1), f"GIT_CONFIG_KEY_{count}": "http.https://github.com/.extraheader",
|
|
131
|
+
f"GIT_CONFIG_VALUE_{count}": f"AUTHORIZATION: basic {basic}"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Track ``projects``' registrations (tracks.load calls ``register(api)`` once per process, staged).
|
|
2
|
+
|
|
3
|
+
- Journal schema ``projects`` v2 (store.py): projects, their check runs and approved CI revisions.
|
|
4
|
+
- The run field ``project`` (public): the project a task belongs to, saved as {id, path, name}, so its memory can be
|
|
5
|
+
the project's (the memory track reads id and path).
|
|
6
|
+
- One service (a ProjectService per Runtime) and the routes under /api/projects. Adding or opening a project tells
|
|
7
|
+
memory its folder (POST /api/memory/projects), when the memory track is here.
|
|
8
|
+
|
|
9
|
+
No CLI command and no MCP provider of its own yet: `mobster test` and the MCP tools already find a project by its
|
|
10
|
+
`.mobster/` folder.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from . import routes, service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_project(value, runtime):
|
|
17
|
+
"""The run field ``project``: a project's id or {id}, saved as {id, path, name} (service.run_field)."""
|
|
18
|
+
found = service.service_for(runtime)
|
|
19
|
+
if found is None:
|
|
20
|
+
raise ValueError(service.GONE)
|
|
21
|
+
return found.run_field(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(api):
|
|
25
|
+
"""``api`` is mobile_agent.harness_api."""
|
|
26
|
+
from .. import journal
|
|
27
|
+
from . import store
|
|
28
|
+
journal.register_schema(store.SCHEMA, store.VERSION, store.migrate)
|
|
29
|
+
api.register_run_field("project", validate_project, public=True)
|
|
30
|
+
api.register_service(service.start, service.close)
|
|
31
|
+
routes.register_routes()
|