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,521 @@
|
|
|
1
|
+
"""queues.json and each queue's journal: the posts a person lines up, which profile posts each, how each ended."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import errno
|
|
5
|
+
import fcntl
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
STORE = "queues.json"
|
|
17
|
+
LOCK = "queues.lock"
|
|
18
|
+
JOURNALS = "queue-journal"
|
|
19
|
+
LOCK_TIMEOUT = 10.0
|
|
20
|
+
MAX_QUEUES = 50
|
|
21
|
+
MAX_ITEMS = 200
|
|
22
|
+
MAX_MEDIA = 10
|
|
23
|
+
CAPTION_CHARS = 2200 # Instagram's caption limit; TikTok's is larger
|
|
24
|
+
HASHTAGS = 30
|
|
25
|
+
SOUND_CHARS = 120
|
|
26
|
+
NAME_CHARS = 60
|
|
27
|
+
LATE_LIMIT_MS = 2 * 3600 * 1000
|
|
28
|
+
# How often a post its profile's pace holds says so again: the proof that Mobster held it, not that it was away.
|
|
29
|
+
PACED_REFRESH_MS = 10 * 60 * 1000
|
|
30
|
+
STAGGER_S = 90
|
|
31
|
+
STAGGER_MAX_S = 3600
|
|
32
|
+
WATCH_SEEN = 2000
|
|
33
|
+
ID = re.compile(r"[a-f0-9]{12}")
|
|
34
|
+
HASHTAG = re.compile(r"#?([\w.]{1,100})", re.UNICODE)
|
|
35
|
+
STATES = ("draft", "ready", "sending", "posting", "posted", "declined", "failed", "unknown", "missed")
|
|
36
|
+
ADD_PHOTOS = "Add photos" # a draft's note: a caption and a profile, no media yet. It never goes out as it is.
|
|
37
|
+
APPROVALS = ("each", "batch")
|
|
38
|
+
FORMATS = ("auto", "post", "reel", "story", "video")
|
|
39
|
+
EDITABLE = frozenset({"ready", "declined", "failed", "missed"})
|
|
40
|
+
RUN_AGAIN = frozenset({"declined", "failed", "missed"}) # the states whose row has Run again
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class QueueError(ValueError):
|
|
44
|
+
def __init__(self, message, code="invalid", status=400):
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
self.code, self.status = code, status
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def now_ms():
|
|
50
|
+
return round(time.time() * 1000)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def armed_for(queue, item):
|
|
54
|
+
"""Whether ``item`` may go out now: its queue runs, for every profile or for the item's."""
|
|
55
|
+
scoped = queue.get("armedProfiles")
|
|
56
|
+
return bool(queue.get("armed")) and (scoped is None or item.get("profile") in scoped)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _text(value, name, limit):
|
|
60
|
+
if value is None:
|
|
61
|
+
return ""
|
|
62
|
+
if not isinstance(value, str) or len(value) > limit:
|
|
63
|
+
raise QueueError(f"{name} is text of at most {limit:,} characters.")
|
|
64
|
+
return value.strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def clean_hashtags(value):
|
|
68
|
+
"""["#spring", "#sale"] from a list or a line ("spring, #sale"): each starts with #, no duplicates, at most 30."""
|
|
69
|
+
if value is None or value == "":
|
|
70
|
+
return []
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
value = re.split(r"[\s,]+", value)
|
|
73
|
+
if not isinstance(value, list):
|
|
74
|
+
raise QueueError("hashtags is a list, or one line of them.")
|
|
75
|
+
tags = []
|
|
76
|
+
for raw in value:
|
|
77
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
78
|
+
continue
|
|
79
|
+
found = HASHTAG.fullmatch(raw.strip())
|
|
80
|
+
if not found:
|
|
81
|
+
raise QueueError(f"{raw[:40]!r} isn't a hashtag: letters, digits, underscores and dots.")
|
|
82
|
+
tag = "#" + found.group(1)
|
|
83
|
+
if tag.casefold() not in {t.casefold() for t in tags}:
|
|
84
|
+
tags.append(tag)
|
|
85
|
+
if len(tags) > HASHTAGS:
|
|
86
|
+
raise QueueError(f"A post takes at most {HASHTAGS} hashtags.")
|
|
87
|
+
return tags
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def clean_media(value):
|
|
91
|
+
"""[{path, name, bytes, sha256, kind}] of files on this Mac that Photos takes, read now."""
|
|
92
|
+
from ..phone_io import PhoneIOError
|
|
93
|
+
from ..phone_io.photos import digest, sniff
|
|
94
|
+
if not isinstance(value, list) or not 1 <= len(value) <= MAX_MEDIA:
|
|
95
|
+
raise QueueError(f"A post has 1 to {MAX_MEDIA} photos or videos.")
|
|
96
|
+
media = []
|
|
97
|
+
for entry in value:
|
|
98
|
+
path = entry.get("path") if isinstance(entry, dict) else entry
|
|
99
|
+
if not isinstance(path, str) or not path.strip() or len(path) > 1024:
|
|
100
|
+
raise QueueError("Each photo or video is a path on this Mac.")
|
|
101
|
+
file = Path(path).expanduser()
|
|
102
|
+
if not file.is_absolute() or not file.is_file():
|
|
103
|
+
raise QueueError(f"There is no file at {path}.", "media_missing")
|
|
104
|
+
try:
|
|
105
|
+
kind, _ = sniff(file)
|
|
106
|
+
except PhoneIOError as error:
|
|
107
|
+
raise QueueError(str(error), "not_media") from None
|
|
108
|
+
known = entry.get("sha256") if isinstance(entry, dict) else None
|
|
109
|
+
size = file.stat().st_size
|
|
110
|
+
same = isinstance(entry, dict) and entry.get("bytes") == size and isinstance(known, str)
|
|
111
|
+
media.append({"path": str(file), "name": file.name, "bytes": size, "kind": kind,
|
|
112
|
+
"sha256": known if same else digest(file)})
|
|
113
|
+
return media
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def fingerprint(item):
|
|
117
|
+
"""What a batch approval covers: the media's checksums, the words, the sound, the format and the profile."""
|
|
118
|
+
words = json.dumps([[m["sha256"] for m in item["media"]], item["caption"], item["hashtags"], item["sound"],
|
|
119
|
+
item["format"], item["profile"]], ensure_ascii=False)
|
|
120
|
+
return hashlib.sha256(words.encode()).hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def caption_text(item):
|
|
124
|
+
"""The caption as posted: the words, then the hashtags on their own line."""
|
|
125
|
+
tags = " ".join(item.get("hashtags") or ())
|
|
126
|
+
caption = (item.get("caption") or "").strip()
|
|
127
|
+
return f"{caption}\n\n{tags}".strip() if tags else caption
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class QueueStore:
|
|
131
|
+
"""queues.json and the journals in ``data_dir``."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, data_dir):
|
|
134
|
+
self.data_dir = Path(data_dir)
|
|
135
|
+
self.path = self.data_dir / STORE
|
|
136
|
+
self.lock_path = self.data_dir / LOCK
|
|
137
|
+
self.journals = self.data_dir / JOURNALS
|
|
138
|
+
self._thread_lock = threading.RLock()
|
|
139
|
+
|
|
140
|
+
def read(self):
|
|
141
|
+
try:
|
|
142
|
+
return self._load()
|
|
143
|
+
except QueueError:
|
|
144
|
+
return {"version": 1, "queues": []}
|
|
145
|
+
|
|
146
|
+
def _load(self):
|
|
147
|
+
"""The stored queues; a missing file is empty, and one it can't trust raises so no edit writes over it."""
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(self.path.read_text())
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
return {"version": 1, "queues": []}
|
|
152
|
+
except (OSError, ValueError) as error:
|
|
153
|
+
raise QueueError(f"Mobster can't read {STORE}: {error}. Fix or move the file, then try again.",
|
|
154
|
+
"store_unreadable", 503) from None
|
|
155
|
+
queues = data.get("queues") if isinstance(data, dict) else None
|
|
156
|
+
if not isinstance(queues, list):
|
|
157
|
+
raise QueueError(f"Mobster can't read {STORE}: it isn't a list of queues. Fix or move the file, then "
|
|
158
|
+
"try again.", "store_unreadable", 503)
|
|
159
|
+
return {"version": 1, "queues": [q for q in queues if isinstance(q, dict) and ID.fullmatch(
|
|
160
|
+
str(q.get("id") or ""))]}
|
|
161
|
+
|
|
162
|
+
def all(self):
|
|
163
|
+
return self.read()["queues"]
|
|
164
|
+
|
|
165
|
+
def get(self, queue_id):
|
|
166
|
+
found = next((q for q in self.all() if q["id"] == queue_id), None)
|
|
167
|
+
if found is None:
|
|
168
|
+
raise QueueError("That queue isn't here any more.", "queue_not_found", 404)
|
|
169
|
+
return found
|
|
170
|
+
|
|
171
|
+
@contextlib.contextmanager
|
|
172
|
+
def edit(self):
|
|
173
|
+
with self._thread_lock:
|
|
174
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
176
|
+
try:
|
|
177
|
+
end = time.monotonic() + LOCK_TIMEOUT
|
|
178
|
+
while True:
|
|
179
|
+
try:
|
|
180
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
181
|
+
break
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
if exc.errno not in (errno.EWOULDBLOCK, errno.EAGAIN) or time.monotonic() >= end:
|
|
184
|
+
raise OSError("Another Mobster process is holding the queue") from None
|
|
185
|
+
time.sleep(.05)
|
|
186
|
+
data = self._load()
|
|
187
|
+
before = json.dumps(data, sort_keys=True)
|
|
188
|
+
yield data
|
|
189
|
+
if json.dumps(data, sort_keys=True) != before:
|
|
190
|
+
self._write(data)
|
|
191
|
+
finally:
|
|
192
|
+
os.close(fd)
|
|
193
|
+
|
|
194
|
+
def _write(self, data):
|
|
195
|
+
fd, temporary = tempfile.mkstemp(prefix=".queues.", suffix=".json", dir=self.data_dir)
|
|
196
|
+
try:
|
|
197
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
198
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
199
|
+
stream.write(json.dumps(data, indent=1, ensure_ascii=False) + "\n")
|
|
200
|
+
os.replace(temporary, self.path)
|
|
201
|
+
except BaseException:
|
|
202
|
+
with contextlib.suppress(OSError):
|
|
203
|
+
os.unlink(temporary)
|
|
204
|
+
raise
|
|
205
|
+
|
|
206
|
+
def journal(self, queue_id, event, **fields):
|
|
207
|
+
"""Append one line to the queue's journal: what happened, when, to which item. Written before the step."""
|
|
208
|
+
self.journals.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
line = json.dumps({"at": now_ms(), "event": event, **fields}, ensure_ascii=False)
|
|
210
|
+
path = self.journals / f"{queue_id}.jsonl"
|
|
211
|
+
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_APPEND | os.O_NOFOLLOW, 0o600)
|
|
212
|
+
try:
|
|
213
|
+
os.write(fd, (line + "\n").encode())
|
|
214
|
+
os.fsync(fd)
|
|
215
|
+
finally:
|
|
216
|
+
os.close(fd)
|
|
217
|
+
|
|
218
|
+
def history(self, queue_id, limit=500):
|
|
219
|
+
path = self.journals / f"{queue_id}.jsonl"
|
|
220
|
+
try:
|
|
221
|
+
lines = path.read_text().splitlines()[-limit:]
|
|
222
|
+
except OSError:
|
|
223
|
+
return []
|
|
224
|
+
out = []
|
|
225
|
+
for line in lines:
|
|
226
|
+
with contextlib.suppress(ValueError):
|
|
227
|
+
out.append(json.loads(line))
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
def create(self, name=None, approval="each"):
|
|
231
|
+
if approval not in APPROVALS:
|
|
232
|
+
raise QueueError("approval is each or batch.")
|
|
233
|
+
with self.edit() as data:
|
|
234
|
+
if len(data["queues"]) >= MAX_QUEUES:
|
|
235
|
+
raise QueueError(f"Mobster keeps at most {MAX_QUEUES} queues. Delete one first.", "too_many")
|
|
236
|
+
stamp = now_ms()
|
|
237
|
+
queue = {"id": uuid.uuid4().hex[:12], "name": _text(name, "name", NAME_CHARS) or
|
|
238
|
+
f"Queue {len(data['queues']) + 1}", "approval": approval, "armed": False, "batch": None,
|
|
239
|
+
"createdAt": stamp, "updatedAt": stamp, "items": [], "watch": None, "staggerSeconds": STAGGER_S}
|
|
240
|
+
data["queues"].append(queue)
|
|
241
|
+
self.journal(queue["id"], "created", approval=approval)
|
|
242
|
+
return queue
|
|
243
|
+
|
|
244
|
+
def update_queue(self, queue_id, changes, profiles=None):
|
|
245
|
+
allowed = {"name", "approval", "watch", "staggerSeconds"}
|
|
246
|
+
if set(changes) - allowed:
|
|
247
|
+
raise QueueError("A queue's name, approval, watched folder and stagger can change.")
|
|
248
|
+
with self.edit() as data:
|
|
249
|
+
queue = self._find(data, queue_id)
|
|
250
|
+
if "name" in changes:
|
|
251
|
+
queue["name"] = _text(changes["name"], "name", NAME_CHARS) or queue["name"]
|
|
252
|
+
if "approval" in changes:
|
|
253
|
+
if changes["approval"] not in APPROVALS:
|
|
254
|
+
raise QueueError("approval is each or batch.")
|
|
255
|
+
if changes["approval"] != queue["approval"]:
|
|
256
|
+
queue["approval"], queue["batch"] = changes["approval"], None
|
|
257
|
+
if "staggerSeconds" in changes:
|
|
258
|
+
value = changes["staggerSeconds"]
|
|
259
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= STAGGER_MAX_S:
|
|
260
|
+
raise QueueError(f"staggerSeconds is 0 to {STAGGER_MAX_S}.")
|
|
261
|
+
queue["staggerSeconds"] = int(value)
|
|
262
|
+
if "watch" in changes:
|
|
263
|
+
queue["watch"] = self._clean_watch(changes["watch"], queue, profiles)
|
|
264
|
+
queue["updatedAt"] = now_ms()
|
|
265
|
+
return queue
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _clean_watch(value, queue, profiles):
|
|
269
|
+
if value is None:
|
|
270
|
+
return None
|
|
271
|
+
if not isinstance(value, dict) or set(value) - {"folder", "profile"}:
|
|
272
|
+
raise QueueError("watch is {folder, profile}, or null.")
|
|
273
|
+
folder = value.get("folder")
|
|
274
|
+
if not isinstance(folder, str) or not Path(folder).expanduser().is_dir():
|
|
275
|
+
raise QueueError("Choose a folder that exists on this Mac.", "folder_missing")
|
|
276
|
+
profile = value.get("profile")
|
|
277
|
+
if profile is not None and (not isinstance(profile, str) or (profiles is not None and profile not in {
|
|
278
|
+
p["id"] for p in profiles})):
|
|
279
|
+
raise QueueError("No profile has that id. Add it in Settings › Profiles.", "profile_not_found")
|
|
280
|
+
folder = str(Path(folder).expanduser().resolve())
|
|
281
|
+
before = queue.get("watch") or {}
|
|
282
|
+
# What the folder holds now is already there: only files that arrive from now on join the queue.
|
|
283
|
+
seen = before.get("seen") if before.get("folder") == folder else None
|
|
284
|
+
if seen is None:
|
|
285
|
+
seen = sorted(str(p) for p in Path(folder).iterdir() if p.is_file())[-WATCH_SEEN:]
|
|
286
|
+
return {"folder": folder, "profile": profile, "seen": seen}
|
|
287
|
+
|
|
288
|
+
def note_seen(self, queue_id, paths):
|
|
289
|
+
with self.edit() as data:
|
|
290
|
+
queue = self._find(data, queue_id)
|
|
291
|
+
watch = queue.get("watch")
|
|
292
|
+
if watch:
|
|
293
|
+
watch["seen"] = (list(watch.get("seen") or ()) + list(paths))[-WATCH_SEEN:]
|
|
294
|
+
|
|
295
|
+
def delete_queue(self, queue_id):
|
|
296
|
+
with self.edit() as data:
|
|
297
|
+
queue = self._find(data, queue_id)
|
|
298
|
+
if any(i["state"] in ("sending", "posting") for i in queue["items"]):
|
|
299
|
+
raise QueueError("A post in this queue is going out now. Wait for it to end.", "busy", 409)
|
|
300
|
+
data["queues"] = [q for q in data["queues"] if q["id"] != queue_id]
|
|
301
|
+
self.journal(queue_id, "deleted")
|
|
302
|
+
|
|
303
|
+
def arm(self, queue_id, armed, profile=None):
|
|
304
|
+
"""Start or stop the queue; with ``profile``, only that profile's posts (a profile's Queue tab). armedProfiles
|
|
305
|
+
is None while the whole queue runs, else the profiles whose posts go out."""
|
|
306
|
+
with self.edit() as data:
|
|
307
|
+
queue = self._find(data, queue_id)
|
|
308
|
+
scoped = queue.get("armedProfiles")
|
|
309
|
+
if profile is None:
|
|
310
|
+
running, scoped = bool(armed), None
|
|
311
|
+
elif armed:
|
|
312
|
+
running = True
|
|
313
|
+
scoped = None if queue.get("armed") and scoped is None else sorted({*(scoped or ()), profile})
|
|
314
|
+
else:
|
|
315
|
+
if queue.get("armed") and scoped is None:
|
|
316
|
+
scoped = sorted({i["profile"] for i in queue["items"] if i.get("profile")} - {profile})
|
|
317
|
+
else:
|
|
318
|
+
scoped = [p for p in scoped or () if p != profile]
|
|
319
|
+
running = bool(queue.get("armed")) and bool(scoped)
|
|
320
|
+
scoped = scoped if running else None
|
|
321
|
+
queue["armed"], queue["armedProfiles"] = running, scoped
|
|
322
|
+
queue["updatedAt"] = now_ms()
|
|
323
|
+
self.journal(queue_id, "started" if armed else "stopped", **({"profile": profile} if profile else {}))
|
|
324
|
+
return queue
|
|
325
|
+
|
|
326
|
+
def approve_batch(self, queue_id, item_ids):
|
|
327
|
+
"""The person's one approval for these items' Posts, as they are now. Items edited later ask again."""
|
|
328
|
+
with self.edit() as data:
|
|
329
|
+
queue = self._find(data, queue_id)
|
|
330
|
+
if queue["approval"] != "batch":
|
|
331
|
+
raise QueueError("This queue asks before each post. Choose Approve the batch first.", "not_batch",
|
|
332
|
+
409)
|
|
333
|
+
items = {i["id"]: i for i in queue["items"]}
|
|
334
|
+
if not item_ids or any(i not in items for i in item_ids):
|
|
335
|
+
raise QueueError("Approve posts that are in this queue.")
|
|
336
|
+
covered = {i: fingerprint(items[i]) for i in item_ids if items[i]["state"] in EDITABLE}
|
|
337
|
+
if not covered:
|
|
338
|
+
raise QueueError("None of those posts is waiting to go out.", "nothing_to_approve", 409)
|
|
339
|
+
queue["batch"] = {"at": now_ms(), "items": covered}
|
|
340
|
+
self.journal(queue_id, "batch_approved", items=sorted(covered))
|
|
341
|
+
return queue
|
|
342
|
+
|
|
343
|
+
def clean_item(self, body, current=None, profiles=None):
|
|
344
|
+
"""A new or edited item from ``body``. ``profiles``: the stored profiles, to check ``profile``."""
|
|
345
|
+
known = {"media", "caption", "hashtags", "sound", "format", "profile", "scheduledAt", "draft"}
|
|
346
|
+
if set(body) - known:
|
|
347
|
+
raise QueueError("Unsupported field: " + ", ".join(sorted(set(body) - known)))
|
|
348
|
+
item = dict(current) if current else {"id": uuid.uuid4().hex[:12], "media": [], "caption": "", "hashtags": [],
|
|
349
|
+
"sound": "", "format": "auto", "profile": None, "scheduledAt": None,
|
|
350
|
+
"state": "ready", "runId": None, "transfer": None, "result": None,
|
|
351
|
+
"note": None}
|
|
352
|
+
if "draft" in body and (type(body["draft"]) is not bool or current is not None):
|
|
353
|
+
raise QueueError("draft is true on a new post that has no photos or videos yet.")
|
|
354
|
+
draft = body.get("draft") is True or (current is not None and current["state"] == "draft")
|
|
355
|
+
if draft and not body.get("media"):
|
|
356
|
+
item["media"] = [] if current is None else item["media"]
|
|
357
|
+
elif "media" in body or current is None:
|
|
358
|
+
item["media"] = clean_media(body.get("media"))
|
|
359
|
+
if draft:
|
|
360
|
+
# Photos turn a draft into a normal ready post; the approvals for sending apply to it from then on.
|
|
361
|
+
item.update(state="ready" if item["media"] else "draft", note=None if item["media"] else ADD_PHOTOS)
|
|
362
|
+
if "caption" in body:
|
|
363
|
+
item["caption"] = _text(body["caption"], "caption", CAPTION_CHARS)
|
|
364
|
+
if "hashtags" in body:
|
|
365
|
+
item["hashtags"] = clean_hashtags(body["hashtags"])
|
|
366
|
+
if "sound" in body:
|
|
367
|
+
item["sound"] = _text(body["sound"], "sound", SOUND_CHARS)
|
|
368
|
+
if "format" in body:
|
|
369
|
+
if body["format"] not in FORMATS:
|
|
370
|
+
raise QueueError("format is auto, post, reel, story or video.")
|
|
371
|
+
item["format"] = body["format"]
|
|
372
|
+
if "profile" in body:
|
|
373
|
+
profile = body["profile"]
|
|
374
|
+
if profile is not None and (not isinstance(profile, str) or not ID.fullmatch(profile)):
|
|
375
|
+
raise QueueError("profile is a profile's id from Settings › Profiles.")
|
|
376
|
+
if profile is not None and profiles is not None and profile not in {p["id"] for p in profiles}:
|
|
377
|
+
raise QueueError("No profile has that id. Add it in Settings › Profiles.", "profile_not_found")
|
|
378
|
+
item["profile"] = profile
|
|
379
|
+
if "scheduledAt" in body:
|
|
380
|
+
when = body["scheduledAt"]
|
|
381
|
+
if when is not None and (isinstance(when, bool) or not isinstance(when, (int, float)) or when <= 0):
|
|
382
|
+
raise QueueError("scheduledAt is a time in milliseconds since 1970, or null for as soon as it starts.")
|
|
383
|
+
item["scheduledAt"] = int(when) if when is not None else None
|
|
384
|
+
if len(caption_text(item)) > CAPTION_CHARS:
|
|
385
|
+
raise QueueError(f"The caption and its hashtags come to more than {CAPTION_CHARS:,} characters.")
|
|
386
|
+
item["updatedAt"] = now_ms()
|
|
387
|
+
return item
|
|
388
|
+
|
|
389
|
+
def add_items(self, queue_id, bodies, profiles=None):
|
|
390
|
+
items = [self.clean_item(body, profiles=profiles) for body in bodies]
|
|
391
|
+
with self.edit() as data:
|
|
392
|
+
queue = self._find(data, queue_id)
|
|
393
|
+
if len(queue["items"]) + len(items) > MAX_ITEMS:
|
|
394
|
+
raise QueueError(f"A queue holds at most {MAX_ITEMS} posts.", "too_many")
|
|
395
|
+
queue["items"].extend(items)
|
|
396
|
+
queue["updatedAt"] = now_ms()
|
|
397
|
+
for item in items:
|
|
398
|
+
self.journal(queue_id, "added", item=item["id"], media=[m["sha256"] for m in item["media"]])
|
|
399
|
+
return items
|
|
400
|
+
|
|
401
|
+
def add_draft(self, queue_id, caption, profile, profiles=None):
|
|
402
|
+
"""A draft in ``queue_id`` for ``profile``: the words, no media yet. Nothing sends it as it is."""
|
|
403
|
+
return self.add_items(queue_id, [{"draft": True, "caption": caption, "profile": profile}], profiles)[0]
|
|
404
|
+
|
|
405
|
+
def edit_item(self, queue_id, item_id, body, profiles=None):
|
|
406
|
+
with self.edit() as data:
|
|
407
|
+
queue = self._find(data, queue_id)
|
|
408
|
+
index, current = self._item(queue, item_id)
|
|
409
|
+
if current["state"] not in EDITABLE and current["state"] != "draft":
|
|
410
|
+
raise QueueError("This post can't change now: it is going out, went out, or may have.", "locked",
|
|
411
|
+
409)
|
|
412
|
+
item = self.clean_item(body, current, profiles)
|
|
413
|
+
if current["state"] not in ("ready", "draft"):
|
|
414
|
+
item.update(state="ready", note=None)
|
|
415
|
+
queue["items"][index] = item
|
|
416
|
+
self.journal(queue_id, "edited", item=item_id)
|
|
417
|
+
return item
|
|
418
|
+
|
|
419
|
+
def remove_item(self, queue_id, item_id):
|
|
420
|
+
with self.edit() as data:
|
|
421
|
+
queue = self._find(data, queue_id)
|
|
422
|
+
_, item = self._item(queue, item_id)
|
|
423
|
+
if item["state"] in ("sending", "posting"):
|
|
424
|
+
raise QueueError("This post is going out now.", "busy", 409)
|
|
425
|
+
queue["items"] = [i for i in queue["items"] if i["id"] != item_id]
|
|
426
|
+
self.journal(queue_id, "removed", item=item_id)
|
|
427
|
+
|
|
428
|
+
def move_item(self, queue_id, item_id, index):
|
|
429
|
+
with self.edit() as data:
|
|
430
|
+
queue = self._find(data, queue_id)
|
|
431
|
+
at, item = self._item(queue, item_id)
|
|
432
|
+
queue["items"].pop(at)
|
|
433
|
+
queue["items"].insert(max(0, min(int(index), len(queue["items"]))), item)
|
|
434
|
+
|
|
435
|
+
def set_state(self, queue_id, item_id, state, **fields):
|
|
436
|
+
"""Journal, then store, an item's new state (and its runId, transfer, result, note, pacedAt or scheduledAt)."""
|
|
437
|
+
if state not in STATES:
|
|
438
|
+
raise QueueError("Unknown state")
|
|
439
|
+
self.journal(queue_id, state, item=item_id, **{k: v for k, v in fields.items() if k in ("runId", "note")})
|
|
440
|
+
with self.edit() as data:
|
|
441
|
+
queue = self._find(data, queue_id)
|
|
442
|
+
_, item = self._item(queue, item_id)
|
|
443
|
+
item["state"] = state
|
|
444
|
+
for key in ("runId", "transfer", "result", "note", "pacedAt", "scheduledAt"):
|
|
445
|
+
if key in fields:
|
|
446
|
+
item[key] = fields[key]
|
|
447
|
+
item["updatedAt"] = now_ms()
|
|
448
|
+
return dict(item)
|
|
449
|
+
|
|
450
|
+
def change_item(self, queue_id, item_id, change):
|
|
451
|
+
"""``change(item)`` edits the item under the lock and returns the event to journal, or None for no change."""
|
|
452
|
+
with self.edit() as data:
|
|
453
|
+
_, item = self._item(self._find(data, queue_id), item_id)
|
|
454
|
+
event = change(item)
|
|
455
|
+
if event is not None:
|
|
456
|
+
self.journal(queue_id, event, item=item_id)
|
|
457
|
+
return dict(item)
|
|
458
|
+
|
|
459
|
+
def mark(self, queue_id, item_id, outcome):
|
|
460
|
+
"""The person looked: an ``unknown`` post went out (``posted``) or didn't (``ready``, to run again).
|
|
461
|
+
``ready`` on a missed, declined or failed post is **Run again**: the press is the person's OK to post it now, so
|
|
462
|
+
its passed time goes, and it posts once its queue runs and its pace allows, never missed for that old time."""
|
|
463
|
+
if outcome not in ("posted", "ready"):
|
|
464
|
+
raise QueueError("Mark it posted, or ready to run again.")
|
|
465
|
+
with self.edit() as data:
|
|
466
|
+
queue = self._find(data, queue_id)
|
|
467
|
+
_, item = self._item(queue, item_id)
|
|
468
|
+
if item["state"] not in ("unknown", "missed", "declined", "failed") and not (
|
|
469
|
+
outcome == "posted" and item["state"] == "posted"):
|
|
470
|
+
raise QueueError("Only a post that ended can be marked.", "locked", 409)
|
|
471
|
+
if outcome == "posted":
|
|
472
|
+
return self.set_state(queue_id, item_id, outcome, note="You said it went out.",
|
|
473
|
+
result={**(item.get("result") or {}), "markedAt": now_ms()})
|
|
474
|
+
again = {"scheduledAt": None} if item["state"] in RUN_AGAIN else {}
|
|
475
|
+
return self.set_state(queue_id, item_id, outcome, note=None, **again)
|
|
476
|
+
|
|
477
|
+
def recover(self):
|
|
478
|
+
"""After a stop mid-post: ``posting`` becomes ``unknown`` (it may have posted), ``sending`` ``failed``."""
|
|
479
|
+
changed = []
|
|
480
|
+
for queue in self.all():
|
|
481
|
+
for item in queue["items"]:
|
|
482
|
+
if item["state"] == "posting":
|
|
483
|
+
changed.append((queue["id"], item["id"], "unknown", "Mobster stopped while this post was going "
|
|
484
|
+
"out. Look at the profile, then mark it."))
|
|
485
|
+
elif item["state"] == "sending":
|
|
486
|
+
changed.append((queue["id"], item["id"], "failed", "Mobster stopped while it sent the media. "
|
|
487
|
+
"Nothing was posted."))
|
|
488
|
+
for queue_id, item_id, state, note in changed:
|
|
489
|
+
self.set_state(queue_id, item_id, state, note=note)
|
|
490
|
+
return len(changed)
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def _find(data, queue_id):
|
|
494
|
+
queue = next((q for q in data["queues"] if q["id"] == queue_id), None)
|
|
495
|
+
if queue is None:
|
|
496
|
+
raise QueueError("That queue isn't here any more.", "queue_not_found", 404)
|
|
497
|
+
return queue
|
|
498
|
+
|
|
499
|
+
@staticmethod
|
|
500
|
+
def _item(queue, item_id):
|
|
501
|
+
for index, item in enumerate(queue["items"]):
|
|
502
|
+
if item["id"] == item_id:
|
|
503
|
+
return index, item
|
|
504
|
+
raise QueueError("That post isn't in this queue any more.", "item_not_found", 404)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
_stores = {}
|
|
508
|
+
_stores_lock = threading.Lock()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def store_for(runtime):
|
|
512
|
+
"""The QueueStore beside ``runtime``'s journal, else in its data folder (one per folder)."""
|
|
513
|
+
from ..paths import user_data_dir
|
|
514
|
+
config = getattr(runtime, "config", None)
|
|
515
|
+
state_db = getattr(config, "state_db", None)
|
|
516
|
+
folder = Path(state_db).parent if state_db else Path(getattr(config, "data_dir", None) or user_data_dir())
|
|
517
|
+
with _stores_lock:
|
|
518
|
+
store = _stores.get(folder)
|
|
519
|
+
if store is None:
|
|
520
|
+
store = _stores[folder] = QueueStore(folder)
|
|
521
|
+
return store
|