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,739 @@
|
|
|
1
|
+
"""MCP tools that hand a task to Mobster's agent in a conversation (provider ``threads``, seam S10)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from .. import engine_health
|
|
11
|
+
from .client import Client, NoService, ServiceError
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("mobster.threads")
|
|
14
|
+
|
|
15
|
+
NO_SERVICE = "Open Mobster or run `mobster serve` so your agent can hand tasks to Mobster's agent."
|
|
16
|
+
WAIT_MAX = 45
|
|
17
|
+
HEADROOM = 7.0 # of the 44 s call budget: the HTTP round trips and the answer
|
|
18
|
+
ID = r"^[a-f0-9]{12}$"
|
|
19
|
+
DONE = frozenset({"completed"})
|
|
20
|
+
BLOCKS_INLINE = 12000 # characters of result blocks a task's answer carries as they are
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def accepted_unavailable(ToolResult, *, text, **handles):
|
|
24
|
+
"""A POST succeeded; losing its subsequent status read must not invite another POST."""
|
|
25
|
+
return ToolResult(text, structured={**handles, "status": "status_unavailable"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def known_refusal(error):
|
|
29
|
+
"""Whether a failed start POST certainly started nothing: Mobster refused it with a 4xx other than a timeout (408)
|
|
30
|
+
or its own "outcome unknown", or said so with ``"started": false`` (api_errors.nothing_started), as it does for a
|
|
31
|
+
503 such as the iPhone helper restarting. No reply, any other server fault (5xx) or a malformed answer leaves the
|
|
32
|
+
outcome unknown."""
|
|
33
|
+
if not isinstance(error, ServiceError) or error.code == "request_outcome_unknown":
|
|
34
|
+
return False
|
|
35
|
+
if error.data.get("started") is False: # only the literal false: a missing or odd value stays unknown
|
|
36
|
+
return True
|
|
37
|
+
# A workflow's 409 "queued" (workflows/runs.py: the run starts when the one going ends) would count as a refusal
|
|
38
|
+
# here, but only a scheduled key gets it: run_workflow and workflow_* send a fresh key on every call.
|
|
39
|
+
return 400 <= error.status < 500 and error.status != 408
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def well_formed(answer):
|
|
43
|
+
"""Whether a start's 2xx answer has the shapes the tools read: an object whose run, item, group and batch are absent
|
|
44
|
+
or objects, with string ids where a tool reads one. Anything else names no task the tool could follow."""
|
|
45
|
+
if not isinstance(answer, dict):
|
|
46
|
+
return False
|
|
47
|
+
run, item, group, batch = (answer.get(key) for key in ("run", "item", "group", "batch"))
|
|
48
|
+
if any(part and not isinstance(part, dict) for part in (run, item, group, batch)):
|
|
49
|
+
return False
|
|
50
|
+
needed = [part.get("id") for part in (group, batch if batch and batch.get("graph") == 2 else None) if part]
|
|
51
|
+
optional = [(run or {}).get("id"), (item or {}).get("runId")]
|
|
52
|
+
return (all(isinstance(value, str) and value for value in needed)
|
|
53
|
+
and all(value is None or isinstance(value, str) for value in optional))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def log_fault(what, error):
|
|
57
|
+
"""Log a fault a tool answered past: its type and stack, never its message, which can carry the phone's text."""
|
|
58
|
+
stack = "".join(traceback.format_tb(error.__traceback__, limit=8))
|
|
59
|
+
log.warning("%s (%s)\n%s", what, type(error).__name__, stack.rstrip())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def mask_handles(mask, name, arguments, result):
|
|
63
|
+
"""Keep only canonical continuation handles usable when a remembered code overlaps their bytes."""
|
|
64
|
+
state = result.structured if isinstance(result.structured, dict) and not result.is_error else {}
|
|
65
|
+
keys = {"thread_id", "run_id", "group_id"} if name in ("phone_task", "phone_task_status") else (
|
|
66
|
+
{"workflow_id", "thread_id", "run_id", "batch_id"} if name == "run_workflow" or name == "graph_status"
|
|
67
|
+
or name.startswith(TOOL_PREFIX) else set())
|
|
68
|
+
selectors = ({"thread_id"} if name == "phone_task" else
|
|
69
|
+
{"thread_id", "run_id", "group_id"} if name == "phone_task_status" else
|
|
70
|
+
{"thread_id"} if name == "run_workflow" else
|
|
71
|
+
{"workflow_id", "batch_id"} if name == "graph_status" else set())
|
|
72
|
+
handles = {key: value for key in keys if isinstance(value := state.get(key), str)
|
|
73
|
+
and re.fullmatch(ID, value) and (key not in selectors or arguments.get(key) is None
|
|
74
|
+
or arguments[key] == value)}
|
|
75
|
+
if name == "run_workflow" and "workflow_id" in handles and re.fullmatch(ID, str(arguments.get("workflow") or "")):
|
|
76
|
+
if handles["workflow_id"] != arguments["workflow"]:
|
|
77
|
+
handles.pop("workflow_id")
|
|
78
|
+
uri = state.get("status_uri")
|
|
79
|
+
expected = ({f"/api/groups/{handles['group_id']}"} if "group_id" in handles else set()) | (
|
|
80
|
+
{f"/api/runs/{handles['run_id']}"} if "run_id" in handles else set()) | (
|
|
81
|
+
{f"/api/workflows/{handles['workflow_id']}/runs"} if "workflow_id" in handles
|
|
82
|
+
and "batch_id" in handles else set())
|
|
83
|
+
masked = mask(result)
|
|
84
|
+
if isinstance(masked.structured, dict):
|
|
85
|
+
masked.structured.update(handles)
|
|
86
|
+
if isinstance(uri, str) and uri in expected:
|
|
87
|
+
masked.structured["status_uri"] = uri
|
|
88
|
+
return masked
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def schema_task():
|
|
92
|
+
return {"type": "object", "properties": {
|
|
93
|
+
"message": {"type": "string", "minLength": 1, "maxLength": 4000,
|
|
94
|
+
"description": "what Mobster's agent should do on the iPhone, in plain words; in a "
|
|
95
|
+
"conversation, a follow-up such as \"now reply to her\" (it sees what the earlier "
|
|
96
|
+
"tasks found)"},
|
|
97
|
+
"thread_id": {"type": "string", "pattern": ID,
|
|
98
|
+
"description": "continue this conversation (from an earlier phone_task); leave it out to "
|
|
99
|
+
"start a new one. While its task runs, the message steers it, or answers "
|
|
100
|
+
"Mobster's question"},
|
|
101
|
+
"device": {"type": "string", "maxLength": 128,
|
|
102
|
+
"description": "the iPhone or simulator by id, UDID or name (default: the conversation's, else "
|
|
103
|
+
"the user's default device)"},
|
|
104
|
+
"profile": {"type": "string", "minLength": 1, "maxLength": 160,
|
|
105
|
+
"description": "act as this profile (from list_profiles, such as "
|
|
106
|
+
"\"@getmobster on Instagram\"): the "
|
|
107
|
+
"task runs on its iPhone, and Mobster confirms the profile on screen before it "
|
|
108
|
+
"posts or sends"},
|
|
109
|
+
"account": {"type": "string", "minLength": 1, "maxLength": 160,
|
|
110
|
+
"description": "deprecated: use profile, which takes the same value"},
|
|
111
|
+
"phones": {"type": "array", "minItems": 2, "maxItems": 100,
|
|
112
|
+
"items": {"type": "string", "minLength": 1, "maxLength": 128},
|
|
113
|
+
"description": "run the same task on each of these devices at once (ids, UDIDs or names from "
|
|
114
|
+
"list_devices), as one task: each phone's answer, then one summary that merges "
|
|
115
|
+
"them. Leave device out"},
|
|
116
|
+
"profiles": {"description": "run the same task as each of these profiles at once, each on its own iPhone, as "
|
|
117
|
+
"one task: \"all\", or a list of profiles (from list_profiles). Mobster "
|
|
118
|
+
"confirms each profile on screen before it posts or sends; a profile that "
|
|
119
|
+
"fails doesn't stop the others. Leave device, phones and profile out",
|
|
120
|
+
"oneOf": [{"const": "all"},
|
|
121
|
+
{"type": "array", "minItems": 1, "maxItems": 100,
|
|
122
|
+
"items": {"type": "string", "minLength": 1, "maxLength": 160}}]},
|
|
123
|
+
"queue": {"type": "boolean",
|
|
124
|
+
"description": "true (the default): when the phone is in use, wait in line "
|
|
125
|
+
"and start when it is free; false: fail at once instead. "
|
|
126
|
+
"The status says who holds the phone and your number"},
|
|
127
|
+
"wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
|
|
128
|
+
"description": "how long to wait for the task before returning (default 30); call "
|
|
129
|
+
"phone_task_status for the rest"}},
|
|
130
|
+
"required": ["message"], "additionalProperties": False}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def schema_status():
|
|
134
|
+
return {"type": "object", "properties": {
|
|
135
|
+
"thread_id": {"type": "string", "pattern": ID, "description": "the conversation's latest task"},
|
|
136
|
+
"run_id": {"type": "string", "pattern": ID, "description": "one task"},
|
|
137
|
+
"group_id": {"type": "string", "pattern": ID, "description": "a task run on several phones (from phone_task "
|
|
138
|
+
"with phones)"},
|
|
139
|
+
"wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
|
|
140
|
+
"description": "wait up to this long for it to finish or need the user (default 0)"}},
|
|
141
|
+
"required": [], "additionalProperties": False}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def describe(run):
|
|
145
|
+
"""{status, answer?, waiting?} for a run: running, waiting_for_approval, waiting_for_answer, waiting_for_phone, or
|
|
146
|
+
how it ended."""
|
|
147
|
+
approval = run.get("approval")
|
|
148
|
+
if run.get("finishedAt") is None:
|
|
149
|
+
line = run.get("queue")
|
|
150
|
+
if isinstance(line, dict) and not approval:
|
|
151
|
+
return {"status": "queued", "position": line.get("position"), "behind": line.get("behind"),
|
|
152
|
+
"queue_message": line.get("message"), "expires_at": line.get("expiresAt")}
|
|
153
|
+
if approval and approval.get("kind") == "clarify":
|
|
154
|
+
return {"status": "waiting_for_answer", "question": approval.get("question") or approval.get("label"),
|
|
155
|
+
"choices": [c.get("label") for c in approval.get("choices") or () if isinstance(c, dict)]}
|
|
156
|
+
if approval:
|
|
157
|
+
return {"status": "waiting_for_approval", "approval": approval.get("title") or approval.get("label"),
|
|
158
|
+
"reason": approval.get("reason"), "explain": approval.get("explain"),
|
|
159
|
+
"expires_at": approval.get("expiresAt")}
|
|
160
|
+
wait = run.get("phoneWait")
|
|
161
|
+
# What the task waits on, in the words every surface uses (states.ROWS).
|
|
162
|
+
found = engine_health.task_wait(wait.get("kind"), run.get("deviceName")) if isinstance(wait, dict) else None
|
|
163
|
+
if found:
|
|
164
|
+
need, line = found
|
|
165
|
+
since, seconds = wait.get("since"), wait.get("seconds")
|
|
166
|
+
return {"status": "waiting_for_phone", "need": need, "message": line,
|
|
167
|
+
"expires_at": since + seconds * 1000 if isinstance(since, (int, float)) and isinstance(seconds, int)
|
|
168
|
+
else None}
|
|
169
|
+
return {"status": "running"}
|
|
170
|
+
summary = run.get("summary") or {}
|
|
171
|
+
out = {"status": run.get("status") or summary.get("status") or "error"}
|
|
172
|
+
data = summary.get("data")
|
|
173
|
+
answer = data if isinstance(data, str) and data.strip() else summary.get("answer")
|
|
174
|
+
if answer:
|
|
175
|
+
out["answer"] = str(answer)[:3000]
|
|
176
|
+
elif summary.get("reason") and out["status"] not in DONE:
|
|
177
|
+
out["reason"] = str(summary["reason"])[:500]
|
|
178
|
+
if isinstance(data, (dict, list)):
|
|
179
|
+
out["data"] = data
|
|
180
|
+
blocks = summary.get("blocks")
|
|
181
|
+
if isinstance(blocks, dict) and blocks.get("blocks"):
|
|
182
|
+
# The answer's result blocks (docs/results.mdx): as they are when small, else as Markdown, so a client's
|
|
183
|
+
# context isn't flooded by a 200-row table.
|
|
184
|
+
if len(json.dumps(blocks, ensure_ascii=False)) <= BLOCKS_INLINE:
|
|
185
|
+
out["blocks"] = blocks
|
|
186
|
+
else:
|
|
187
|
+
from ..result_blocks import to_markdown
|
|
188
|
+
out["blocks_markdown"] = to_markdown(blocks)[:BLOCKS_INLINE]
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def sentence(state, thread_id, answerable=True, origin="mcp"):
|
|
193
|
+
"""The tool's text."""
|
|
194
|
+
status = state["status"]
|
|
195
|
+
remote = origin == "remote"
|
|
196
|
+
where = "in Mobster on their Mac" if remote else "in the Mobster app"
|
|
197
|
+
if status == "queued":
|
|
198
|
+
return (f"{state.get('queue_message') or 'Waiting for the phone.'} Nothing has been done on the phone yet. It "
|
|
199
|
+
f"starts when the phone is free; call phone_task_status with thread_id {thread_id} to see when.")
|
|
200
|
+
if status == "running":
|
|
201
|
+
return ("Mobster's agent is still working on it. Call phone_task_status with thread_id "
|
|
202
|
+
f"{thread_id} to see how it ends.")
|
|
203
|
+
if status == "waiting_for_approval":
|
|
204
|
+
if remote:
|
|
205
|
+
label = str(state.get("approval") or "an action").strip()
|
|
206
|
+
end = "" if label.endswith((".", "?", "!")) else "."
|
|
207
|
+
return (f"It's waiting for the user to approve this in Mobster on their Mac: {label}{end} You can't "
|
|
208
|
+
"approve it. Tell the user, then call phone_task_status.")
|
|
209
|
+
left = ""
|
|
210
|
+
if isinstance(state.get("expires_at"), (int, float)):
|
|
211
|
+
minutes = max(0, int((state["expires_at"] / 1000 - time.time()) // 60))
|
|
212
|
+
left = f" It waits about {minutes} more minutes, then ends without doing it."
|
|
213
|
+
why = f" ({state['reason']})" if state.get("reason") else ""
|
|
214
|
+
said = f" {state['explain']}" if state.get("explain") else ""
|
|
215
|
+
return (f"It's waiting for the user's OK in the Mobster app, which notified them: "
|
|
216
|
+
f"{state.get('approval') or 'an action'}{why}.{said} Nothing happens until they answer.{left} "
|
|
217
|
+
"Tell the user; then call phone_task_status with wait_seconds to keep waiting.")
|
|
218
|
+
if status == "waiting_for_phone":
|
|
219
|
+
left = ""
|
|
220
|
+
if isinstance(state.get("expires_at"), (int, float)):
|
|
221
|
+
seconds = max(0, int(state["expires_at"] / 1000 - time.time()))
|
|
222
|
+
left = f" It waits about {seconds} more seconds, then stops."
|
|
223
|
+
return (f"It's waiting for the user on the iPhone itself. {state.get('message') or ''} Nothing is done on the "
|
|
224
|
+
f"phone meanwhile.{left} Tell the user now; then call phone_task_status with thread_id {thread_id} and "
|
|
225
|
+
"wait_seconds to keep waiting.")
|
|
226
|
+
if status == "approval_timeout":
|
|
227
|
+
return "Nobody answered the approval in the Mobster app in time, so Mobster didn't do it."
|
|
228
|
+
if status == "waiting_for_answer":
|
|
229
|
+
choices = state.get("choices") or []
|
|
230
|
+
asks = f"Mobster's agent asks: {state.get('question')}" + (f" (choices: {', '.join(choices)})" if choices
|
|
231
|
+
else "")
|
|
232
|
+
if not answerable:
|
|
233
|
+
return asks + f". The user answers this {where}; then call phone_task_status."
|
|
234
|
+
return asks + f". Answer with phone_task, thread_id {thread_id}, or let the user answer {where}."
|
|
235
|
+
if status in DONE:
|
|
236
|
+
return "Done." + (f" {state['answer']}" if state.get("answer") else "")
|
|
237
|
+
return f"It ended {status.replace('_', ' ')}." + (f" {state.get('answer') or state.get('reason') or ''}".rstrip())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ThreadsProvider:
|
|
241
|
+
"""mcp_server.registry.ToolProvider for conversations."""
|
|
242
|
+
|
|
243
|
+
name = "threads"
|
|
244
|
+
|
|
245
|
+
def __init__(self, connect=None, *, origin="mcp", wait_max=None, extra_headers=None, default_wait=30):
|
|
246
|
+
"""``origin``: "mcp" (`mobster mcp`), or "remote" for Mobster Remote's tools, which word approvals for an
|
|
247
|
+
assistant away from the Mac."""
|
|
248
|
+
self.origin = origin
|
|
249
|
+
self.wait_cap = WAIT_MAX - HEADROOM if wait_max is None else float(wait_max)
|
|
250
|
+
self.default_wait = default_wait
|
|
251
|
+
headers = dict(extra_headers or {})
|
|
252
|
+
self._connect = connect or (lambda: self._connect_default(headers))
|
|
253
|
+
|
|
254
|
+
def _connect_default(self, headers):
|
|
255
|
+
client = Client.connect(origin=self.origin)
|
|
256
|
+
client.extra_headers.update(headers)
|
|
257
|
+
return client
|
|
258
|
+
|
|
259
|
+
def definitions(self, toolset):
|
|
260
|
+
return [
|
|
261
|
+
{"name": "phone_task",
|
|
262
|
+
"description": "Hand a task to Mobster's agent on the user's iPhone. It asks the user in the Mobster app "
|
|
263
|
+
"before it sends, buys, posts or deletes. Pass thread_id for a follow-up that sees what "
|
|
264
|
+
"earlier tasks found.",
|
|
265
|
+
"inputSchema": schema_task()},
|
|
266
|
+
{"name": "phone_task_status",
|
|
267
|
+
"description": "Where a phone_task task is: running, waiting for the user's approval or answer, or "
|
|
268
|
+
"finished with its answer.",
|
|
269
|
+
"inputSchema": schema_status()}]
|
|
270
|
+
|
|
271
|
+
def annotations(self):
|
|
272
|
+
from ..mcp_server.tools import ACTS, READ_ONLY
|
|
273
|
+
return {"phone_task": ACTS, "phone_task_status": READ_ONLY}
|
|
274
|
+
|
|
275
|
+
def instructions(self):
|
|
276
|
+
return ("phone_task hands a whole task to Mobster's agent in the user's Mobster app, in a conversation that "
|
|
277
|
+
"remembers; the user approves sends, buys, posts and deletes there.")
|
|
278
|
+
|
|
279
|
+
def client(self):
|
|
280
|
+
from ..mcp_server.tools import ToolError
|
|
281
|
+
try:
|
|
282
|
+
return self._connect()
|
|
283
|
+
except (NoService, ServiceError):
|
|
284
|
+
raise ToolError(NO_SERVICE) from None
|
|
285
|
+
|
|
286
|
+
def call(self, name, arguments, call, toolset):
|
|
287
|
+
from ..mcp_server.protocol import ToolResult
|
|
288
|
+
from ..mcp_server.tools import ToolError
|
|
289
|
+
client = self.client()
|
|
290
|
+
if not client.has_threads:
|
|
291
|
+
raise ToolError("This Mobster doesn't have conversations yet. Ask the user to update Mobster.")
|
|
292
|
+
if self.origin == "mcp":
|
|
293
|
+
# The holder label on the phone ("Claude Code is using this phone"): the client's own name for itself.
|
|
294
|
+
from ..ownership import clean_client
|
|
295
|
+
label = clean_client(toolset.client_name()) if callable(getattr(toolset, "client_name", None)) else None
|
|
296
|
+
if label:
|
|
297
|
+
client.extra_headers["X-Mobster-Client"] = label
|
|
298
|
+
try:
|
|
299
|
+
if name == "phone_task":
|
|
300
|
+
return self.task(client, arguments, toolset, ToolResult, call=call)
|
|
301
|
+
return self.status(client, arguments, ToolResult)
|
|
302
|
+
except ServiceError as error:
|
|
303
|
+
if error.code == "approval_pending":
|
|
304
|
+
raise ToolError(self.approval_pending()) from None
|
|
305
|
+
if error.code == "steer_not_allowed":
|
|
306
|
+
raise ToolError("That task was started somewhere else, so it takes messages only there. Start a "
|
|
307
|
+
"new conversation without thread_id.") from None
|
|
308
|
+
raise ToolError(str(error)) from None
|
|
309
|
+
except NoService:
|
|
310
|
+
raise ToolError(NO_SERVICE) from None
|
|
311
|
+
|
|
312
|
+
def approval_pending(self):
|
|
313
|
+
if self.origin == "remote":
|
|
314
|
+
return ("That conversation's task is waiting for the user's approval in Mobster on their Mac. You can't "
|
|
315
|
+
"approve it. Tell the user, then call phone_task_status.")
|
|
316
|
+
return ("That conversation's task is waiting for the user's approval in the Mobster app. Tell the user, then "
|
|
317
|
+
"call phone_task_status.")
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def allowed(toolset, *names):
|
|
321
|
+
allow = getattr(toolset, "allow_devices", None)
|
|
322
|
+
if allow is None:
|
|
323
|
+
return True
|
|
324
|
+
return any(isinstance(n, str) and n.strip().casefold() in allow for n in names)
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def profile(arguments):
|
|
328
|
+
"""The profile a task acts as: ``profile``, or ``account``, its deprecated name. ToolError when they differ."""
|
|
329
|
+
from ..mcp_server.tools import ToolError
|
|
330
|
+
profile, account = arguments.get("profile"), arguments.get("account")
|
|
331
|
+
if profile and account and profile.strip().casefold() != account.strip().casefold():
|
|
332
|
+
raise ToolError("Name the profile once, with profile. account is its old name.")
|
|
333
|
+
return profile or account
|
|
334
|
+
|
|
335
|
+
def poll_seconds(self, arguments, call, toolset, client):
|
|
336
|
+
"""Leave one HTTP timeout for the last status read before the MCP call budget ends."""
|
|
337
|
+
wait = min(float(arguments.get("wait_seconds", self.default_wait)), self.wait_cap)
|
|
338
|
+
started = getattr(call, "started", None)
|
|
339
|
+
budget = getattr(toolset, "call_budget", None)
|
|
340
|
+
if isinstance(started, (int, float)) and isinstance(budget, (int, float)):
|
|
341
|
+
reserve = max(0.0, float(getattr(client, "timeout", 10.0))) + 2.0
|
|
342
|
+
remaining = budget - (time.monotonic() - started) - reserve
|
|
343
|
+
if remaining <= 0:
|
|
344
|
+
return None
|
|
345
|
+
wait = min(wait, remaining)
|
|
346
|
+
return wait
|
|
347
|
+
|
|
348
|
+
def in_time(self, tool, kind, arguments, call, toolset, client):
|
|
349
|
+
"""Refuse before the start POST when the call's budget can't fit it and one status read: nothing is sent."""
|
|
350
|
+
from ..mcp_server.tools import ToolError
|
|
351
|
+
if self.poll_seconds(arguments, call, toolset, client) is None:
|
|
352
|
+
raise ToolError(f"{tool} ran out of time before it sent the {kind}, so nothing started. Call {tool} again.")
|
|
353
|
+
|
|
354
|
+
def submit(self, post, kind):
|
|
355
|
+
"""(thread id, answer) of ``post()``, the one POST that starts a ``kind``. Any failure but Mobster's own refusal
|
|
356
|
+
leaves the outcome unknown, and the error says so instead of inviting another POST."""
|
|
357
|
+
from ..mcp_server.tools import ToolError
|
|
358
|
+
try:
|
|
359
|
+
thread_id, answer = post()
|
|
360
|
+
if not well_formed(answer):
|
|
361
|
+
raise ValueError("the answer doesn't have the shape a start answers with")
|
|
362
|
+
return thread_id, answer
|
|
363
|
+
except Exception as error: # noqa: BLE001 -- no reply, a 5xx or a malformed answer: the outcome is unknown
|
|
364
|
+
if known_refusal(error):
|
|
365
|
+
raise
|
|
366
|
+
if not isinstance(error, (NoService, ServiceError)):
|
|
367
|
+
log_fault(f"a {kind}'s start request failed, so its outcome is unknown", error)
|
|
368
|
+
check = "Ask the user to check Mobster on their Mac" if self.origin == "remote" else "Check the Mobster app"
|
|
369
|
+
raise ToolError(f"Mobster didn't confirm this {kind}, so whether it started is unknown. {check} before you "
|
|
370
|
+
"start it again; don't start another to find out.") from None
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def follow(wait, read, unavailable):
|
|
374
|
+
"""``read(wait)``, the accepted task's answer; ``unavailable``, its handles with status_unavailable, when the
|
|
375
|
+
call has no time left or the read fails in any way. A lost status read never invites another POST."""
|
|
376
|
+
if wait is None:
|
|
377
|
+
return unavailable
|
|
378
|
+
try:
|
|
379
|
+
return read(wait)
|
|
380
|
+
except Exception as error: # noqa: BLE001 -- the task was accepted: any failed read keeps its handle
|
|
381
|
+
if not isinstance(error, (NoService, ServiceError)):
|
|
382
|
+
log_fault("a status read after an accepted task failed", error)
|
|
383
|
+
return unavailable
|
|
384
|
+
|
|
385
|
+
def task(self, client, arguments, toolset, ToolResult, *, call=None):
|
|
386
|
+
from ..mcp_server.tools import ToolError
|
|
387
|
+
thread_id, device, phones = arguments.get("thread_id"), arguments.get("device"), arguments.get("phones")
|
|
388
|
+
if device and phones:
|
|
389
|
+
raise ToolError("Name one phone with device, or several with phones, not both.")
|
|
390
|
+
profile = self.profile(arguments)
|
|
391
|
+
profiles = arguments.get("profiles")
|
|
392
|
+
if profile and phones:
|
|
393
|
+
raise ToolError("A profile runs on its own iPhone: leave phones out.")
|
|
394
|
+
if profiles is not None and (profile or phones or device):
|
|
395
|
+
raise ToolError("Run on several profiles with profiles alone: each runs on its own iPhone.")
|
|
396
|
+
thread = client.thread(thread_id)["thread"] if thread_id else None
|
|
397
|
+
if getattr(toolset, "allow_devices", None) is not None:
|
|
398
|
+
if profiles is not None:
|
|
399
|
+
raise ToolError("This server may use only the devices given with --allow-device: run profiles one "
|
|
400
|
+
"at a time, naming each profile's phone with device.")
|
|
401
|
+
if profile and not device:
|
|
402
|
+
# The profile's phone isn't known here: naming it lets the allowlist check it, and the task is
|
|
403
|
+
# refused if it isn't the profile's phone (accounts/routing.py).
|
|
404
|
+
raise ToolError("This server may use only the devices given with --allow-device: name the "
|
|
405
|
+
"profile's phone with device too.")
|
|
406
|
+
if phones:
|
|
407
|
+
if not all(self.allowed(toolset, name) for name in phones):
|
|
408
|
+
raise ToolError("This server may use only the devices given with --allow-device: name only "
|
|
409
|
+
"those in phones.")
|
|
410
|
+
else:
|
|
411
|
+
names = (device,) if device else ((thread or {}).get("device"), (thread or {}).get("deviceName"))
|
|
412
|
+
if not self.allowed(toolset, *names):
|
|
413
|
+
raise ToolError("This server may use only the devices given with --allow-device: name one of "
|
|
414
|
+
"them with device.")
|
|
415
|
+
body = {"text": arguments["message"]}
|
|
416
|
+
if device:
|
|
417
|
+
body["device"] = device
|
|
418
|
+
if phones:
|
|
419
|
+
body["phones"] = list(phones)
|
|
420
|
+
if profile:
|
|
421
|
+
body["account"] = profile
|
|
422
|
+
if profiles is not None:
|
|
423
|
+
body["profiles"] = profiles if profiles == "all" else list(profiles)
|
|
424
|
+
if arguments.get("queue") is False:
|
|
425
|
+
body["queue"] = False
|
|
426
|
+
self.in_time("phone_task", "task", arguments, call, toolset, client)
|
|
427
|
+
key = uuid.uuid4().hex # with a key, Mobster answers "outcome unknown" for a start it can't confirm
|
|
428
|
+
thread_id, answer = self.submit(lambda: client.send(thread_id, body, key=key), "task")
|
|
429
|
+
wait = self.poll_seconds(arguments, call, toolset, client)
|
|
430
|
+
if answer.get("group"):
|
|
431
|
+
group_id = answer["group"]["id"]
|
|
432
|
+
unavailable = accepted_unavailable(
|
|
433
|
+
ToolResult, text=f"Mobster accepted the task, but its status is unavailable. Call "
|
|
434
|
+
f"phone_task_status with group_id {group_id}; do not start it again.",
|
|
435
|
+
thread_id=thread_id, group_id=group_id, status_uri=f"/api/groups/{group_id}")
|
|
436
|
+
return self.follow(wait, lambda seconds: self.group_result(client.wait_group(group_id, seconds),
|
|
437
|
+
thread_id, ToolResult), unavailable)
|
|
438
|
+
run = answer.get("run") or {}
|
|
439
|
+
routed = answer.get("routed") or ("new_run" if run else None)
|
|
440
|
+
run_id = run.get("id") or (answer.get("item") or {}).get("runId")
|
|
441
|
+
if routed in ("steer", "answer"):
|
|
442
|
+
text = ("Mobster's agent got your message while it works on the task." if routed == "steer"
|
|
443
|
+
else "Mobster's agent has your answer and goes on.")
|
|
444
|
+
return ToolResult(text, structured={"thread_id": thread_id, "run_id": run_id, "status": "running",
|
|
445
|
+
"routed": routed})
|
|
446
|
+
if not run_id:
|
|
447
|
+
state = {"status": "running"}
|
|
448
|
+
return ToolResult(sentence(state, thread_id, origin=self.origin),
|
|
449
|
+
structured={"thread_id": thread_id, "run_id": None, **state})
|
|
450
|
+
|
|
451
|
+
def read(seconds):
|
|
452
|
+
state = describe(client.wait_run(run_id, seconds, through_approval=True))
|
|
453
|
+
return ToolResult(sentence(state, thread_id, origin=self.origin),
|
|
454
|
+
structured={"thread_id": thread_id, "run_id": run_id, **state})
|
|
455
|
+
return self.follow(wait, read, accepted_unavailable(
|
|
456
|
+
ToolResult, text=f"Mobster accepted the task, but its status is unavailable. Call "
|
|
457
|
+
f"phone_task_status with run_id {run_id}; do not start it again.",
|
|
458
|
+
thread_id=thread_id, run_id=run_id, status_uri=f"/api/runs/{run_id}"))
|
|
459
|
+
|
|
460
|
+
def group_result(self, group, thread_id, ToolResult):
|
|
461
|
+
"""phone_task's answer for a task on several phones: each phone's state and answer, and when every phone is
|
|
462
|
+
done, the merged summary (the lines several phones found, once)."""
|
|
463
|
+
phones = []
|
|
464
|
+
summary = group.get("summary") or {}
|
|
465
|
+
answers = {item.get("profile") or item.get("device"): item for item in summary.get("answers") or ()}
|
|
466
|
+
for member in group.get("members") or ():
|
|
467
|
+
approval = member.get("approval") or {}
|
|
468
|
+
if member.get("state") == "refused":
|
|
469
|
+
state = {"status": "not_started", "reason": member.get("error")}
|
|
470
|
+
elif member.get("state") == "held":
|
|
471
|
+
state = {"status": "waiting_for_mac"}
|
|
472
|
+
elif approval:
|
|
473
|
+
state = {"status": "waiting_for_answer" if approval.get("kind") == "clarify" else
|
|
474
|
+
"waiting_for_approval", "approval": approval.get("title") or approval.get("label")}
|
|
475
|
+
elif member.get("finishedAt") is None:
|
|
476
|
+
state = {"status": "running"}
|
|
477
|
+
else:
|
|
478
|
+
found = answers.get(member.get("profile") or member.get("device")) or {}
|
|
479
|
+
state = {"status": member.get("status") or found.get("status") or "error",
|
|
480
|
+
**({"answer": found["answer"]} if found.get("answer") else {})}
|
|
481
|
+
phones.append({"device": member.get("device"), "name": member.get("deviceName"),
|
|
482
|
+
**({"profile": member.get("profileName") or member["profile"]}
|
|
483
|
+
if member.get("profile") else {}),
|
|
484
|
+
"run_id": member.get("runId"), **state})
|
|
485
|
+
counts = group.get("counts") or {}
|
|
486
|
+
total = counts.get("phones") or len(phones)
|
|
487
|
+
if group.get("state") == "finished":
|
|
488
|
+
lines = [line["text"] + (f" ({len(line['phones'])} phones)" if len(line.get("phones") or ()) > 1 else "")
|
|
489
|
+
for line in summary.get("lines") or ()][:40]
|
|
490
|
+
text = (f"Done on {summary.get('completed', 0)} of {total} phones." +
|
|
491
|
+
(" " + " ".join(lines) if lines else ""))
|
|
492
|
+
elif group.get("state") == "waiting":
|
|
493
|
+
text = (f"{counts.get('waiting', 0)} of {total} phones wait for the user in the Mobster app. Tell the "
|
|
494
|
+
"user; "
|
|
495
|
+
f"then call phone_task_status with group_id {group.get('id')}.")
|
|
496
|
+
else:
|
|
497
|
+
text = (f"Mobster's agent is working on {total} phones. Call phone_task_status with group_id "
|
|
498
|
+
f"{group.get('id')} to see how it ends.")
|
|
499
|
+
structured = {"thread_id": thread_id, "group_id": group.get("id"), "status": group.get("state"),
|
|
500
|
+
"phones": phones}
|
|
501
|
+
if group.get("state") == "finished":
|
|
502
|
+
structured["summary"] = {"lines": summary.get("lines") or [], "rows": summary.get("rows"),
|
|
503
|
+
"completed": summary.get("completed"), "costUsd": summary.get("costUsd")}
|
|
504
|
+
return ToolResult(text, structured=structured)
|
|
505
|
+
|
|
506
|
+
def status(self, client, arguments, ToolResult):
|
|
507
|
+
from ..mcp_server.tools import ToolError
|
|
508
|
+
run_id, thread_id, group_id = arguments.get("run_id"), arguments.get("thread_id"), arguments.get("group_id")
|
|
509
|
+
if not run_id and not thread_id and not group_id:
|
|
510
|
+
raise ToolError("Give the thread_id, run_id or group_id that phone_task returned.")
|
|
511
|
+
if group_id:
|
|
512
|
+
group = client.wait_group(group_id, min(float(arguments.get("wait_seconds", 0)), self.wait_cap))
|
|
513
|
+
return self.group_result(group, thread_id or group.get("threadId"), ToolResult)
|
|
514
|
+
if not run_id:
|
|
515
|
+
thread = client.thread(thread_id)["thread"]
|
|
516
|
+
if thread.get("lastGroupId") and thread.get("group"):
|
|
517
|
+
group = client.wait_group(thread["lastGroupId"],
|
|
518
|
+
min(float(arguments.get("wait_seconds", 0)), self.wait_cap))
|
|
519
|
+
return self.group_result(group, thread_id, ToolResult)
|
|
520
|
+
run_id = thread.get("lastRunId")
|
|
521
|
+
if not run_id:
|
|
522
|
+
return ToolResult("No task has started in this conversation yet.",
|
|
523
|
+
structured={"thread_id": thread_id, "run_id": None, "status": "idle"})
|
|
524
|
+
run = client.wait_run(run_id, min(float(arguments.get("wait_seconds", 0)), self.wait_cap),
|
|
525
|
+
through_approval=True)
|
|
526
|
+
thread_id = thread_id or ((run.get("extras") or {}).get("threadId"))
|
|
527
|
+
if not re.fullmatch(r"[a-f0-9]{12}", str(thread_id or "")):
|
|
528
|
+
thread_id = None
|
|
529
|
+
state = describe(run)
|
|
530
|
+
return ToolResult(sentence(state, thread_id or "(none)", answerable=run.get("origin") in (None, self.origin),
|
|
531
|
+
origin=self.origin),
|
|
532
|
+
structured={"thread_id": thread_id, "run_id": run_id, **state})
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
TOOL_PREFIX = "workflow_" # a workflow made a tool (workflows.tools names it)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def graph_state(batch, limit=60):
|
|
539
|
+
"""A graph run as a tool answers: its status, its result and each step's state, reason and output preview."""
|
|
540
|
+
nodes = sorted((batch.get("nodes") or {}).values(), key=lambda node: node.get("seq", 0))
|
|
541
|
+
steps = [{key: node[key] for key in ("instance", "type", "state", "reason", "output", "runId") if key in node}
|
|
542
|
+
for node in nodes if not str(node.get("type")).startswith("trigger.")][:limit]
|
|
543
|
+
return {"batch_id": batch.get("id"), "status": batch.get("status"), "reason": batch.get("reason"),
|
|
544
|
+
"result": batch.get("result"), "steps": steps}
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def schema_workflows():
|
|
548
|
+
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def schema_run_workflow():
|
|
552
|
+
return {"type": "object", "properties": {
|
|
553
|
+
"workflow": {"type": "string", "minLength": 1, "maxLength": 120,
|
|
554
|
+
"description": "the workflow's name or id, from list_workflows"},
|
|
555
|
+
"values": {"type": "object", "additionalProperties": {"type": "string", "maxLength": 300},
|
|
556
|
+
"description": "a value for each blank, by name, like {\"topic\": \"basketball\"}; a blank left "
|
|
557
|
+
"out takes its default"},
|
|
558
|
+
"thread_id": {"type": "string", "pattern": ID,
|
|
559
|
+
"description": "run it in this conversation (from phone_task); leave it out for a new "
|
|
560
|
+
"conversation, or the workflow's own one when it always continues one"},
|
|
561
|
+
"new_conversation": {"type": "boolean",
|
|
562
|
+
"description": "start a new conversation even when the workflow always continues one"},
|
|
563
|
+
"wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
|
|
564
|
+
"description": "how long to wait for the task before returning (default 30); call "
|
|
565
|
+
"phone_task_status for the rest"}},
|
|
566
|
+
"required": ["workflow"], "additionalProperties": False}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def schema_graph_status():
|
|
570
|
+
return {"type": "object", "properties": {
|
|
571
|
+
"workflow_id": {"type": "string", "pattern": ID, "description": "the saved workflow's id from run_workflow"},
|
|
572
|
+
"batch_id": {"type": "string", "pattern": ID, "description": "the graph batch's id from run_workflow"},
|
|
573
|
+
"wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
|
|
574
|
+
"description": "wait up to this long for the graph to finish (default 0)"}},
|
|
575
|
+
"required": ["workflow_id", "batch_id"], "additionalProperties": False}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def shape(workflow):
|
|
579
|
+
"""{"nodes", "branches", "loops"}: how many steps its graph has, and how many branch or loop."""
|
|
580
|
+
nodes = [n for n in ((workflow.get("graph") or {}).get("nodes") or [])
|
|
581
|
+
if not str(n.get("type")).startswith("trigger.")]
|
|
582
|
+
return {"nodes": len(nodes), "branches": sum(1 for n in nodes if n.get("type") == "branch"),
|
|
583
|
+
"loops": sum(1 for n in nodes if n.get("type") == "loop")}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def describe_workflow(workflow):
|
|
587
|
+
"""One line: its name, id, steps, its graph's shape and its blanks."""
|
|
588
|
+
blanks = workflow.get("blanks") or []
|
|
589
|
+
words = ", ".join(f"{b['name']}" + (f" (default {b['default']})" if b.get("default") else " (no default)")
|
|
590
|
+
for b in blanks)
|
|
591
|
+
steps = " ".join(str(workflow.get("goal") or "").split())
|
|
592
|
+
line = f"“{workflow.get('name')}” ({workflow.get('id')}): {steps[:300]}"
|
|
593
|
+
counts = shape(workflow)
|
|
594
|
+
if counts["nodes"] > 1:
|
|
595
|
+
singular = {"branches": "branch", "loops": "loop"}
|
|
596
|
+
parts = [f"{counts['nodes']} steps"] + [f"{counts[key]} {key if counts[key] > 1 else singular[key]}"
|
|
597
|
+
for key in ("branches", "loops") if counts[key]]
|
|
598
|
+
line += " Graph: " + ", ".join(parts) + "."
|
|
599
|
+
if words:
|
|
600
|
+
line += f" Blanks: {words}."
|
|
601
|
+
if workflow.get("sameConversation"):
|
|
602
|
+
line += " It always continues one conversation."
|
|
603
|
+
return line
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class WorkflowsProvider(ThreadsProvider):
|
|
607
|
+
"""mcp_server.registry.ToolProvider for saved workflows: list them, and run one with its blanks filled, in a new
|
|
608
|
+
conversation or one you name."""
|
|
609
|
+
|
|
610
|
+
name = "workflows"
|
|
611
|
+
|
|
612
|
+
def definitions(self, toolset):
|
|
613
|
+
return self.static_definitions() + [tool for tool, _workflow in self.workflow_tools()]
|
|
614
|
+
|
|
615
|
+
def workflow_tools(self):
|
|
616
|
+
"""[(definition, tool)] for each published workflow the person made a tool of (``asTool``), as Mobster defines
|
|
617
|
+
it (GET /api/workflows/tools; the same tool is its REST route), or none when Mobster isn't reachable: a tool
|
|
618
|
+
list never fails for them."""
|
|
619
|
+
try:
|
|
620
|
+
listed = self.client().workflow_tools()
|
|
621
|
+
except Exception: # noqa: BLE001 -- no Mobster to ask, or one too old to say: the fixed tools only
|
|
622
|
+
return []
|
|
623
|
+
return [({"name": tool["name"], "description": tool["description"], "inputSchema": tool["inputSchema"]}, tool)
|
|
624
|
+
for tool in listed if str(tool.get("name") or "").startswith(TOOL_PREFIX)]
|
|
625
|
+
|
|
626
|
+
def static_definitions(self):
|
|
627
|
+
return [
|
|
628
|
+
{"name": "list_workflows",
|
|
629
|
+
"description": "The user's saved workflows in Mobster: each one's name, steps and blanks.",
|
|
630
|
+
"inputSchema": schema_workflows()},
|
|
631
|
+
{"name": "run_workflow",
|
|
632
|
+
"description": "Run one of the user's saved workflows on their iPhone, with a value for each blank, in "
|
|
633
|
+
"a new conversation or the one you name with thread_id. Mobster asks the user before it "
|
|
634
|
+
"sends, buys, posts or deletes.",
|
|
635
|
+
"inputSchema": schema_run_workflow()},
|
|
636
|
+
{"name": "graph_status",
|
|
637
|
+
"description": "Read a graph workflow's accepted batch by workflow_id and batch_id, without "
|
|
638
|
+
"starting it again.",
|
|
639
|
+
"inputSchema": schema_graph_status()}]
|
|
640
|
+
|
|
641
|
+
def annotations(self):
|
|
642
|
+
from ..mcp_server.tools import ACTS, READ_ONLY
|
|
643
|
+
return {"list_workflows": READ_ONLY, "run_workflow": ACTS, "graph_status": READ_ONLY,
|
|
644
|
+
**{tool["name"]: ACTS for tool, _workflow in self.workflow_tools()}}
|
|
645
|
+
|
|
646
|
+
def instructions(self):
|
|
647
|
+
return "run_workflow runs one of the user's saved workflows (list_workflows) with its blanks filled in."
|
|
648
|
+
|
|
649
|
+
def call(self, name, arguments, call, toolset):
|
|
650
|
+
from ..mcp_server.protocol import ToolResult
|
|
651
|
+
from ..mcp_server.tools import ToolError
|
|
652
|
+
client = self.client()
|
|
653
|
+
try:
|
|
654
|
+
if name == "list_workflows":
|
|
655
|
+
listed = client.workflows()
|
|
656
|
+
if not listed:
|
|
657
|
+
return ToolResult("The user has no saved workflows yet.", structured={"workflows": []})
|
|
658
|
+
return ToolResult("\n".join(describe_workflow(w) for w in listed), structured={"workflows": [
|
|
659
|
+
{"id": w.get("id"), "name": w.get("name"), "steps": w.get("goal"), "blanks": w.get("blanks") or [],
|
|
660
|
+
"sameConversation": bool(w.get("sameConversation")), **shape(w),
|
|
661
|
+
"published": (w.get("published") or {}).get("revision")} for w in listed]})
|
|
662
|
+
if name == "graph_status":
|
|
663
|
+
workflow_id, batch_id = arguments["workflow_id"], arguments["batch_id"]
|
|
664
|
+
wait = self.poll_seconds({"wait_seconds": arguments.get("wait_seconds", 0)}, call, toolset, client)
|
|
665
|
+
if wait is None:
|
|
666
|
+
return ToolResult("There is no time to read this graph in the current call. Call graph_status "
|
|
667
|
+
"again with the same workflow_id and batch_id; do not start the workflow again.",
|
|
668
|
+
structured={"workflow_id": workflow_id, "batch_id": batch_id,
|
|
669
|
+
"status": "status_unavailable",
|
|
670
|
+
"status_uri": f"/api/workflows/{workflow_id}/runs"})
|
|
671
|
+
batch = client.wait_batch(workflow_id, batch_id, wait)
|
|
672
|
+
if batch is None:
|
|
673
|
+
raise ToolError("Mobster cannot find that graph batch in recent history. Inspect the Mobster "
|
|
674
|
+
"app; do not restart it to check.")
|
|
675
|
+
state = graph_state(batch)
|
|
676
|
+
return ToolResult(f"Graph batch {batch_id} is {str(state['status']).replace('_', ' ')}.",
|
|
677
|
+
structured={"workflow_id": workflow_id, **state,
|
|
678
|
+
"status_uri": f"/api/workflows/{workflow_id}/runs"})
|
|
679
|
+
if name.startswith(TOOL_PREFIX):
|
|
680
|
+
found = next((tool for definition, tool in self.workflow_tools() if definition["name"] == name), None)
|
|
681
|
+
if found is None:
|
|
682
|
+
raise ToolError(f"No workflow is the tool {name} any more. Call list_workflows.")
|
|
683
|
+
values = {k: v for k, v in arguments.items() if k != "wait_seconds"}
|
|
684
|
+
arguments = {"workflow": found["workflowId"], "values": values, **(
|
|
685
|
+
{"wait_seconds": arguments["wait_seconds"]} if "wait_seconds" in arguments else {})}
|
|
686
|
+
if getattr(toolset, "allow_devices", None) is not None:
|
|
687
|
+
raise ToolError("This server may use only the devices given with --allow-device. Saved workflows "
|
|
688
|
+
"can route to other phones, so this server cannot start them. Run the workflow in "
|
|
689
|
+
"the Mobster app.")
|
|
690
|
+
try:
|
|
691
|
+
workflow = client.find_workflow(arguments["workflow"])
|
|
692
|
+
except LookupError as error:
|
|
693
|
+
raise ToolError(str(error)) from None
|
|
694
|
+
thread_id = arguments.get("thread_id")
|
|
695
|
+
self.in_time(name, "workflow run", arguments, call, toolset, client)
|
|
696
|
+
thread_id, answer = self.submit(lambda: client.run_workflow(
|
|
697
|
+
workflow["id"], values=arguments.get("values"), thread_id=thread_id,
|
|
698
|
+
new_conversation=bool(arguments.get("new_conversation"))), "workflow run")
|
|
699
|
+
run_id = (answer.get("run") or {}).get("id")
|
|
700
|
+
wait = self.poll_seconds(arguments, call, toolset, client)
|
|
701
|
+
batch = answer.get("batch") or {}
|
|
702
|
+
if batch.get("graph") == 2:
|
|
703
|
+
# A graph run: follow it to its end, then say how each step ended.
|
|
704
|
+
def graph(seconds):
|
|
705
|
+
state = graph_state(client.wait_batch(workflow["id"], batch["id"], seconds) or batch)
|
|
706
|
+
done = sum(1 for step in state["steps"] if step.get("state") == "done")
|
|
707
|
+
reason = f" {state['reason']}" if state.get("reason") else ""
|
|
708
|
+
waits = " Approvals wait for the user in the Mobster app." if state["status"] == "running" else ""
|
|
709
|
+
return ToolResult(f"“{workflow['name']}” {str(state['status']).replace('_', ' ')}: {done} of "
|
|
710
|
+
f"{len(state['steps'])} steps done.{reason}{waits}", structured={
|
|
711
|
+
"workflow_id": workflow["id"], "thread_id": thread_id, "run_id": run_id,
|
|
712
|
+
"status_uri": f"/api/workflows/{workflow['id']}/runs", **state})
|
|
713
|
+
return self.follow(wait, graph, accepted_unavailable(
|
|
714
|
+
ToolResult, text=f"Mobster accepted the graph workflow, but its status is unavailable. "
|
|
715
|
+
f"Call graph_status with workflow_id {workflow['id']} and batch_id {batch['id']}; "
|
|
716
|
+
"do not run it again.", workflow_id=workflow["id"], thread_id=thread_id, run_id=run_id,
|
|
717
|
+
batch_id=batch["id"], status_uri=f"/api/workflows/{workflow['id']}/runs"))
|
|
718
|
+
if not run_id:
|
|
719
|
+
state = {"status": "running"}
|
|
720
|
+
return ToolResult(f"Started “{workflow['name']}”. " + sentence(state, thread_id or "(none)",
|
|
721
|
+
origin=self.origin),
|
|
722
|
+
structured={"workflow_id": workflow["id"], "thread_id": thread_id,
|
|
723
|
+
"run_id": None, **state})
|
|
724
|
+
|
|
725
|
+
def task(seconds):
|
|
726
|
+
state = describe(client.wait_run(run_id, seconds))
|
|
727
|
+
return ToolResult(f"Started “{workflow['name']}”. " + sentence(state, thread_id
|
|
728
|
+
or "(none)", origin=self.origin),
|
|
729
|
+
structured={"workflow_id": workflow["id"], "thread_id": thread_id, "run_id": run_id,
|
|
730
|
+
**state})
|
|
731
|
+
return self.follow(wait, task, accepted_unavailable(
|
|
732
|
+
ToolResult, text=f"Mobster accepted the workflow, but its status is unavailable. Call "
|
|
733
|
+
f"phone_task_status with run_id {run_id}; do not run it again.",
|
|
734
|
+
workflow_id=workflow["id"], thread_id=thread_id, run_id=run_id,
|
|
735
|
+
status_uri=f"/api/runs/{run_id}"))
|
|
736
|
+
except ServiceError as error:
|
|
737
|
+
raise ToolError(str(error)) from None
|
|
738
|
+
except NoService:
|
|
739
|
+
raise ToolError(NO_SERVICE) from None
|