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,289 @@
|
|
|
1
|
+
"""Files to and from an iPhone app's Documents folder, the one Files shows under On My iPhone › <App>.
|
|
2
|
+
|
|
3
|
+
Only apps that share files (``UIFileSharingEnabled``: Pages, Numbers, Keynote, VLC, Documents…) have one; ``apps``
|
|
4
|
+
lists them from the phone's installed apps (ideviceinstaller). ``put``, ``get`` and ``ls`` go through Apple's file
|
|
5
|
+
service for those folders (AFC house_arrest, "VendDocuments") with ``afcclient``: the copy bundled in the Mac app,
|
|
6
|
+
else Homebrew's. Nothing else on the phone is reachable: not other apps' data, not the camera roll.
|
|
7
|
+
|
|
8
|
+
Rules every caller gets:
|
|
9
|
+
- **USB only.** A phone on Wi-Fi answers ``needs_cable`` until the wireless probe shows the file service runs
|
|
10
|
+
through the encrypted tunnel. afcclient is never given ``-n``/``--network``.
|
|
11
|
+
- **Names**: sniff.clean_name (no ``/``, ``..`` or control characters), so nothing lands outside the app's folder;
|
|
12
|
+
remote paths are always ``/<name>`` at the folder's top.
|
|
13
|
+
- **100 MB** a file. A put never overwrites: a name that is taken becomes "menu 2.pdf" (the CLI's ``--replace``
|
|
14
|
+
overwrites on purpose). Each put is checked by listing the folder after.
|
|
15
|
+
- Under ``MOBSTER_NO_DEVICES=1`` (local CI) the real tools never run: RuntimeError.
|
|
16
|
+
|
|
17
|
+
Who approves: the agent's PUT_FILE asks first (its title), the Mac app's "Put on iPhone…" is the user's own click
|
|
18
|
+
(and needs the app session), ``mobster phone file put`` is the user's own command, and MCP's put_file needs
|
|
19
|
+
``mobster mcp --allow-files``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import plistlib
|
|
24
|
+
import re
|
|
25
|
+
import stat
|
|
26
|
+
import subprocess
|
|
27
|
+
import tempfile
|
|
28
|
+
import time
|
|
29
|
+
from contextlib import contextmanager
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from . import PhoneIOError
|
|
33
|
+
|
|
34
|
+
MAX_BYTES = 100 * 1024 * 1024
|
|
35
|
+
TIMEOUT = 120
|
|
36
|
+
LIST_TIMEOUT = 30
|
|
37
|
+
BUNDLE = re.compile(r"[A-Za-z0-9][A-Za-z0-9.-]{0,154}")
|
|
38
|
+
ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
39
|
+
LINE = re.compile(r"^([-dlfbcs])[rwx-]{9}\s+\d+\s+mobile\s+mobile\s+(\d+)\s+(\d{2} \S{3} \d{4} \d{2}:\d{2}:\d{2}) "
|
|
40
|
+
r"(.+)$")
|
|
41
|
+
NO_SHARING = re.compile(r"UIFileSharingEnabled|InstallationLookupFailed|not present on the device", re.I)
|
|
42
|
+
NEEDS_TOOLS = ("Install the iPhone tools: brew install libimobiledevice", "tools")
|
|
43
|
+
NEEDS_CABLE = "Files move only over the cable for now. Plug the iPhone into this Mac, then try again."
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def installer_tool():
|
|
47
|
+
from ..mac_tools import tool
|
|
48
|
+
return tool("ideviceinstaller")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run(argv, timeout):
|
|
52
|
+
"""(exit code, standard output and error) of ``argv``, no shell, no colours, the C locale."""
|
|
53
|
+
from ..native_helpers import require_devices
|
|
54
|
+
require_devices()
|
|
55
|
+
env = dict(os.environ, NO_COLOR="1", LC_ALL="C", LANG="C")
|
|
56
|
+
try:
|
|
57
|
+
done = subprocess.run(argv, capture_output=True, text=True, errors="replace", timeout=timeout,
|
|
58
|
+
stdin=subprocess.DEVNULL, env=env)
|
|
59
|
+
except subprocess.TimeoutExpired:
|
|
60
|
+
raise PhoneIOError(f"The iPhone didn't answer within {timeout} seconds.", "failed",
|
|
61
|
+
"Keep it unlocked and plugged in, then try again.") from None
|
|
62
|
+
except OSError as error:
|
|
63
|
+
raise PhoneIOError(f"The iPhone tools couldn't run ({type(error).__name__}).", "tools") from None
|
|
64
|
+
return done.returncode, (done.stdout or "") + (done.stderr or "")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PhoneFiles:
|
|
68
|
+
"""The file service for one device ``record`` (kind, udid, name, transport)."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, record, runner=None, afc=None, installer=None, container=False):
|
|
71
|
+
self.record = record or {}
|
|
72
|
+
# container: the app's whole sandbox (house_arrest's VendContainer), its Documents folder inside; only a
|
|
73
|
+
# development build of your own (signed with get-task-allow) opens it. Otherwise the folder Files shows.
|
|
74
|
+
self.container = bool(container)
|
|
75
|
+
self.base = "/Documents/" if self.container else "/"
|
|
76
|
+
self._runner = runner
|
|
77
|
+
self._afc_path, self._installer = afc, installer
|
|
78
|
+
if self.record.get("kind") != "usb":
|
|
79
|
+
what = "a simulator" if self.record.get("kind") == "simulator" else "not an iPhone on a cable"
|
|
80
|
+
raise PhoneIOError(f"“{self.name}” is {what}. Mobster moves files to iPhones plugged into this Mac.",
|
|
81
|
+
"usage")
|
|
82
|
+
if self.record.get("transport") == "wifi":
|
|
83
|
+
raise PhoneIOError(f"“{self.name}” is on Wi-Fi. " + NEEDS_CABLE, "needs_cable")
|
|
84
|
+
udid = str(self.record.get("udid") or "")
|
|
85
|
+
if not re.fullmatch(r"[0-9A-Fa-f-]{24,40}", udid):
|
|
86
|
+
raise PhoneIOError(f"Mobster doesn't know which iPhone “{self.name}” is yet.", "no_device",
|
|
87
|
+
"Set it up in the Mobster app.")
|
|
88
|
+
self.udid = udid
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def name(self):
|
|
92
|
+
return self.record.get("name") or self.record.get("id") or "the iPhone"
|
|
93
|
+
|
|
94
|
+
def run(self, argv, timeout):
|
|
95
|
+
if self._runner is not None:
|
|
96
|
+
return self._runner(argv, timeout)
|
|
97
|
+
return _run(argv, timeout)
|
|
98
|
+
|
|
99
|
+
def _afcclient(self):
|
|
100
|
+
from ..mac_tools import tool
|
|
101
|
+
found = self._afc_path or tool("afcclient")
|
|
102
|
+
if not found:
|
|
103
|
+
raise PhoneIOError("Mobster needs the iPhone tools to move files.", NEEDS_TOOLS[1], NEEDS_TOOLS[0])
|
|
104
|
+
return found
|
|
105
|
+
|
|
106
|
+
def apps(self):
|
|
107
|
+
"""[{"bundleId", "name"}] of the apps whose Documents folder shows in Files, sorted by name."""
|
|
108
|
+
tool = self._installer or installer_tool()
|
|
109
|
+
if not tool:
|
|
110
|
+
raise PhoneIOError("Mobster needs the iPhone tools to list the apps.", NEEDS_TOOLS[1], NEEDS_TOOLS[0])
|
|
111
|
+
argv = [tool, "-u", self.udid, "list", "--all", "--xml", "-a", "CFBundleIdentifier", "-a",
|
|
112
|
+
"CFBundleDisplayName", "-a", "CFBundleName", "-a", "UIFileSharingEnabled"]
|
|
113
|
+
code, output = self.run(argv, LIST_TIMEOUT)
|
|
114
|
+
if code != 0:
|
|
115
|
+
raise _explain(output, self.name)
|
|
116
|
+
start = output.find("<?xml")
|
|
117
|
+
try:
|
|
118
|
+
items = plistlib.loads(output[start if start >= 0 else 0:].encode())
|
|
119
|
+
except Exception: # noqa: BLE001
|
|
120
|
+
raise PhoneIOError("The iPhone's app list couldn't be read.", "failed") from None
|
|
121
|
+
apps = []
|
|
122
|
+
for item in items if isinstance(items, list) else ():
|
|
123
|
+
if not isinstance(item, dict) or item.get("UIFileSharingEnabled") is not True:
|
|
124
|
+
continue
|
|
125
|
+
bundle = item.get("CFBundleIdentifier")
|
|
126
|
+
if not isinstance(bundle, str) or not BUNDLE.fullmatch(bundle):
|
|
127
|
+
continue
|
|
128
|
+
name = item.get("CFBundleDisplayName") or item.get("CFBundleName") or bundle
|
|
129
|
+
apps.append({"bundleId": bundle, "name": " ".join(str(name).split())[:60]})
|
|
130
|
+
return sorted(apps, key=lambda app: (app["name"].casefold(), app["bundleId"]))
|
|
131
|
+
|
|
132
|
+
def _afc(self, app, *command, timeout=TIMEOUT):
|
|
133
|
+
if not isinstance(app, str) or not BUNDLE.fullmatch(app):
|
|
134
|
+
raise PhoneIOError("Name the app by its bundle ID, such as com.apple.Pages.", "usage")
|
|
135
|
+
# "--" ends afcclient's own options: macOS's getopt_long reorders arguments, so without it the command's
|
|
136
|
+
# flags ("ls -l", "put -f") would be read as afcclient's and refused as unknown options.
|
|
137
|
+
argv = [self._afcclient(), "-u", self.udid, "--container" if self.container else "--documents", app, "--",
|
|
138
|
+
*map(str, command)]
|
|
139
|
+
code, output = self.run(argv, timeout)
|
|
140
|
+
output = ANSI.sub("", output)
|
|
141
|
+
if NO_SHARING.search(output) or (self.container and re.search(r"InstallationLookupFailed|not allowed|"
|
|
142
|
+
r"Permission", output, re.I)):
|
|
143
|
+
if self.container:
|
|
144
|
+
raise PhoneIOError(f"{app}'s own folder opens only for a development build of yours on {self.name}.",
|
|
145
|
+
"no_file_sharing", "Install a debug build from Xcode, or use its Documents folder.")
|
|
146
|
+
raise PhoneIOError(f"{app} doesn't keep files you can see in Files, or it isn't on {self.name}.",
|
|
147
|
+
"no_file_sharing", "Choose an app that does, such as Pages, Numbers or Keynote.")
|
|
148
|
+
if code != 0:
|
|
149
|
+
raise _explain(output, self.name)
|
|
150
|
+
errors = [line for line in output.splitlines() if line.startswith(("Error:", "ERROR:"))]
|
|
151
|
+
return output, errors
|
|
152
|
+
|
|
153
|
+
def ls(self, app):
|
|
154
|
+
"""[{"name", "bytes", "modifiedAt" (ms), "folder"}] at the top of ``app``'s folder."""
|
|
155
|
+
output, errors = self._afc(app, "ls", "-l", self.base, timeout=LIST_TIMEOUT)
|
|
156
|
+
if errors:
|
|
157
|
+
raise _explain("\n".join(errors), self.name)
|
|
158
|
+
entries = []
|
|
159
|
+
for line in output.splitlines():
|
|
160
|
+
found = LINE.match(line.rstrip("\n"))
|
|
161
|
+
if not found:
|
|
162
|
+
continue
|
|
163
|
+
kind, size, stamp, name = found.groups()
|
|
164
|
+
try:
|
|
165
|
+
modified = round(time.mktime(time.strptime(stamp, "%d %b %Y %H:%M:%S")) * 1000)
|
|
166
|
+
except (ValueError, OverflowError):
|
|
167
|
+
modified = None
|
|
168
|
+
entries.append({"name": name, "bytes": int(size), "modifiedAt": modified, "folder": kind == "d"})
|
|
169
|
+
return sorted(entries, key=lambda e: e["name"].casefold())
|
|
170
|
+
|
|
171
|
+
def put(self, app, local, name=None, replace=False, *, before_copy=None):
|
|
172
|
+
"""Copy ``local`` into ``app``'s folder, never over a name already there; ``before_copy()`` may refuse."""
|
|
173
|
+
from ..attachments.sniff import clean_name
|
|
174
|
+
source = Path(local).expanduser()
|
|
175
|
+
wanted = clean_name(name or source.name)
|
|
176
|
+
with snapshot(source, MAX_BYTES) as staged:
|
|
177
|
+
size = staged.stat().st_size
|
|
178
|
+
existing = {entry["name"] for entry in self.ls(app)}
|
|
179
|
+
target = wanted if replace or wanted not in existing else _free_name(wanted, existing)
|
|
180
|
+
command = ["put", "-f"] if replace else ["put"]
|
|
181
|
+
if before_copy is not None:
|
|
182
|
+
before_copy()
|
|
183
|
+
_, errors = self._afc(app, *command, str(staged), self.base + target)
|
|
184
|
+
if errors:
|
|
185
|
+
raise _explain("\n".join(errors), self.name)
|
|
186
|
+
arrived = next((e for e in self.ls(app) if e["name"] == target and not e["folder"]), None)
|
|
187
|
+
if arrived is None or arrived["bytes"] != size:
|
|
188
|
+
raise PhoneIOError(f"{target} didn't arrive whole on {self.name}.", "failed",
|
|
189
|
+
"Keep the iPhone unlocked and plugged in, then try again.")
|
|
190
|
+
return {"name": target, "path": self.base + target, "bytes": size, "app": app}
|
|
191
|
+
|
|
192
|
+
def get(self, app, name, folder):
|
|
193
|
+
"""Copy ``name`` from ``app``'s folder into the Mac folder ``folder``. Returns the local Path."""
|
|
194
|
+
from ..attachments.sniff import clean_name
|
|
195
|
+
wanted = clean_name(name)
|
|
196
|
+
if wanted != name:
|
|
197
|
+
raise PhoneIOError(f"There is no file named {name!r} in {app}'s folder.", "usage")
|
|
198
|
+
entry = next((e for e in self.ls(app) if e["name"] == wanted), None)
|
|
199
|
+
if entry is None:
|
|
200
|
+
raise PhoneIOError(f"There is no file named {wanted} in {app}'s folder on {self.name}.", "not_found",
|
|
201
|
+
"List the folder with `mobster phone file ls --app " + app + "`.")
|
|
202
|
+
if entry["folder"]:
|
|
203
|
+
raise PhoneIOError(f"{wanted} is a folder; Mobster copies files.", "usage")
|
|
204
|
+
if entry["bytes"] > MAX_BYTES:
|
|
205
|
+
raise PhoneIOError(f"{wanted} is over 100 MB; Mobster moves files up to 100 MB.", "usage")
|
|
206
|
+
folder = Path(folder)
|
|
207
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
with tempfile.TemporaryDirectory(prefix="mobster-get-", dir=folder) as scratch:
|
|
209
|
+
partial = Path(scratch) / "file"
|
|
210
|
+
_, errors = self._afc(app, "get", self.base + wanted, str(partial))
|
|
211
|
+
if errors or not partial.is_file():
|
|
212
|
+
raise _explain("\n".join(errors) or "Error: nothing arrived", self.name)
|
|
213
|
+
if partial.stat().st_size != entry["bytes"]:
|
|
214
|
+
raise PhoneIOError(f"{wanted} didn't arrive whole.", "failed", "Try again.")
|
|
215
|
+
return land(partial, folder, wanted)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def land(partial, folder, name, *, exact=False):
|
|
219
|
+
"""Move the finished ``partial`` into ``folder`` as ``name``, or a free name next to it ("menu 2.pdf") when ``name``
|
|
220
|
+
is taken. Returns the local Path. The name is claimed with a hard link, which fails if the name exists, so two
|
|
221
|
+
runs landing into one folder never replace each other's file; ``partial`` must be on the same filesystem.
|
|
222
|
+
``exact`` raises FileExistsError instead of choosing another name."""
|
|
223
|
+
folder = Path(folder)
|
|
224
|
+
stem, ext = os.path.splitext(name)
|
|
225
|
+
for number in range(1, 10_000):
|
|
226
|
+
target = folder / (name if number == 1 else f"{stem} {number}{ext}")
|
|
227
|
+
try:
|
|
228
|
+
os.link(partial, target)
|
|
229
|
+
except FileExistsError:
|
|
230
|
+
if exact:
|
|
231
|
+
raise
|
|
232
|
+
continue
|
|
233
|
+
except OSError: # a volume without hard links (exFAT, some network shares)
|
|
234
|
+
if target.exists():
|
|
235
|
+
if exact:
|
|
236
|
+
raise FileExistsError(target) from None
|
|
237
|
+
continue
|
|
238
|
+
os.replace(partial, target)
|
|
239
|
+
return target
|
|
240
|
+
Path(partial).unlink()
|
|
241
|
+
return target
|
|
242
|
+
raise PhoneIOError("That folder has too many files with this name.", "failed")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@contextmanager
|
|
246
|
+
def snapshot(source, limit):
|
|
247
|
+
"""A bounded, private copy of one regular source file, removed after its consumer finishes."""
|
|
248
|
+
source = Path(source).expanduser()
|
|
249
|
+
try:
|
|
250
|
+
stream = open(source, "rb", opener=lambda path, flags: os.open(path, flags | os.O_NONBLOCK))
|
|
251
|
+
except OSError:
|
|
252
|
+
raise PhoneIOError(f"There is no file at {source}.", "usage") from None
|
|
253
|
+
with stream, tempfile.TemporaryDirectory(prefix="mobster-upload-") as folder:
|
|
254
|
+
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
|
|
255
|
+
raise PhoneIOError(f"{source.name} isn't a file.", "usage")
|
|
256
|
+
staged = Path(folder) / source.name
|
|
257
|
+
with open(staged, "xb", opener=lambda path, flags: os.open(path, flags, 0o600)) as out:
|
|
258
|
+
size = 0
|
|
259
|
+
while block := stream.read(min(1 << 20, limit + 1 - size)):
|
|
260
|
+
size += len(block)
|
|
261
|
+
if size > limit:
|
|
262
|
+
units = f"{limit / 1024 ** 3:g} GB" if limit >= 1024 ** 3 else f"{limit / 1024 ** 2:g} MB"
|
|
263
|
+
raise PhoneIOError(f"{source.name} is over {units}.", "usage")
|
|
264
|
+
out.write(block)
|
|
265
|
+
yield staged
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _free_name(name, taken):
|
|
269
|
+
"""``name`` made unique among ``taken``, as Finder does: "menu 2.pdf", "menu 3.pdf"…"""
|
|
270
|
+
stem, ext = os.path.splitext(name)
|
|
271
|
+
for number in range(2, 10_000):
|
|
272
|
+
candidate = f"{stem} {number}{ext}"
|
|
273
|
+
if candidate not in taken:
|
|
274
|
+
return candidate
|
|
275
|
+
raise PhoneIOError("That folder has too many files with this name.", "failed")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _explain(output, name):
|
|
279
|
+
"""A PhoneIOError for a failed tool run, from its output."""
|
|
280
|
+
text = " ".join((output or "").split())
|
|
281
|
+
if re.search(r"No device found|Device .* not found|not connected", text, re.I):
|
|
282
|
+
return PhoneIOError(f"{name} isn't plugged in.", "no_device", "Plug it into this Mac and unlock it.")
|
|
283
|
+
if re.search(r"lockdown|pair|trust|password protected|PasswordProtected", text, re.I):
|
|
284
|
+
return PhoneIOError(f"{name} is locked or doesn't trust this Mac.", "locked",
|
|
285
|
+
"Unlock it, tap Trust if it asks, then try again.")
|
|
286
|
+
if re.search(r"Object not found|No such file", text, re.I):
|
|
287
|
+
return PhoneIOError("That file isn't on the iPhone.", "not_found")
|
|
288
|
+
return PhoneIOError(f"Moving the file failed{': ' + text[:200] if text else ''}.", "failed",
|
|
289
|
+
"Keep the iPhone unlocked and plugged in, then try again.")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Files from this Mac to an iPhone with no cable: AirDrop to the one recipient named exactly, or iPhone Mirroring."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import PhoneIOError
|
|
9
|
+
|
|
10
|
+
AIRDROP_WAIT_S = 30
|
|
11
|
+
HELPER_SLACK_S = 30
|
|
12
|
+
|
|
13
|
+
PERMISSION = ("Mobster needs Accessibility to use AirDrop and iPhone Mirroring for you.",
|
|
14
|
+
"Turn it on in System Settings › Privacy & Security › Accessibility, then try again.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def helper(build=True):
|
|
18
|
+
"""The transfer helper's path: the Mac app's copy, else a build from source (Xcode's command line tools)."""
|
|
19
|
+
from .. import native_helpers
|
|
20
|
+
found = native_helpers.find("transfer")
|
|
21
|
+
if found is None and build:
|
|
22
|
+
try:
|
|
23
|
+
found = native_helpers.build("transfer")
|
|
24
|
+
except (LookupError, OSError, subprocess.SubprocessError, RuntimeError):
|
|
25
|
+
found = None
|
|
26
|
+
if found is None:
|
|
27
|
+
raise PhoneIOError("This Mac is missing Mobster's transfer helper.", "tools",
|
|
28
|
+
"Install Xcode's command line tools (xcode-select --install), or use the Mobster app.")
|
|
29
|
+
return str(found)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run(argv, timeout, runner=None):
|
|
33
|
+
from ..native_helpers import require_devices
|
|
34
|
+
require_devices()
|
|
35
|
+
if runner is not None:
|
|
36
|
+
return runner(argv, timeout)
|
|
37
|
+
try:
|
|
38
|
+
done = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL)
|
|
39
|
+
except subprocess.TimeoutExpired:
|
|
40
|
+
return None
|
|
41
|
+
except OSError as error:
|
|
42
|
+
raise PhoneIOError(f"The transfer helper couldn't run ({type(error).__name__}).", "tools") from None
|
|
43
|
+
return done.stdout
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _answer(output):
|
|
47
|
+
for line in reversed((output or "").strip().splitlines()):
|
|
48
|
+
try:
|
|
49
|
+
value = json.loads(line)
|
|
50
|
+
except ValueError:
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(value, dict):
|
|
53
|
+
return value
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _files(paths):
|
|
58
|
+
files = [str(Path(p).expanduser().resolve()) for p in paths]
|
|
59
|
+
if not files:
|
|
60
|
+
raise PhoneIOError("Name at least one file.", "usage")
|
|
61
|
+
for file in files:
|
|
62
|
+
if not os.path.isfile(file):
|
|
63
|
+
raise PhoneIOError(f"There is no file at {file}.", "usage")
|
|
64
|
+
return files
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def airdrop(paths, to, *, wait=AIRDROP_WAIT_S, transfer_wait=900, runner=None, tool=None):
|
|
68
|
+
"""AirDrop ``paths`` to the device named exactly ``to``; ``unknown``: the helper went silent after the press."""
|
|
69
|
+
if not isinstance(to, str) or not to.strip() or len(to) > 120:
|
|
70
|
+
raise PhoneIOError("Name the iPhone AirDrop should send to.", "usage")
|
|
71
|
+
files = _files(paths)
|
|
72
|
+
argv = [tool or helper(), "airdrop", "--to", to, "--timeout", str(int(wait)), "--transfer-timeout",
|
|
73
|
+
str(int(transfer_wait)), *files]
|
|
74
|
+
output = _run(argv, wait + transfer_wait + HELPER_SLACK_S, runner)
|
|
75
|
+
answer = _answer(output)
|
|
76
|
+
if answer is None:
|
|
77
|
+
raise PhoneIOError(f"AirDrop to {to} stopped answering, so the files may or may not have arrived.",
|
|
78
|
+
"unknown", "Look in Photos on the iPhone before you send them again.")
|
|
79
|
+
if answer.get("ok"):
|
|
80
|
+
return {"sent": int(answer.get("sent") or len(files)), "seconds": float(answer.get("seconds") or 0)}
|
|
81
|
+
code = answer.get("code")
|
|
82
|
+
if code == "permission":
|
|
83
|
+
raise PhoneIOError(PERMISSION[0], "permission", PERMISSION[1])
|
|
84
|
+
if code == "not_found":
|
|
85
|
+
error = PhoneIOError(f"AirDrop doesn't see {to}.", "airdrop_not_found",
|
|
86
|
+
"Unlock it, keep it near this Mac with Wi-Fi and Bluetooth on, and set AirDrop to "
|
|
87
|
+
"Contacts Only or Everyone for 10 Minutes.")
|
|
88
|
+
error.seen = [str(name)[:80] for name in answer.get("seen") or ()][:20]
|
|
89
|
+
raise error
|
|
90
|
+
if code == "ambiguous":
|
|
91
|
+
raise PhoneIOError(f"More than one device near this Mac is called {to}, so Mobster sent nothing.",
|
|
92
|
+
"airdrop_ambiguous", "Give the iPhone its own name in Settings › General › About › Name.")
|
|
93
|
+
if code in ("failed", "cancelled"):
|
|
94
|
+
raise PhoneIOError(f"AirDrop to {to} didn't go through: {answer.get('error') or 'it was declined'}.",
|
|
95
|
+
"airdrop_declined", "Accept it on the iPhone, then try again.")
|
|
96
|
+
if code == "timeout":
|
|
97
|
+
raise PhoneIOError(f"AirDrop to {to} didn't finish in time, so the files may or may not have arrived.",
|
|
98
|
+
"unknown", "Look in Photos on the iPhone before you send them again.")
|
|
99
|
+
raise PhoneIOError(f"AirDrop didn't work: {answer.get('error') or code}.", "failed")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def mirroring_drop(paths, x=0.5, y=0.5, *, runner=None, tool=None):
|
|
103
|
+
"""Drop ``paths`` on the iPhone Mirroring window at (x, y), fractions of the window. Returns {"operation"}."""
|
|
104
|
+
files = _files(paths)
|
|
105
|
+
if not (0 <= float(x) <= 1 and 0 <= float(y) <= 1):
|
|
106
|
+
raise PhoneIOError("x and y are fractions of the window, 0 to 1.", "usage")
|
|
107
|
+
argv = [tool or helper(), "drop", "--x", f"{float(x):.4f}", "--y", f"{float(y):.4f}", *files]
|
|
108
|
+
answer = _answer(_run(argv, 30, runner))
|
|
109
|
+
if answer is None:
|
|
110
|
+
raise PhoneIOError("The drop on iPhone Mirroring stopped answering, so the files may or may not have arrived.",
|
|
111
|
+
"unknown", "Look on the iPhone before you send them again.")
|
|
112
|
+
if answer.get("ok"):
|
|
113
|
+
return {"operation": int(answer.get("operation") or 1)}
|
|
114
|
+
code = answer.get("code")
|
|
115
|
+
if code == "permission":
|
|
116
|
+
raise PhoneIOError(PERMISSION[0], "permission", PERMISSION[1])
|
|
117
|
+
if code == "no_window":
|
|
118
|
+
raise PhoneIOError("iPhone Mirroring isn't showing the iPhone.", "no_mirroring",
|
|
119
|
+
"Open iPhone Mirroring, lock the iPhone and keep it near this Mac, then try again.")
|
|
120
|
+
if code == "refused":
|
|
121
|
+
raise PhoneIOError("iPhone Mirroring didn't take the files: the app in front on the iPhone doesn't take drops.",
|
|
122
|
+
"mirroring_refused", "Open Photos or Files on the iPhone, then try again.")
|
|
123
|
+
raise PhoneIOError(f"The drop on iPhone Mirroring didn't work: {answer.get('error') or code}.", "failed")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Photos both ways through iCloud Photos, when the person has it on, on this Mac and the iPhone.
|
|
2
|
+
|
|
3
|
+
Mac to iPhone: ``import_media`` adds the files to this Mac's Photos library (into the album ``album``) with Photos'
|
|
4
|
+
own AppleScript import; iCloud Photos then carries them to every device signed in to the same Apple Account, usually
|
|
5
|
+
within a minute or two on Wi-Fi. iPhone to Mac: ``export_recent`` copies the newest items in this Mac's library (which
|
|
6
|
+
iCloud fills from the iPhone) to a folder, originals as they are.
|
|
7
|
+
|
|
8
|
+
It is the slow route: Mobster uses it only when you choose it (``--via icloud``, or the queue's route), never as a
|
|
9
|
+
silent fallback. It needs Automation permission for Photos the first time (macOS asks). Photos opens in the
|
|
10
|
+
background to do it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import PhoneIOError
|
|
18
|
+
|
|
19
|
+
OSASCRIPT = "/usr/bin/osascript"
|
|
20
|
+
TIMEOUT_S = 300
|
|
21
|
+
|
|
22
|
+
# Each script takes its inputs as arguments (argv), never pasted into its source.
|
|
23
|
+
IMPORT_SCRIPT = """
|
|
24
|
+
on run argv
|
|
25
|
+
set albumName to item 1 of argv
|
|
26
|
+
set theFiles to {}
|
|
27
|
+
repeat with i from 2 to count of argv
|
|
28
|
+
set end of theFiles to (POSIX file (item i of argv)) as alias
|
|
29
|
+
end repeat
|
|
30
|
+
tell application "Photos"
|
|
31
|
+
if albumName is "" then
|
|
32
|
+
set imported to import theFiles skip check duplicates true
|
|
33
|
+
else
|
|
34
|
+
if not (exists album albumName) then make new album named albumName
|
|
35
|
+
set imported to import theFiles into album albumName skip check duplicates true
|
|
36
|
+
end if
|
|
37
|
+
return count of imported
|
|
38
|
+
end tell
|
|
39
|
+
end run
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
EXPORT_SCRIPT = """
|
|
43
|
+
on run argv
|
|
44
|
+
set target to POSIX file (item 1 of argv) as alias
|
|
45
|
+
set wanted to (item 2 of argv) as integer
|
|
46
|
+
tell application "Photos"
|
|
47
|
+
set total to count of media items
|
|
48
|
+
if total is 0 then return "[]"
|
|
49
|
+
set first_index to total - wanted + 1
|
|
50
|
+
if first_index < 1 then set first_index to 1
|
|
51
|
+
set picked to items first_index thru total of (get media items)
|
|
52
|
+
export picked to target with using originals
|
|
53
|
+
set names to {}
|
|
54
|
+
repeat with one in picked
|
|
55
|
+
set end of names to filename of one
|
|
56
|
+
end repeat
|
|
57
|
+
end tell
|
|
58
|
+
set AppleScript's text item delimiters to linefeed
|
|
59
|
+
return names as text
|
|
60
|
+
end run
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _osascript(script, args, timeout=TIMEOUT_S, runner=None):
|
|
65
|
+
from ..native_helpers import require_devices
|
|
66
|
+
require_devices()
|
|
67
|
+
argv = [OSASCRIPT, "-", *map(str, args)]
|
|
68
|
+
if runner is not None:
|
|
69
|
+
return runner(argv, script, timeout)
|
|
70
|
+
try:
|
|
71
|
+
done = subprocess.run(argv, input=script, capture_output=True, text=True, timeout=timeout)
|
|
72
|
+
except subprocess.TimeoutExpired:
|
|
73
|
+
raise PhoneIOError("Photos on this Mac didn't answer in time.", "unknown",
|
|
74
|
+
"Open Photos and look before you try again.") from None
|
|
75
|
+
except OSError as error:
|
|
76
|
+
raise PhoneIOError(f"Photos couldn't be reached ({type(error).__name__}).", "tools") from None
|
|
77
|
+
return done.returncode, done.stdout, done.stderr
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _explain(code, stderr):
|
|
81
|
+
text = " ".join((stderr or "").split())
|
|
82
|
+
if "-1743" in text or "Not authorized" in text:
|
|
83
|
+
return PhoneIOError("Mobster isn't allowed to use Photos on this Mac.", "permission",
|
|
84
|
+
"Allow it in System Settings › Privacy & Security › Automation › Photos.")
|
|
85
|
+
return PhoneIOError(f"Photos on this Mac refused: {text[:160] or f'exit {code}'}.", "failed")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def import_media(paths, album="Mobster", runner=None):
|
|
89
|
+
"""Add ``paths`` to this Mac's Photos library (and ``album``)."""
|
|
90
|
+
files = [str(Path(p).expanduser().resolve()) for p in paths]
|
|
91
|
+
for file in files:
|
|
92
|
+
if not os.path.isfile(file):
|
|
93
|
+
raise PhoneIOError(f"There is no file at {file}.", "usage")
|
|
94
|
+
code, out, err = _osascript(IMPORT_SCRIPT, [album or "", *files], runner=runner)
|
|
95
|
+
if code != 0:
|
|
96
|
+
raise _explain(code, err)
|
|
97
|
+
try:
|
|
98
|
+
count = int((out or "0").strip() or 0)
|
|
99
|
+
except ValueError:
|
|
100
|
+
count = 0
|
|
101
|
+
if count != len(files):
|
|
102
|
+
raise PhoneIOError(f"Photos on this Mac took {count} of {len(files)}.", "failed",
|
|
103
|
+
"Check that each one opens in Photos, then try again.")
|
|
104
|
+
return {"imported": count}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def export_recent(folder, count=1, runner=None):
|
|
108
|
+
"""Copy the newest ``count`` items of this Mac's Photos library (originals) into ``folder``. Returns [paths]."""
|
|
109
|
+
folder = Path(folder).expanduser()
|
|
110
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
before = {p.name for p in folder.iterdir()}
|
|
112
|
+
code, out, err = _osascript(EXPORT_SCRIPT, [str(folder.resolve()), max(1, min(int(count), 20))], runner=runner)
|
|
113
|
+
if code != 0:
|
|
114
|
+
raise _explain(code, err)
|
|
115
|
+
names = [line.strip() for line in (out or "").splitlines() if line.strip()]
|
|
116
|
+
arrived = sorted(p for p in folder.iterdir() if p.name not in before and p.is_file())
|
|
117
|
+
return {"names": names, "paths": [str(p) for p in arrived]}
|
|
118
|
+
|