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,46 @@
|
|
|
1
|
+
"""`mobster doctor --apps`: score the apps on the phone for automation. Hidden; needs MOBSTER_EXPERIMENTAL_ATTACH=1."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from . import ENV_FLAG, enabled, score
|
|
9
|
+
|
|
10
|
+
SETTLE_SECONDS = 3.0
|
|
11
|
+
ENV_HINT = f"mobster doctor --apps is experimental: set {ENV_FLAG}=1 to use it"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def installed_bundles(udid):
|
|
15
|
+
"""User-app bundle IDs on the phone, or () when the listing is unavailable."""
|
|
16
|
+
from ..installed_apps import InstalledApps
|
|
17
|
+
inventory = InstalledApps(lambda: {"udid": udid, "trusted": True}).refresh() if udid else None
|
|
18
|
+
return tuple(app["bundleId"] for app in inventory.user) if inventory else ()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(args, wda_url):
|
|
22
|
+
"""Launch each installed app, read its first screen, store and print the scores. Returns an exit code."""
|
|
23
|
+
from ..compose import build_target_driver
|
|
24
|
+
from ..verify.tree import read_tree
|
|
25
|
+
if not enabled():
|
|
26
|
+
print(ENV_HINT, file=sys.stderr)
|
|
27
|
+
return 2
|
|
28
|
+
with urllib.request.urlopen(wda_url + "/status", timeout=10) as response:
|
|
29
|
+
session = json.load(response)["sessionId"]
|
|
30
|
+
driver = build_target_driver(wda_url=wda_url, session=session)
|
|
31
|
+
bundles = installed_bundles(getattr(args, "device_udid", None))
|
|
32
|
+
if not bundles:
|
|
33
|
+
print("mobster doctor --apps: no installed apps could be listed. Plug the iPhone in with a cable, or "
|
|
34
|
+
"name a USB device with --device.", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
scores = score.score_apps(
|
|
37
|
+
lambda: read_tree(driver), lambda b: driver.call("POST", "/wda/apps/activate", {"bundleId": b}, timeout=20),
|
|
38
|
+
bundles, settle=lambda: time.sleep(SETTLE_SECONDS))
|
|
39
|
+
driver.call("POST", "/wda/pressButton", {"name": "home"}, timeout=10)
|
|
40
|
+
path = score.save(scores)
|
|
41
|
+
if getattr(args, "json", False):
|
|
42
|
+
print(json.dumps({"ok": True, "apps": [s.public() for s in scores], "stored": str(path)}))
|
|
43
|
+
else:
|
|
44
|
+
print(score.report(scores))
|
|
45
|
+
print(f"Stored in {path}")
|
|
46
|
+
return 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Recognise Flutter from engine marks or the shape of its accessibility tree.
|
|
2
|
+
|
|
3
|
+
The shape heuristic uses labelled Other nodes and typed-control density. It is
|
|
4
|
+
covered by synthetic fixtures, not calibrated against a real-app survey.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
MARKS = ("fluttersemantics", "flutterview", "flutter_view")
|
|
8
|
+
TYPED = frozenset({"Button", "Cell", "Link", "TextField", "SearchField", "TextView", "Switch", "Slider", "Tab",
|
|
9
|
+
"SegmentedControl", "Stepper", "Toggle", "NavigationBar", "TabBar", "Table", "CollectionView",
|
|
10
|
+
"Picker", "PickerWheel", "Key"})
|
|
11
|
+
MIN_NODES = 6
|
|
12
|
+
OTHER_SHARE = .6
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _marked(node):
|
|
16
|
+
return any(mark in f"{node.identifier} {node.label}".casefold().replace(" ", "") for mark in MARKS)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_flutter(tree):
|
|
20
|
+
"""Whether the shown tree has Flutter's shape: engine marks, or mostly labelled ``Other`` and few typed controls."""
|
|
21
|
+
shown = tree.shown()
|
|
22
|
+
if any(_marked(node) for node in shown):
|
|
23
|
+
return True
|
|
24
|
+
labelled = [node for node in shown if node.label.strip() or node.identifier]
|
|
25
|
+
if len(labelled) < MIN_NODES:
|
|
26
|
+
return False
|
|
27
|
+
other = sum(node.role == "Other" for node in labelled)
|
|
28
|
+
return other / len(labelled) >= OTHER_SHARE and sum(node.role in TYPED for node in shown) <= len(labelled) // 4
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Rate an app for automation from the accessibility tree it publishes: good, partial or poor, with reasons."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
|
|
7
|
+
from ..paths import user_data_dir, write_text_atomic
|
|
8
|
+
from . import flutter, web
|
|
9
|
+
|
|
10
|
+
ACTIONABLE = frozenset({"Button", "Cell", "Link", "TextField", "SearchField", "TextView", "Switch", "Slider", "Tab",
|
|
11
|
+
"Key", "SegmentedControl", "PickerWheel", "Stepper", "Toggle"})
|
|
12
|
+
OPAQUE_NODES = 3
|
|
13
|
+
GOOD_LABELLED = .85
|
|
14
|
+
PARTIAL_LABELLED = .6
|
|
15
|
+
MIN_DENSITY = 8 # shown nodes below this on a full screen leave the agent little to act on
|
|
16
|
+
RATINGS = ("good", "partial", "poor")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AppScore:
|
|
21
|
+
bundle_id: str
|
|
22
|
+
rating: str
|
|
23
|
+
reasons: tuple
|
|
24
|
+
nodes: int
|
|
25
|
+
actionable: int
|
|
26
|
+
labelled_share: float | None
|
|
27
|
+
webview: bool
|
|
28
|
+
flutter: bool
|
|
29
|
+
scored_at: float
|
|
30
|
+
|
|
31
|
+
def public(self):
|
|
32
|
+
return {**asdict(self), "reasons": list(self.reasons)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def score_tree(tree, bundle_id=None, now=None):
|
|
36
|
+
"""Score one screen. The caller decides which screen speaks for the app (its first screen after launch)."""
|
|
37
|
+
shown = tree.shown()
|
|
38
|
+
actionable = [node for node in shown if node.role in ACTIONABLE]
|
|
39
|
+
labelled = [node for node in actionable if (node.label or node.identifier or node.value).strip()]
|
|
40
|
+
share = len(labelled) / len(actionable) if actionable else None
|
|
41
|
+
view = web.web_view(tree)
|
|
42
|
+
has_web = view is not None
|
|
43
|
+
has_flutter = flutter.is_flutter(tree)
|
|
44
|
+
reasons, rating = [], "good"
|
|
45
|
+
|
|
46
|
+
def lower(to, why):
|
|
47
|
+
nonlocal rating
|
|
48
|
+
reasons.append(why)
|
|
49
|
+
if RATINGS.index(to) > RATINGS.index(rating):
|
|
50
|
+
rating = to
|
|
51
|
+
|
|
52
|
+
if len(shown) <= OPAQUE_NODES:
|
|
53
|
+
lower("poor", f"the tree has {len(shown)} elements: the screen is drawn, not published")
|
|
54
|
+
elif len(shown) < MIN_DENSITY:
|
|
55
|
+
lower("partial", f"the tree has {len(shown)} elements, too few to act on with confidence")
|
|
56
|
+
if share is None:
|
|
57
|
+
if len(shown) > OPAQUE_NODES:
|
|
58
|
+
lower("partial", "no buttons, fields or links are exposed")
|
|
59
|
+
elif share < PARTIAL_LABELLED:
|
|
60
|
+
lower("poor", f"{len(labelled)} of {len(actionable)} controls have a label ({share:.0%})")
|
|
61
|
+
elif share < GOOD_LABELLED:
|
|
62
|
+
lower("partial", f"{len(labelled)} of {len(actionable)} controls have a label ({share:.0%})")
|
|
63
|
+
if has_web:
|
|
64
|
+
inside = sum(1 for node in shown if web.inside(node, view)) - 1
|
|
65
|
+
lower("partial", f"a WebView holds the content ({inside} accessibility elements inside it); "
|
|
66
|
+
"only its published accessibility elements are scored")
|
|
67
|
+
if has_flutter:
|
|
68
|
+
lower("partial", "Flutter draws its own widgets; its semantics tree is the only thing the agent can read")
|
|
69
|
+
if not reasons:
|
|
70
|
+
reasons.append(f"{len(labelled)} of {len(actionable)} controls are labelled across {len(shown)} elements")
|
|
71
|
+
return AppScore(bundle_id or tree.bundle_id, rating, tuple(reasons), len(shown), len(actionable),
|
|
72
|
+
None if share is None else round(share, 3), has_web, has_flutter,
|
|
73
|
+
time.time() if now is None else now)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def score_apps(read_tree, launch, bundles, settle=lambda: None):
|
|
77
|
+
"""Launch each bundle, read its tree and score it. ``launch(bundle)`` and ``read_tree()`` are the driver's.
|
|
78
|
+
|
|
79
|
+
An app that will not open or read is recorded as poor with the reason, so one bad app never stops the survey.
|
|
80
|
+
"""
|
|
81
|
+
scores = []
|
|
82
|
+
for bundle in bundles:
|
|
83
|
+
try:
|
|
84
|
+
launch(bundle)
|
|
85
|
+
settle()
|
|
86
|
+
tree = read_tree()
|
|
87
|
+
except Exception as error:
|
|
88
|
+
scores.append(AppScore(bundle, "poor", (f"could not be read ({type(error).__name__})",), 0, 0, None,
|
|
89
|
+
False, False, time.time()))
|
|
90
|
+
continue
|
|
91
|
+
scores.append(score_tree(tree, bundle))
|
|
92
|
+
return scores
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def store_path():
|
|
96
|
+
return user_data_dir() / "attach" / "app-scores.json"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def save(scores, path=None):
|
|
100
|
+
"""Merge ``scores`` into the stored file by bundle ID (newest wins) and return the path."""
|
|
101
|
+
path = path or store_path()
|
|
102
|
+
merged = {row["bundle_id"]: row for row in load(path)}
|
|
103
|
+
merged.update({score.bundle_id: score.public() for score in scores})
|
|
104
|
+
return write_text_atomic(path, json.dumps(sorted(merged.values(), key=lambda row: row["bundle_id"]), indent=1))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load(path=None):
|
|
108
|
+
try:
|
|
109
|
+
rows = json.loads((path or store_path()).read_text())
|
|
110
|
+
except (OSError, ValueError):
|
|
111
|
+
return []
|
|
112
|
+
return [row for row in rows if isinstance(row, dict) and isinstance(row.get("bundle_id"), str)] \
|
|
113
|
+
if isinstance(rows, list) else []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def report(scores):
|
|
117
|
+
"""Plain lines: bundle, rating and the first reason."""
|
|
118
|
+
width = max((len(score.bundle_id) for score in scores), default=0)
|
|
119
|
+
return "\n".join(f"{score.bundle_id:{width}} {score.rating:7} {score.reasons[0]}" for score in scores)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""WebView detection for the accessibility-based app score."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def web_view(tree):
|
|
5
|
+
"""The largest shown WebView in the accessibility tree, or None."""
|
|
6
|
+
views = [node for node in tree.shown() if node.role == "WebView"]
|
|
7
|
+
return max(views, key=lambda node: node.rect[2] * node.rect[3], default=None)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def inside(node, view):
|
|
11
|
+
x, y, w, h = node.rect
|
|
12
|
+
vx, vy, vw, vh = view.rect
|
|
13
|
+
return node is view or (x >= vx and y >= vy and x + w <= vx + vw + 1 and y + h <= vy + vh + 1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Track `files`: files attached to tasks and conversations, and files to and from the phone (docs/files.md)."""
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""What Mobster's agent gets from attached files: one context block, and the READ_ATTACHMENT and PUT_FILE tools."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .. import harness_api
|
|
8
|
+
from ..agent_hooks import Prepared, SkillResult
|
|
9
|
+
from . import sniff
|
|
10
|
+
from .extract import pages_of
|
|
11
|
+
|
|
12
|
+
MAX_CHARS = 12_000
|
|
13
|
+
READ_CHARS = 6_000
|
|
14
|
+
MIN_SHARE = 800
|
|
15
|
+
MAX_IMAGES = 4
|
|
16
|
+
TITLE = "Files the user attached to this task"
|
|
17
|
+
CLIPBOARD_MAX = 64 * 1024
|
|
18
|
+
# The approval title's parts: with "Put “…” in … on …?" they stay within the 160 characters the card shows.
|
|
19
|
+
NAME_CHARS, APP_CHARS, DEVICE_CHARS = 48, 40, 40
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _defang(text):
|
|
23
|
+
"""File text can't close the fence it sits in."""
|
|
24
|
+
return str(text or "").replace("<<<", "‹‹‹").replace(">>>", "›››")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _short(text, limit):
|
|
28
|
+
"""``text`` at most ``limit`` characters, cut in the middle so a file keeps its extension ("Luigi’s…menu.pdf").
|
|
29
|
+
The approval card's title is at most 160 characters: a long file name must never push out where it goes."""
|
|
30
|
+
text = str(text or "")
|
|
31
|
+
if len(text) <= limit:
|
|
32
|
+
return text
|
|
33
|
+
stem, dot, ext = text.rpartition(".")
|
|
34
|
+
tail = f".{ext}" if dot and stem and len(ext) <= 8 else ""
|
|
35
|
+
head = text[:len(text) - len(tail)] if tail else text
|
|
36
|
+
keep = limit - len(tail) - 1
|
|
37
|
+
return head[:max(1, keep)].rstrip() + "…" + tail
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def describe(attachment):
|
|
41
|
+
""""menu.pdf: PDF, 2 pages, 412 KB"."""
|
|
42
|
+
data = attachment.data
|
|
43
|
+
parts = [data.get("label") or attachment.kind or "file"]
|
|
44
|
+
if data.get("pages"):
|
|
45
|
+
parts.append(f"{data['pages']} page" + ("" if data["pages"] == 1 else "s"))
|
|
46
|
+
if data.get("rows") is not None:
|
|
47
|
+
parts.append(f"{data['rows']:,} row" + ("" if data["rows"] == 1 else "s"))
|
|
48
|
+
if data.get("width") and data.get("height"):
|
|
49
|
+
parts.append(f"{data['width']} × {data['height']}")
|
|
50
|
+
parts.append(sniff.size_words(data.get("bytes")))
|
|
51
|
+
return f"{attachment.name}: " + ", ".join(parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def chunks(attachment, text):
|
|
55
|
+
"""The pages READ_ATTACHMENT serves: a PDF's own pages, else 6,000-character parts of the text."""
|
|
56
|
+
if attachment.kind == "pdf":
|
|
57
|
+
return pages_of(text)
|
|
58
|
+
if not text:
|
|
59
|
+
return [""]
|
|
60
|
+
return [text[i:i + READ_CHARS] for i in range(0, len(text), READ_CHARS)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find(attachments, wanted):
|
|
64
|
+
"""The attachment ``wanted`` names: its exact name (any case), else the one name containing it, else the only file
|
|
65
|
+
there is."""
|
|
66
|
+
wanted = " ".join(str(wanted or "").strip().strip("\"'“”‘’").split()).casefold()
|
|
67
|
+
if not attachments:
|
|
68
|
+
return None
|
|
69
|
+
exact = [a for a in attachments if a.name.casefold() == wanted]
|
|
70
|
+
if exact:
|
|
71
|
+
return exact[0]
|
|
72
|
+
partial = [a for a in attachments if wanted and (wanted in a.name.casefold() or a.name.casefold() in wanted)]
|
|
73
|
+
if len(partial) == 1:
|
|
74
|
+
return partial[0]
|
|
75
|
+
if len(attachments) == 1 and not partial:
|
|
76
|
+
return attachments[0]
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_files(ctx, store):
|
|
81
|
+
"""The attachments this run may read: the ones its ``attachmentIds`` names that exist and belong to no other
|
|
82
|
+
conversation."""
|
|
83
|
+
ids = list(ctx.extras.get("attachmentIds") or ())
|
|
84
|
+
if store is None or not ids:
|
|
85
|
+
return []
|
|
86
|
+
thread = ctx.thread_id
|
|
87
|
+
return [a for a in store.many(ids) if a.thread_id is None or thread is None or a.thread_id == thread]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AttachmentsProvider:
|
|
91
|
+
"""harness_api.ContextProvider ``attachments``: one stable, untrusted block for the run's files."""
|
|
92
|
+
|
|
93
|
+
key = "attachments"
|
|
94
|
+
max_chars = MAX_CHARS
|
|
95
|
+
|
|
96
|
+
def __init__(self, store):
|
|
97
|
+
self.store = store
|
|
98
|
+
|
|
99
|
+
def blocks(self, ctx):
|
|
100
|
+
files = run_files(ctx, self.store)
|
|
101
|
+
if not files:
|
|
102
|
+
return ()
|
|
103
|
+
try:
|
|
104
|
+
# This task reads them: record it, so they stay while it is in history and join its conversation.
|
|
105
|
+
self.store.attach([a.id for a in files], ctx.run_id, ctx.thread_id)
|
|
106
|
+
except Exception: # noqa: BLE001 -- the block matters more than the bookkeeping
|
|
107
|
+
harness_api.log.exception("could not record which task read its files")
|
|
108
|
+
texts = {a.id: self.store.text(a) for a in files}
|
|
109
|
+
headers = [f"{index}. {describe(a)}" for index, a in enumerate(files, 1)]
|
|
110
|
+
room = max(MIN_SHARE * len(files), MAX_CHARS - sum(len(h) + 80 for h in headers))
|
|
111
|
+
shares = _shares([len(texts[a.id]) for a in files], room)
|
|
112
|
+
parts, images = [], []
|
|
113
|
+
for index, (attachment, header, share) in enumerate(zip(files, headers, shares), 1):
|
|
114
|
+
body = self._excerpt(attachment, texts[attachment.id], share, images)
|
|
115
|
+
parts.append(header + ("\n" + body if body else ""))
|
|
116
|
+
try:
|
|
117
|
+
ctx.emit({"event": "attachment_read", "id": attachment.id, "name": attachment.name,
|
|
118
|
+
**({"pages": attachment.data["pages"]} if attachment.data.get("pages") else {})})
|
|
119
|
+
except Exception: # noqa: BLE001 -- an event never stops the block
|
|
120
|
+
pass
|
|
121
|
+
text = _defang("\n\n".join(parts))
|
|
122
|
+
if len(text) > MAX_CHARS:
|
|
123
|
+
text = text[:MAX_CHARS - 1] + "…"
|
|
124
|
+
return (harness_api.ContextBlock(self.key, TITLE, text, stable=True, untrusted=True,
|
|
125
|
+
images=tuple(images[:MAX_IMAGES])),)
|
|
126
|
+
|
|
127
|
+
def turn_blocks(self, ctx, turn):
|
|
128
|
+
return ()
|
|
129
|
+
|
|
130
|
+
def _excerpt(self, attachment, text, share, images):
|
|
131
|
+
name = attachment.name
|
|
132
|
+
if attachment.kind == "image":
|
|
133
|
+
lines = []
|
|
134
|
+
model = self.store.path(attachment, "model")
|
|
135
|
+
if model is not None and len(images) < MAX_IMAGES:
|
|
136
|
+
images.append(("image/jpeg", model.read_bytes()))
|
|
137
|
+
lines.append(f"Shown to you as picture {len(images)} above.")
|
|
138
|
+
else:
|
|
139
|
+
lines.append("Not shown to you as a picture." if model is None
|
|
140
|
+
else f"Not shown: at most {MAX_IMAGES} pictures go with a task.")
|
|
141
|
+
if text.strip():
|
|
142
|
+
lines.append("Text in the picture:\n" + _cut(text.strip(), share))
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
if not attachment.data.get("textAvailable") and not text.strip():
|
|
145
|
+
return ("Mobster couldn't read text from this file on this Mac."
|
|
146
|
+
if attachment.kind == "pdf" else "It holds no text.")
|
|
147
|
+
if attachment.kind == "pdf":
|
|
148
|
+
pages = pages_of(text)
|
|
149
|
+
out, used, shown = [], 0, 0
|
|
150
|
+
for number, page in enumerate(pages, 1):
|
|
151
|
+
page = page.strip()
|
|
152
|
+
block = f"Page {number}:\n{page or '(no text on this page)'}"
|
|
153
|
+
if used + len(block) > share and shown:
|
|
154
|
+
break
|
|
155
|
+
if used + len(block) > share:
|
|
156
|
+
block = _cut(block, share - used)
|
|
157
|
+
out.append(block)
|
|
158
|
+
used += len(block) + 2
|
|
159
|
+
shown = number
|
|
160
|
+
if shown < len(pages):
|
|
161
|
+
out.append(f"[Pages {shown + 1}–{len(pages)} not shown: READ_ATTACHMENT \"{name} page {shown + 1}\"]")
|
|
162
|
+
elif out and out[-1].endswith("…"):
|
|
163
|
+
out.append(f"[Page {shown} cut short: READ_ATTACHMENT \"{name} page {shown}\" reads it all]")
|
|
164
|
+
return "\n\n".join(out)
|
|
165
|
+
if len(text) <= share:
|
|
166
|
+
return text.strip()
|
|
167
|
+
parts = chunks(attachment, text)
|
|
168
|
+
shown = _cut(text, share)
|
|
169
|
+
next_page = min(len(parts), len(shown) // READ_CHARS + 1)
|
|
170
|
+
note = (f"[{len(text) - len(shown) + 1:,} more characters: READ_ATTACHMENT \"{name} page {next_page}\" "
|
|
171
|
+
f"(page {next_page} of {len(parts)})]")
|
|
172
|
+
return shown + "\n" + note
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _cut(text, limit):
|
|
176
|
+
limit = max(1, int(limit))
|
|
177
|
+
return text if len(text) <= limit else text[:limit - 1].rstrip() + "…"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _shares(lengths, room):
|
|
181
|
+
"""Each file's characters: an equal share, with what short files don't need handed to the long ones."""
|
|
182
|
+
if not lengths:
|
|
183
|
+
return []
|
|
184
|
+
shares = [0] * len(lengths)
|
|
185
|
+
left, open_ = room, list(range(len(lengths)))
|
|
186
|
+
while open_ and left > 0:
|
|
187
|
+
fair = left // len(open_)
|
|
188
|
+
done = [i for i in open_ if lengths[i] - shares[i] <= fair]
|
|
189
|
+
if not done:
|
|
190
|
+
for i in open_:
|
|
191
|
+
shares[i] += fair
|
|
192
|
+
break
|
|
193
|
+
for i in done:
|
|
194
|
+
left -= lengths[i] - shares[i]
|
|
195
|
+
shares[i] = lengths[i]
|
|
196
|
+
open_ = [i for i in open_ if i not in done]
|
|
197
|
+
return [max(MIN_SHARE, s) for s in shares]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
READ_TEXT = re.compile(r"^\s*(?P<name>.+?)(?:\s*(?:,|#|\bp\.?|\bpage|\bpart)\s*(?P<page>\d{1,4}))?\s*$", re.I)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ReadAttachment:
|
|
204
|
+
"""agent_hooks.Skill READ_ATTACHMENT: a page of an attached file, fenced as data."""
|
|
205
|
+
|
|
206
|
+
op = "READ_ATTACHMENT"
|
|
207
|
+
prompt = ("- READ_ATTACHMENT text: read more of a file the user attached; text is its name, then \"page N\" for "
|
|
208
|
+
"a PDF's page or a later part of a long text (\"menu.pdf page 2\"). The text comes back next turn.")
|
|
209
|
+
needs_target = False
|
|
210
|
+
|
|
211
|
+
def __init__(self, store, ctx):
|
|
212
|
+
self.store, self.ctx = store, ctx
|
|
213
|
+
|
|
214
|
+
def available(self, ctx):
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def prepare(self, ctx, act, snapshot):
|
|
218
|
+
return Prepared(None)
|
|
219
|
+
|
|
220
|
+
def perform(self, ctx, prepared, act, snapshot):
|
|
221
|
+
files = run_files(self.ctx, self.store)
|
|
222
|
+
found = READ_TEXT.match(str(act.get("text") or ""))
|
|
223
|
+
attachment = find(files, found.group("name") if found else "")
|
|
224
|
+
if attachment is None:
|
|
225
|
+
names = ", ".join(f"“{a.name}”" for a in files) or "none"
|
|
226
|
+
return SkillResult(feedback=f"READ_ATTACHMENT needs one file's name. The attached files: {names}.")
|
|
227
|
+
parts = chunks(attachment, self.store.text(attachment))
|
|
228
|
+
page = int(found.group("page")) if found and found.group("page") else 1
|
|
229
|
+
if not 1 <= page <= len(parts):
|
|
230
|
+
return SkillResult(feedback=f"“{attachment.name}” has {len(parts)} page"
|
|
231
|
+
f"{'' if len(parts) == 1 else 's'}; ask for page 1 to {len(parts)}.")
|
|
232
|
+
text = parts[page - 1].strip() or "(no text on this page)"
|
|
233
|
+
if len(text) > READ_CHARS:
|
|
234
|
+
text = text[:READ_CHARS - 1] + "…"
|
|
235
|
+
try:
|
|
236
|
+
self.ctx.emit({"event": "attachment_read", "id": attachment.id, "name": attachment.name,
|
|
237
|
+
"page": page, **({"pages": len(parts)} if attachment.kind == "pdf" else {})})
|
|
238
|
+
except Exception: # noqa: BLE001
|
|
239
|
+
pass
|
|
240
|
+
title = _defang(f"{attachment.name}, page {page} of {len(parts)}")
|
|
241
|
+
more = f"\n(Next: READ_ATTACHMENT \"{attachment.name} page {page + 1}\".)" if page < len(parts) else ""
|
|
242
|
+
return SkillResult(feedback=harness_api.fence(title, _defang(text)) + more, changed=False)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
PUT_TEXT = re.compile(r"^\s*(?P<name>.+?)\s*(?:>|->|→|\bto\b|\binto\b|\bin\b)\s*(?P<dest>[A-Za-z0-9:._-]+)\s*$", re.I)
|
|
246
|
+
_apps_cache = {}
|
|
247
|
+
_apps_lock = threading.Lock()
|
|
248
|
+
APPS_TTL = 60.0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def file_sharing_apps(phone_files):
|
|
252
|
+
"""The phone's apps that share files, cached a minute per iPhone."""
|
|
253
|
+
key = phone_files.udid
|
|
254
|
+
with _apps_lock:
|
|
255
|
+
at, apps = _apps_cache.get(key, (0.0, None))
|
|
256
|
+
if apps is not None and time.monotonic() - at < APPS_TTL:
|
|
257
|
+
return apps
|
|
258
|
+
apps = phone_files.apps()
|
|
259
|
+
with _apps_lock:
|
|
260
|
+
_apps_cache[key] = (time.monotonic(), apps)
|
|
261
|
+
return apps
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class _Plan:
|
|
265
|
+
def __init__(self, refusal=None, attachment=None, destination=None, app_name=None, phone=None):
|
|
266
|
+
self.refusal, self.attachment, self.destination = refusal, attachment, destination
|
|
267
|
+
self.app_name, self.phone = app_name, phone
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class PutFile:
|
|
271
|
+
"""agent_hooks.Skill PUT_FILE: an attached file into an app's folder or onto the clipboard, after the user's OK (its
|
|
272
|
+
``prepare`` returns the question, so the frontier asks; with no way to ask, the copy never runs)."""
|
|
273
|
+
|
|
274
|
+
op = "PUT_FILE"
|
|
275
|
+
prompt = ("- PUT_FILE text: put a file the user attached on the iPhone, only when the task needs it there; text "
|
|
276
|
+
"is the file's name, \" > \", then clipboard (a text file, to paste), photos (an image, into the Photos "
|
|
277
|
+
"library so any app's photo picker can attach it) or the bundle id of an app whose folder shows in Files "
|
|
278
|
+
"(\"menu.pdf > com.apple.Pages\", \"beach.jpg > photos\"). Mobster asks the user first.")
|
|
279
|
+
needs_target = False
|
|
280
|
+
|
|
281
|
+
def __init__(self, store, ctx):
|
|
282
|
+
self.store, self.ctx = store, ctx
|
|
283
|
+
|
|
284
|
+
def available(self, ctx):
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
def _device_name(self):
|
|
288
|
+
try:
|
|
289
|
+
from .service import device_record
|
|
290
|
+
return _short(device_record(self.ctx.runtime, self.ctx.device_id)["name"], DEVICE_CHARS)
|
|
291
|
+
except Exception: # noqa: BLE001
|
|
292
|
+
return "your iPhone"
|
|
293
|
+
|
|
294
|
+
def prepare(self, ctx, act, snapshot):
|
|
295
|
+
found = PUT_TEXT.match(str(act.get("text") or ""))
|
|
296
|
+
files = run_files(self.ctx, self.store)
|
|
297
|
+
if not found:
|
|
298
|
+
return Prepared(None, _Plan("PUT_FILE needs the file's name, \" > \", then clipboard or an app's bundle "
|
|
299
|
+
"id, such as \"menu.pdf > com.apple.Pages\"."))
|
|
300
|
+
attachment = find(files, found.group("name"))
|
|
301
|
+
if attachment is None:
|
|
302
|
+
names = ", ".join(f"“{a.name}”" for a in files) or "none"
|
|
303
|
+
return Prepared(None, _Plan(f"PUT_FILE found no attached file by that name. The attached files: {names}."))
|
|
304
|
+
destination = found.group("dest")
|
|
305
|
+
destination = destination[4:] if destination.lower().startswith("app:") else destination
|
|
306
|
+
device = self._device_name()
|
|
307
|
+
if destination.lower() == "clipboard":
|
|
308
|
+
if attachment.kind not in ("text", "csv"):
|
|
309
|
+
return Prepared(None, _Plan(f"Only a text file goes on the clipboard; “{attachment.name}” is "
|
|
310
|
+
f"{attachment.data.get('label') or attachment.kind}. Put it in an app's "
|
|
311
|
+
"folder instead, or type what is needed."))
|
|
312
|
+
if (attachment.data.get("bytes") or 0) > CLIPBOARD_MAX:
|
|
313
|
+
return Prepared(None, _Plan(f"“{attachment.name}” is over 64 KB, too long for the clipboard."))
|
|
314
|
+
return Prepared(f"Put the text of “{_short(attachment.name, NAME_CHARS)}” on {device}'s clipboard?",
|
|
315
|
+
_Plan(attachment=attachment, destination="clipboard"))
|
|
316
|
+
if destination.lower() == "photos":
|
|
317
|
+
if attachment.kind != "image":
|
|
318
|
+
return Prepared(None, _Plan(f"Only a picture goes into Photos; “{attachment.name}” is "
|
|
319
|
+
f"{attachment.data.get('label') or attachment.kind}."))
|
|
320
|
+
return Prepared(f"Add “{_short(attachment.name, NAME_CHARS)}” to Photos on {device}?",
|
|
321
|
+
_Plan(attachment=attachment, destination="photos"))
|
|
322
|
+
from ..permissions import from_env as permission_settings
|
|
323
|
+
apps = permission_settings().apps
|
|
324
|
+
if not apps.allows(destination, outside=not self.ctx.interactive):
|
|
325
|
+
return Prepared(None, _Plan(apps.blocked_message(destination, outside=apps.allows(destination))))
|
|
326
|
+
if not self._approved(ctx, destination, apps):
|
|
327
|
+
# The app the file goes into has to be one the person allowed (the run's guard asks the first time).
|
|
328
|
+
return Prepared(None, _Plan(apps.unapproved_message(destination)))
|
|
329
|
+
try:
|
|
330
|
+
from . import service
|
|
331
|
+
record = service.device_record(self.ctx.runtime, self.ctx.device_id)
|
|
332
|
+
phone = service.phone_files(record)
|
|
333
|
+
apps = file_sharing_apps(phone)
|
|
334
|
+
except Exception as error: # noqa: BLE001 -- a phone that can't take files is a sentence for the model
|
|
335
|
+
return Prepared(None, _Plan(f"PUT_FILE can't reach the iPhone's files: {_sentence(error)}"))
|
|
336
|
+
app = next((a for a in apps if a["bundleId"] == destination), None)
|
|
337
|
+
if app is None:
|
|
338
|
+
names = ", ".join(f"{a['name']} ({a['bundleId']})" for a in apps[:12]) or "none on this iPhone"
|
|
339
|
+
return Prepared(None, _Plan(f"{destination} doesn't keep files you can see in Files. Apps that do: "
|
|
340
|
+
f"{names}."))
|
|
341
|
+
title = f"Put “{_short(attachment.name, NAME_CHARS)}” in {_short(app['name'], APP_CHARS)} on {device}?"
|
|
342
|
+
return Prepared(title, _Plan(attachment=attachment, destination=destination, app_name=app["name"], phone=phone))
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _approved(ctx, destination, apps):
|
|
346
|
+
"""Whether the person allowed ``destination``: through the run's guard (it asks the first time), else the
|
|
347
|
+
approvals store."""
|
|
348
|
+
from ..agent_hooks import safe_allows
|
|
349
|
+
guard = getattr(ctx, "guard", None)
|
|
350
|
+
if guard is not None:
|
|
351
|
+
return safe_allows(guard, destination)
|
|
352
|
+
try:
|
|
353
|
+
from ..permissions.exceptions import Exceptions, default_path
|
|
354
|
+
return apps.approved(destination, Exceptions(default_path()))
|
|
355
|
+
except Exception: # noqa: BLE001
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def perform(self, ctx, prepared, act, snapshot):
|
|
359
|
+
plan = prepared.state if isinstance(prepared.state, _Plan) else _Plan("PUT_FILE didn't run.")
|
|
360
|
+
if plan.refusal or plan.attachment is None:
|
|
361
|
+
return SkillResult(feedback=plan.refusal or "PUT_FILE didn't run.")
|
|
362
|
+
attachment = plan.attachment
|
|
363
|
+
size = attachment.data.get("bytes") or 0
|
|
364
|
+
if plan.destination == "clipboard":
|
|
365
|
+
from ..phone_io.clipboard import set_text
|
|
366
|
+
driver = ctx.driver
|
|
367
|
+
text = self.store.whole_text(attachment)
|
|
368
|
+
if text is None:
|
|
369
|
+
return SkillResult(feedback=f"“{attachment.name}” is no longer on this Mac.")
|
|
370
|
+
try:
|
|
371
|
+
set_text(lambda method, path, body, timeout: driver.call(method, path, body, timeout=timeout), text)
|
|
372
|
+
except Exception as error: # noqa: BLE001
|
|
373
|
+
self._event(attachment, size, "clipboard", "failed")
|
|
374
|
+
return SkillResult(feedback=f"PUT_FILE couldn't set the clipboard: {_sentence(error)}")
|
|
375
|
+
self._event(attachment, size, "clipboard", "copied")
|
|
376
|
+
return SkillResult(feedback=f"The text of “{attachment.name}” is on the clipboard now: paste it with a "
|
|
377
|
+
"long press in a field, then Paste.", changed=False)
|
|
378
|
+
original = self.store.path(attachment, "original")
|
|
379
|
+
if original is None:
|
|
380
|
+
return SkillResult(feedback=f"“{attachment.name}” is no longer on this Mac.")
|
|
381
|
+
if plan.destination == "photos":
|
|
382
|
+
from ..phone_io.photos import send
|
|
383
|
+
driver = ctx.driver
|
|
384
|
+
try:
|
|
385
|
+
send(lambda method, path, body, timeout: driver.call(method, path, body, timeout=timeout), original,
|
|
386
|
+
name=self._device_name(),
|
|
387
|
+
activate=lambda bundle: driver.call("POST", "/wda/apps/activate", {"bundleId": bundle},
|
|
388
|
+
timeout=15),
|
|
389
|
+
front=getattr(driver, "active_app", None))
|
|
390
|
+
except Exception as error: # noqa: BLE001
|
|
391
|
+
self._event(attachment, size, "photos", "failed")
|
|
392
|
+
return SkillResult(feedback=f"PUT_FILE couldn't add “{attachment.name}” to Photos: {_sentence(error)}")
|
|
393
|
+
self._event(attachment, size, "photos", "copied")
|
|
394
|
+
return SkillResult(feedback=f"Added “{attachment.name}” to Photos: it is the newest item in the photo "
|
|
395
|
+
"picker now.", changed=False)
|
|
396
|
+
try:
|
|
397
|
+
result = plan.phone.put(plan.destination, original, name=attachment.name)
|
|
398
|
+
except Exception as error: # noqa: BLE001
|
|
399
|
+
self._event(attachment, size, f"app:{plan.destination}", "failed")
|
|
400
|
+
return SkillResult(feedback=f"PUT_FILE couldn't copy “{attachment.name}”: {_sentence(error)}")
|
|
401
|
+
self._event(attachment, size, f"app:{plan.destination}", "copied")
|
|
402
|
+
renamed = f" (as “{result['name']}”, since that name was taken)" if result["name"] != attachment.name else ""
|
|
403
|
+
return SkillResult(feedback=f"Put “{attachment.name}” in {plan.app_name}{renamed}. In the Files app it is "
|
|
404
|
+
f"under On My iPhone › {plan.app_name}, and {plan.app_name} can open it.",
|
|
405
|
+
changed=False)
|
|
406
|
+
|
|
407
|
+
def _event(self, attachment, size, destination, status):
|
|
408
|
+
try:
|
|
409
|
+
self.ctx.emit({"event": "file_put", "name": attachment.name, "bytes": size, "destination": destination,
|
|
410
|
+
"status": status})
|
|
411
|
+
except Exception: # noqa: BLE001
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _sentence(error):
|
|
416
|
+
text = " ".join(str(error).split())[:240] or type(error).__name__
|
|
417
|
+
fix = getattr(error, "fix", "")
|
|
418
|
+
return (text if text.endswith((".", "?", "!")) else text + ".") + (f" {fix}" if fix else "")
|