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,998 @@
|
|
|
1
|
+
"""The agent harnesses `mobster mcp install` knows: where each keeps its MCP servers, and the entry it needs."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import jsonc, textedit
|
|
12
|
+
from .client_files import _home, _read, backup, write_atomic
|
|
13
|
+
from .server_options import _options, _redact_cli_env
|
|
14
|
+
|
|
15
|
+
SERVER = "mobster"
|
|
16
|
+
GOOSE_TIMEOUT_S = 300
|
|
17
|
+
CLI_TIMEOUT_S = 60
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Env:
|
|
22
|
+
"""Where to look: the home folder, the environment and how to find a command."""
|
|
23
|
+
|
|
24
|
+
home: Path
|
|
25
|
+
environ: dict
|
|
26
|
+
which: object = None
|
|
27
|
+
applications: tuple = ()
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def current(cls):
|
|
31
|
+
home = Path(os.environ.get("HOME") or Path.home())
|
|
32
|
+
return cls(home=home, environ=dict(os.environ), which=shutil.which,
|
|
33
|
+
applications=(Path("/Applications"), home / "Applications"))
|
|
34
|
+
|
|
35
|
+
def find(self, name):
|
|
36
|
+
return self.which(name, path=self.environ.get("PATH")) if self.which else None
|
|
37
|
+
|
|
38
|
+
def var(self, name):
|
|
39
|
+
value = self.environ.get(name, "")
|
|
40
|
+
return Path(value).expanduser() if value else None
|
|
41
|
+
|
|
42
|
+
def xdg_config(self):
|
|
43
|
+
return self.var("XDG_CONFIG_HOME") or self.home / ".config"
|
|
44
|
+
|
|
45
|
+
def support(self, *parts):
|
|
46
|
+
"""~/Library/Application Support/..."""
|
|
47
|
+
return self.home.joinpath("Library", "Application Support", *parts)
|
|
48
|
+
|
|
49
|
+
def app(self, *names):
|
|
50
|
+
return any((folder / f"{name}.app").exists() for folder in self.applications for name in names)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Server:
|
|
55
|
+
"""What the client runs: ``command`` (absolute) with ``args``, and ``env`` (never a key)."""
|
|
56
|
+
|
|
57
|
+
command: str
|
|
58
|
+
args: list
|
|
59
|
+
env: dict = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
def argv(self):
|
|
62
|
+
return [self.command, *self.args]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Change:
|
|
67
|
+
"""What install or remove did (or, with --dry-run, would do) to one config."""
|
|
68
|
+
|
|
69
|
+
client: str
|
|
70
|
+
action: str # added, updated, unchanged, removed, absent, skipped, error
|
|
71
|
+
path: str = ""
|
|
72
|
+
method: str = "file" # file or cli
|
|
73
|
+
command: list = None
|
|
74
|
+
message: str = ""
|
|
75
|
+
|
|
76
|
+
def json(self):
|
|
77
|
+
out = {"client": self.client, "action": self.action, "path": self.path, "method": self.method}
|
|
78
|
+
if self.command:
|
|
79
|
+
out["command"] = self.command
|
|
80
|
+
if self.message:
|
|
81
|
+
out["message"] = self.message
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ----------------------------------------------------------------------------------------------- the server
|
|
86
|
+
|
|
87
|
+
def server_for(args=(), env=None):
|
|
88
|
+
"""The server entry for this Mobster: its absolute path (Dock apps start servers on launchd's PATH),
|
|
89
|
+
``mcp`` and ``args``."""
|
|
90
|
+
env = env or Env.current()
|
|
91
|
+
mcp_args = ["mcp", *args]
|
|
92
|
+
if getattr(sys, "frozen", False):
|
|
93
|
+
exe = os.path.realpath(sys.executable)
|
|
94
|
+
candidates = [env.find("mobster"), env.home / ".local/bin/mobster", "/opt/homebrew/bin/mobster",
|
|
95
|
+
"/usr/local/bin/mobster"]
|
|
96
|
+
for candidate in candidates:
|
|
97
|
+
if candidate and Path(candidate).exists() and os.path.realpath(candidate) == exe:
|
|
98
|
+
return Server(str(Path(candidate).absolute()), mcp_args)
|
|
99
|
+
return Server(exe, mcp_args)
|
|
100
|
+
script = Path(sys.executable).parent / "mobster"
|
|
101
|
+
if script.is_file() and os.access(script, os.X_OK):
|
|
102
|
+
return Server(str(script), mcp_args)
|
|
103
|
+
found = env.find("mobster")
|
|
104
|
+
if found:
|
|
105
|
+
return Server(str(Path(found).absolute()), mcp_args)
|
|
106
|
+
root = Path(__file__).resolve().parents[2]
|
|
107
|
+
return Server(sys.executable, ["-m", "mobile_agent", *mcp_args], {"PYTHONPATH": str(root)})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def same(existing, wanted):
|
|
111
|
+
"""Whether a client's entry already runs ``wanted``: the same command, arguments and environment."""
|
|
112
|
+
if not isinstance(existing, dict):
|
|
113
|
+
return False
|
|
114
|
+
keys = set(wanted) | {k for k in existing if k in ("command", "args", "env", "cmd", "envs", "environment")}
|
|
115
|
+
return all(_norm(existing.get(k)) == _norm(wanted.get(k)) for k in keys)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _norm(value):
|
|
119
|
+
return None if value in (None, {}, []) else value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _configured_server(client, current):
|
|
123
|
+
"""Read one existing client entry, refusing unsupported launch and environment settings."""
|
|
124
|
+
if not isinstance(current, dict):
|
|
125
|
+
raise ValueError("its server entry isn't an object")
|
|
126
|
+
if isinstance(client, OpenCode):
|
|
127
|
+
command_line = current.get("command")
|
|
128
|
+
if not isinstance(command_line, list) or not command_line:
|
|
129
|
+
raise ValueError("its command isn't a nonempty list")
|
|
130
|
+
command, argv = command_line[0], command_line[1:]
|
|
131
|
+
env_key = "environment"
|
|
132
|
+
else:
|
|
133
|
+
command = current.get("cmd" if isinstance(client, Goose) else "command")
|
|
134
|
+
argv = current.get("args")
|
|
135
|
+
env_key = "envs" if isinstance(client, Goose) else "env"
|
|
136
|
+
if not isinstance(command, str) or not command:
|
|
137
|
+
raise ValueError("its command isn't a program name")
|
|
138
|
+
options = _options(argv)
|
|
139
|
+
environment = current.get(env_key, {})
|
|
140
|
+
if not isinstance(environment, dict) or any(not isinstance(k, str) or not isinstance(v, str)
|
|
141
|
+
for k, v in environment.items()):
|
|
142
|
+
raise ValueError(f"its {env_key} isn't a string-to-string mapping")
|
|
143
|
+
expected = client.entry(Server(command, argv, environment))
|
|
144
|
+
launch_keys = {"command", env_key} if isinstance(client, OpenCode) else \
|
|
145
|
+
({"cmd", "args", env_key} if isinstance(client, Goose) else {"command", "args", env_key})
|
|
146
|
+
for key, value in current.items():
|
|
147
|
+
if key not in launch_keys and (key not in expected or value != expected[key]):
|
|
148
|
+
raise ValueError("its client settings aren't managed by Mobster")
|
|
149
|
+
return options, environment
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _merged_server(client, current, server, overrides):
|
|
153
|
+
old_options, old_env = _configured_server(client, current)
|
|
154
|
+
new_options = _options(server.args)
|
|
155
|
+
options = [pair for pair in old_options if pair[0] not in overrides]
|
|
156
|
+
options.extend(pair for pair in new_options if pair[0] in overrides)
|
|
157
|
+
prefix = server.args[:2] if server.args[:2] == ["-m", "mobile_agent"] else []
|
|
158
|
+
argv = [*prefix, "mcp"]
|
|
159
|
+
for flag, value in options:
|
|
160
|
+
argv.append(flag)
|
|
161
|
+
if value is not None:
|
|
162
|
+
argv.append(value)
|
|
163
|
+
return Server(server.command, argv, {**old_env, **server.env})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# -------------------------------------------------------------------------------------------- client base
|
|
167
|
+
|
|
168
|
+
class Client:
|
|
169
|
+
"""One harness. Subclasses give the paths and the entry; the base edits JSON or JSONC."""
|
|
170
|
+
|
|
171
|
+
id = ""
|
|
172
|
+
name = ""
|
|
173
|
+
aliases = ()
|
|
174
|
+
docs = ""
|
|
175
|
+
container = ("mcpServers",)
|
|
176
|
+
cli_replaces = False # whether the client's own add command replaces an existing entry
|
|
177
|
+
|
|
178
|
+
def detected(self, env):
|
|
179
|
+
"""(True, what was found) or (False, what was looked for)."""
|
|
180
|
+
raise NotImplementedError
|
|
181
|
+
|
|
182
|
+
def paths(self, env, project=None):
|
|
183
|
+
"""The config files to write, user-level or for ``project``. [] when the client has no such file."""
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
186
|
+
def entry(self, server):
|
|
187
|
+
entry = {"command": server.command, "args": list(server.args)}
|
|
188
|
+
if server.env:
|
|
189
|
+
entry["env"] = dict(server.env)
|
|
190
|
+
return entry
|
|
191
|
+
|
|
192
|
+
def skills_dir(self, env, project=None):
|
|
193
|
+
"""Where the client reads Agent Skills (SKILL.md folders), or None."""
|
|
194
|
+
if project:
|
|
195
|
+
return Path(project) / ".agents" / "skills"
|
|
196
|
+
return env.home / ".agents" / "skills"
|
|
197
|
+
|
|
198
|
+
# The JSON edit. Codex (TOML) and Goose (YAML) override these three.
|
|
199
|
+
def read(self, path):
|
|
200
|
+
text = _read(path)
|
|
201
|
+
return None if text is None else jsonc.get_member(text, list(self.container), SERVER)
|
|
202
|
+
|
|
203
|
+
def edit(self, text, server):
|
|
204
|
+
return jsonc.set_member(text or "", list(self.container), SERVER, self.entry(server))
|
|
205
|
+
|
|
206
|
+
def unedit(self, text):
|
|
207
|
+
return jsonc.remove_member(text, list(self.container), SERVER)
|
|
208
|
+
|
|
209
|
+
# Install and remove, through the client's command where it has one (``via_cli``), else the file.
|
|
210
|
+
def via_cli(self, env, project):
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def install(self, env, server, project=None, dry_run=False, *, overrides=frozenset(), reset_options=False):
|
|
214
|
+
paths = self.paths(env, project)
|
|
215
|
+
if not paths:
|
|
216
|
+
return [Change(self.id, "skipped", message=f"{self.name} has no project-level config")]
|
|
217
|
+
changes = []
|
|
218
|
+
for path in paths:
|
|
219
|
+
current = self._safe_read(path)
|
|
220
|
+
if isinstance(current, Change):
|
|
221
|
+
changes.append(current)
|
|
222
|
+
continue
|
|
223
|
+
selected = server
|
|
224
|
+
if current is not None and not reset_options:
|
|
225
|
+
try:
|
|
226
|
+
selected = _merged_server(self, current, server, overrides)
|
|
227
|
+
except ValueError as error:
|
|
228
|
+
changes.append(Change(self.id, "error", str(path), message=f"{_home(path)}: {error}. "
|
|
229
|
+
"Review its Mobster entry, or use --reset-server-options"))
|
|
230
|
+
continue
|
|
231
|
+
if current is not None and same(current, self.entry(selected)):
|
|
232
|
+
changes.append(Change(self.id, "unchanged", str(path)))
|
|
233
|
+
continue
|
|
234
|
+
action = "updated" if current is not None else "added"
|
|
235
|
+
argv = self.via_cli(env, project)
|
|
236
|
+
if argv:
|
|
237
|
+
replace = current is not None and not self.cli_replaces
|
|
238
|
+
changes.append(self._run_cli(env, path, action, argv("add", selected), dry_run,
|
|
239
|
+
remove_first=argv("remove", selected) if replace else None))
|
|
240
|
+
else:
|
|
241
|
+
changes.append(self._write(path, action, lambda text: self.edit(text, selected), dry_run))
|
|
242
|
+
return changes
|
|
243
|
+
|
|
244
|
+
def remove(self, env, project=None, dry_run=False):
|
|
245
|
+
paths = self.paths(env, project)
|
|
246
|
+
if not paths:
|
|
247
|
+
return [Change(self.id, "skipped", message=f"{self.name} has no project-level config")]
|
|
248
|
+
changes = []
|
|
249
|
+
for path in paths:
|
|
250
|
+
current = self._safe_read(path)
|
|
251
|
+
if isinstance(current, Change):
|
|
252
|
+
changes.append(current)
|
|
253
|
+
continue
|
|
254
|
+
if current is None:
|
|
255
|
+
changes.append(Change(self.id, "absent", str(path)))
|
|
256
|
+
continue
|
|
257
|
+
argv = self.via_cli(env, project)
|
|
258
|
+
if argv:
|
|
259
|
+
changes.append(self._run_cli(env, path, "removed", argv("remove", None), dry_run))
|
|
260
|
+
else:
|
|
261
|
+
changes.append(self._write(path, "removed", self.unedit, dry_run))
|
|
262
|
+
return changes
|
|
263
|
+
|
|
264
|
+
def status_paths(self, env, project=None):
|
|
265
|
+
return self.paths(env, project)
|
|
266
|
+
|
|
267
|
+
def status(self, env, project=None):
|
|
268
|
+
"""{configured, path, command, command_ok} for `mobster mcp clients`."""
|
|
269
|
+
for path in self.status_paths(env, project):
|
|
270
|
+
current = self._safe_read(path)
|
|
271
|
+
if isinstance(current, dict):
|
|
272
|
+
command = self.command_of(current)
|
|
273
|
+
return {"configured": True, "path": str(path), "command": command,
|
|
274
|
+
"command_ok": bool(command) and Path(command).is_file() and os.access(command, os.X_OK)}
|
|
275
|
+
if isinstance(current, Change):
|
|
276
|
+
return {"configured": False, "path": str(path), "error": current.message}
|
|
277
|
+
paths = self.paths(env, project)
|
|
278
|
+
return {"configured": False, "path": str(paths[0]) if paths else ""}
|
|
279
|
+
|
|
280
|
+
def command_of(self, entry):
|
|
281
|
+
return entry.get("command") if isinstance(entry.get("command"), str) else None
|
|
282
|
+
|
|
283
|
+
# Helpers.
|
|
284
|
+
def _safe_read(self, path):
|
|
285
|
+
try:
|
|
286
|
+
return self.read(path)
|
|
287
|
+
except (jsonc.JsoncError, textedit.EditError) as error:
|
|
288
|
+
return Change(self.id, "error", str(path), message=f"{_home(path)} can't be read: {error}. Fix it, "
|
|
289
|
+
"or add Mobster by hand")
|
|
290
|
+
except OSError as error:
|
|
291
|
+
return Change(self.id, "error", str(path), message=f"{_home(path)} can't be read ({error.strerror})")
|
|
292
|
+
|
|
293
|
+
def _write(self, path, action, transform, dry_run):
|
|
294
|
+
try:
|
|
295
|
+
before = _read(path)
|
|
296
|
+
after = transform(before)
|
|
297
|
+
except (jsonc.JsoncError, textedit.EditError) as error:
|
|
298
|
+
return Change(self.id, "error", str(path), message=f"{_home(path)}: {error}. Nothing was written")
|
|
299
|
+
except OSError as error:
|
|
300
|
+
return Change(self.id, "error", str(path), message=f"{_home(path)} can't be read ({error.strerror})")
|
|
301
|
+
if after == (before or ""):
|
|
302
|
+
return Change(self.id, "unchanged" if action != "removed" else "absent", str(path))
|
|
303
|
+
if not dry_run:
|
|
304
|
+
try:
|
|
305
|
+
write_atomic(path, after)
|
|
306
|
+
except OSError as error:
|
|
307
|
+
return Change(self.id, "error", str(path), message=f"{_home(path)} can't be written "
|
|
308
|
+
f"({error.strerror}). Nothing was changed")
|
|
309
|
+
return Change(self.id, action, str(path))
|
|
310
|
+
|
|
311
|
+
def _run_cli(self, env, path, action, argv, dry_run, remove_first=None):
|
|
312
|
+
commands = [remove_first, argv] if remove_first else [argv]
|
|
313
|
+
safe_argv = _redact_cli_env(argv)
|
|
314
|
+
if dry_run:
|
|
315
|
+
first = f"`{' '.join(remove_first[1:])}` first, then the command below" if remove_first else ""
|
|
316
|
+
return Change(self.id, action, str(path), "cli", command=safe_argv, message=first and f"would run {first}")
|
|
317
|
+
try:
|
|
318
|
+
backup(path)
|
|
319
|
+
except OSError as error:
|
|
320
|
+
return Change(self.id, "error", str(path), "cli", command=safe_argv,
|
|
321
|
+
message=f"{_home(path)} can't be backed up ({error.strerror}). Nothing was changed")
|
|
322
|
+
for command in commands:
|
|
323
|
+
try:
|
|
324
|
+
done = subprocess.run(command, env=env.environ, capture_output=True, text=True,
|
|
325
|
+
timeout=CLI_TIMEOUT_S, stdin=subprocess.DEVNULL, cwd=str(env.home))
|
|
326
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
327
|
+
return Change(self.id, "error", str(path), "cli", _redact_cli_env(command),
|
|
328
|
+
message=f"`{command[0]}` didn't run ({type(error).__name__})")
|
|
329
|
+
if done.returncode != 0:
|
|
330
|
+
return Change(self.id, "error", str(path), "cli", _redact_cli_env(command),
|
|
331
|
+
message=f"`{command[0]} mcp` exited {done.returncode}; check the client config")
|
|
332
|
+
return Change(self.id, action, str(path), "cli", command=safe_argv)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# -------------------------------------------------------------------------------------------- the clients
|
|
336
|
+
|
|
337
|
+
class ClaudeCode(Client):
|
|
338
|
+
# https://code.claude.com/docs/en/mcp (read 7 Oct 2026): `claude mcp add --scope user <name> -- <command>
|
|
339
|
+
# [args]` stores the server in ~/.claude.json's top-level "mcpServers" ($CLAUDE_CONFIG_DIR/.claude.json when
|
|
340
|
+
# that is set, from Claude Code 2.1.290's own code); project scope is .mcp.json at the project's root.
|
|
341
|
+
# Claude Code rewrites ~/.claude.json all the time, so its own command edits it whenever it's installed.
|
|
342
|
+
id, name, aliases = "claude-code", "Claude Code", ("claude", "claudecode", "claude_code")
|
|
343
|
+
docs = "https://code.claude.com/docs/en/mcp"
|
|
344
|
+
|
|
345
|
+
def config_dir(self, env):
|
|
346
|
+
return env.var("CLAUDE_CONFIG_DIR")
|
|
347
|
+
|
|
348
|
+
def detected(self, env):
|
|
349
|
+
if env.find("claude"):
|
|
350
|
+
return True, "the claude command"
|
|
351
|
+
folder = self.config_dir(env) or env.home / ".claude"
|
|
352
|
+
if folder.is_dir() or (env.home / ".claude.json").is_file():
|
|
353
|
+
return True, _home(folder)
|
|
354
|
+
return False, "no claude command or ~/.claude"
|
|
355
|
+
|
|
356
|
+
def paths(self, env, project=None):
|
|
357
|
+
if project:
|
|
358
|
+
return [Path(project) / ".mcp.json"]
|
|
359
|
+
return [(self.config_dir(env) or env.home) / ".claude.json"]
|
|
360
|
+
|
|
361
|
+
def entry(self, server):
|
|
362
|
+
# What `claude mcp add` writes, so a file edit and the command agree.
|
|
363
|
+
return {"type": "stdio", **super().entry(server), "env": dict(server.env)}
|
|
364
|
+
|
|
365
|
+
def via_cli(self, env, project):
|
|
366
|
+
command = env.find("claude")
|
|
367
|
+
if project or not command:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
def argv(verb, server):
|
|
371
|
+
if verb == "remove":
|
|
372
|
+
return [command, "mcp", "remove", "--scope", "user", SERVER]
|
|
373
|
+
extra = [item for key, value in server.env.items() for item in ("-e", f"{key}={value}")]
|
|
374
|
+
return [command, "mcp", "add", "--scope", "user", *extra, SERVER, "--", server.command, *server.args]
|
|
375
|
+
return argv
|
|
376
|
+
|
|
377
|
+
def skills_dir(self, env, project=None):
|
|
378
|
+
# https://code.claude.com/docs/en/skills: ~/.claude/skills/<name>/SKILL.md, .claude/skills in a project.
|
|
379
|
+
if project:
|
|
380
|
+
return Path(project) / ".claude" / "skills"
|
|
381
|
+
return (self.config_dir(env) or env.home / ".claude") / "skills"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Codex(Client):
|
|
385
|
+
# https://learn.chatgpt.com/docs/extend/mcp?surface=cli (read 7 Oct 2026): [mcp_servers.<name>] with command
|
|
386
|
+
# and args in $CODEX_HOME/config.toml (default ~/.codex), or .codex/config.toml in a trusted project;
|
|
387
|
+
# `codex mcp add <name> -- <command> [args]` writes the same table. Skills: $HOME/.agents/skills
|
|
388
|
+
# (https://learn.chatgpt.com/docs/build-skills).
|
|
389
|
+
id, name, aliases = "codex", "Codex", ("codex-cli", "openai-codex")
|
|
390
|
+
docs = "https://learn.chatgpt.com/docs/extend/mcp?surface=cli"
|
|
391
|
+
cli_replaces = True
|
|
392
|
+
|
|
393
|
+
def home(self, env):
|
|
394
|
+
return env.var("CODEX_HOME") or env.home / ".codex"
|
|
395
|
+
|
|
396
|
+
def detected(self, env):
|
|
397
|
+
if env.find("codex"):
|
|
398
|
+
return True, "the codex command"
|
|
399
|
+
if self.home(env).is_dir():
|
|
400
|
+
return True, _home(self.home(env))
|
|
401
|
+
return False, "no codex command or ~/.codex"
|
|
402
|
+
|
|
403
|
+
def paths(self, env, project=None):
|
|
404
|
+
if project:
|
|
405
|
+
return [Path(project) / ".codex" / "config.toml"]
|
|
406
|
+
return [self.home(env) / "config.toml"]
|
|
407
|
+
|
|
408
|
+
def via_cli(self, env, project):
|
|
409
|
+
command = env.find("codex")
|
|
410
|
+
if project or not command:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def argv(verb, server):
|
|
414
|
+
if verb == "remove":
|
|
415
|
+
return [command, "mcp", "remove", SERVER]
|
|
416
|
+
extra = [item for key, value in server.env.items() for item in ("--env", f"{key}={value}")]
|
|
417
|
+
# `codex mcp add` replaces an existing entry itself.
|
|
418
|
+
return [command, "mcp", "add", SERVER, *extra, "--", server.command, *server.args]
|
|
419
|
+
return argv
|
|
420
|
+
|
|
421
|
+
def read(self, path):
|
|
422
|
+
text = _read(path)
|
|
423
|
+
if text is None:
|
|
424
|
+
return None
|
|
425
|
+
servers = textedit.toml_loads(text).get("mcp_servers")
|
|
426
|
+
return servers.get(SERVER) if isinstance(servers, dict) else None
|
|
427
|
+
|
|
428
|
+
def edit(self, text, server):
|
|
429
|
+
return textedit.toml_set_table(text or "", ("mcp_servers", SERVER), self.entry(server))
|
|
430
|
+
|
|
431
|
+
def unedit(self, text):
|
|
432
|
+
return textedit.toml_remove_table(text, ("mcp_servers", SERVER))
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class Cursor(Client):
|
|
436
|
+
# https://cursor.com/docs/context/mcp (read 7 Oct 2026): ~/.cursor/mcp.json for every project,
|
|
437
|
+
# .cursor/mcp.json for one, {"mcpServers": {name: {command, args, env}}}. Its field table marks
|
|
438
|
+
# "type": "stdio" required while its examples leave it out, so the entry has it.
|
|
439
|
+
id, name, aliases = "cursor", "Cursor", ()
|
|
440
|
+
docs = "https://cursor.com/docs/context/mcp"
|
|
441
|
+
|
|
442
|
+
def detected(self, env):
|
|
443
|
+
if env.app("Cursor"):
|
|
444
|
+
return True, "Cursor.app"
|
|
445
|
+
if (env.home / ".cursor").is_dir():
|
|
446
|
+
return True, "~/.cursor"
|
|
447
|
+
return False, "no Cursor.app or ~/.cursor"
|
|
448
|
+
|
|
449
|
+
def paths(self, env, project=None):
|
|
450
|
+
return [(Path(project) if project else env.home) / ".cursor" / "mcp.json"]
|
|
451
|
+
|
|
452
|
+
def entry(self, server):
|
|
453
|
+
return {"type": "stdio", **super().entry(server)}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class VSCode(Client):
|
|
457
|
+
# https://code.visualstudio.com/docs/copilot/customization/mcp-servers (read 7 Oct 2026): the user profile's
|
|
458
|
+
# mcp.json (~/Library/Application Support/Code/User/mcp.json for the default profile) or .vscode/mcp.json,
|
|
459
|
+
# {"servers": {name: {"type": "stdio", command, args, env}}}. The file is JSONC: comments are kept.
|
|
460
|
+
id, name, aliases = "vscode", "VS Code", ("code", "vs-code", "visual-studio-code")
|
|
461
|
+
docs = "https://code.visualstudio.com/docs/copilot/customization/mcp-servers"
|
|
462
|
+
container = ("servers",)
|
|
463
|
+
|
|
464
|
+
def detected(self, env):
|
|
465
|
+
if env.app("Visual Studio Code"):
|
|
466
|
+
return True, "Visual Studio Code.app"
|
|
467
|
+
if env.support("Code", "User").is_dir():
|
|
468
|
+
return True, "~/Library/Application Support/Code"
|
|
469
|
+
return False, "no Visual Studio Code.app"
|
|
470
|
+
|
|
471
|
+
def paths(self, env, project=None):
|
|
472
|
+
if project:
|
|
473
|
+
return [Path(project) / ".vscode" / "mcp.json"]
|
|
474
|
+
return [env.support("Code", "User", "mcp.json")]
|
|
475
|
+
|
|
476
|
+
def entry(self, server):
|
|
477
|
+
return {"type": "stdio", **super().entry(server)}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ClaudeDesktop(Client):
|
|
481
|
+
# https://modelcontextprotocol.io/docs/develop/connect-local-servers (read 7 Oct 2026):
|
|
482
|
+
# ~/Library/Application Support/Claude/claude_desktop_config.json, {"mcpServers": {name: {command, args,
|
|
483
|
+
# env}}}, with absolute paths; restart the app after editing. It has no skills folder.
|
|
484
|
+
id, name, aliases = "claude-desktop", "Claude Desktop", ("claude-app", "claudedesktop", "desktop")
|
|
485
|
+
docs = "https://modelcontextprotocol.io/docs/develop/connect-local-servers"
|
|
486
|
+
|
|
487
|
+
def detected(self, env):
|
|
488
|
+
if env.app("Claude"):
|
|
489
|
+
return True, "Claude.app"
|
|
490
|
+
if env.support("Claude").is_dir():
|
|
491
|
+
return True, "~/Library/Application Support/Claude"
|
|
492
|
+
return False, "no Claude.app"
|
|
493
|
+
|
|
494
|
+
def paths(self, env, project=None):
|
|
495
|
+
return [] if project else [env.support("Claude", "claude_desktop_config.json")]
|
|
496
|
+
|
|
497
|
+
def skills_dir(self, env, project=None):
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class OpenCode(Client):
|
|
502
|
+
# https://opencode.ai/docs/mcp-servers/ and https://opencode.ai/docs/config/ (read 7 Oct 2026): "mcp":
|
|
503
|
+
# {name: {"type": "local", "command": [command, ...args], "enabled": true, "environment": {}}} in
|
|
504
|
+
# $XDG_CONFIG_HOME/opencode/opencode.json or opencode.jsonc (JSONC), or opencode.json in a project.
|
|
505
|
+
# Skills: ~/.config/opencode/skills, ~/.claude/skills and ~/.agents/skills.
|
|
506
|
+
id, name, aliases = "opencode", "opencode", ("open-code",)
|
|
507
|
+
docs = "https://opencode.ai/docs/mcp-servers/"
|
|
508
|
+
container = ("mcp",)
|
|
509
|
+
|
|
510
|
+
def folder(self, env):
|
|
511
|
+
return env.xdg_config() / "opencode"
|
|
512
|
+
|
|
513
|
+
def detected(self, env):
|
|
514
|
+
if env.find("opencode"):
|
|
515
|
+
return True, "the opencode command"
|
|
516
|
+
if self.folder(env).is_dir():
|
|
517
|
+
return True, _home(self.folder(env))
|
|
518
|
+
return False, "no opencode command or ~/.config/opencode"
|
|
519
|
+
|
|
520
|
+
def paths(self, env, project=None):
|
|
521
|
+
folder = Path(project) if project else self.folder(env)
|
|
522
|
+
for name in ("opencode.jsonc", "opencode.json"):
|
|
523
|
+
if (folder / name).is_file():
|
|
524
|
+
return [folder / name]
|
|
525
|
+
return [folder / "opencode.json"]
|
|
526
|
+
|
|
527
|
+
def entry(self, server):
|
|
528
|
+
entry = {"type": "local", "command": [server.command, *server.args], "enabled": True}
|
|
529
|
+
if server.env:
|
|
530
|
+
entry["environment"] = dict(server.env)
|
|
531
|
+
return entry
|
|
532
|
+
|
|
533
|
+
def command_of(self, entry):
|
|
534
|
+
command = entry.get("command")
|
|
535
|
+
return command[0] if isinstance(command, list) and command and isinstance(command[0], str) else None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class Gemini(Client):
|
|
539
|
+
# https://geminicli.com/docs/tools/mcp-server/ (read 7 Oct 2026): "mcpServers": {name: {command, args, env}}
|
|
540
|
+
# in ~/.gemini/settings.json ($GEMINI_CLI_HOME/.gemini when set) or .gemini/settings.json in a project.
|
|
541
|
+
# Skills: ~/.gemini/skills or ~/.agents/skills.
|
|
542
|
+
id, name, aliases = "gemini", "Gemini CLI", ("gemini-cli", "google-gemini")
|
|
543
|
+
docs = "https://geminicli.com/docs/tools/mcp-server/"
|
|
544
|
+
|
|
545
|
+
def folder(self, env):
|
|
546
|
+
return (env.var("GEMINI_CLI_HOME") or env.home) / ".gemini"
|
|
547
|
+
|
|
548
|
+
def detected(self, env):
|
|
549
|
+
if env.find("gemini"):
|
|
550
|
+
return True, "the gemini command"
|
|
551
|
+
if self.folder(env).is_dir():
|
|
552
|
+
return True, _home(self.folder(env))
|
|
553
|
+
return False, "no gemini command or ~/.gemini"
|
|
554
|
+
|
|
555
|
+
def paths(self, env, project=None):
|
|
556
|
+
return [(Path(project) / ".gemini" if project else self.folder(env)) / "settings.json"]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class Windsurf(Client):
|
|
560
|
+
# https://docs.devin.ai/desktop/devin-local/customizations (read 5 Nov 2026; Windsurf became Devin Desktop on
|
|
561
|
+
# 2 Jun 2026): {"mcpServers": {name: {command, args, env}}}, at user scope in ~/.config/devin/mcp_config.json.
|
|
562
|
+
# Mobster puts that file under $XDG_CONFIG_HOME when it's set, as it does for every client under ~/.config; Devin's
|
|
563
|
+
# pages don't mention the variable. Mobster 0.4.0 and earlier wrote ~/.codeium/windsurf/mcp_config.json, which
|
|
564
|
+
# Devin's CLI still imports by default. So install moves an entry found only there into the new file with its
|
|
565
|
+
# options (an --allow-device kept, never widened), then takes it out of the old one; remove and status look at both.
|
|
566
|
+
# Skills: ~/.codeium/windsurf/skills.
|
|
567
|
+
id, name, aliases = "windsurf", "Windsurf", ("devin", "devin-desktop", "codeium")
|
|
568
|
+
docs = "https://docs.devin.ai/desktop/devin-local/customizations"
|
|
569
|
+
|
|
570
|
+
def detected(self, env):
|
|
571
|
+
if env.app("Windsurf", "Devin"):
|
|
572
|
+
return True, "Windsurf.app"
|
|
573
|
+
for folder in (env.home / ".codeium" / "windsurf", env.xdg_config() / "devin"):
|
|
574
|
+
if folder.is_dir():
|
|
575
|
+
return True, _home(folder)
|
|
576
|
+
return False, "no Windsurf.app, Devin.app, ~/.codeium/windsurf or ~/.config/devin"
|
|
577
|
+
|
|
578
|
+
def paths(self, env, project=None):
|
|
579
|
+
return [] if project else [env.xdg_config() / "devin" / "mcp_config.json"]
|
|
580
|
+
|
|
581
|
+
def legacy_path(self, env):
|
|
582
|
+
return env.home / ".codeium" / "windsurf" / "mcp_config.json"
|
|
583
|
+
|
|
584
|
+
def install(self, env, server, project=None, dry_run=False, *, overrides=frozenset(), reset_options=False):
|
|
585
|
+
if project:
|
|
586
|
+
return super().install(env, server, project, dry_run, overrides=overrides, reset_options=reset_options)
|
|
587
|
+
legacy = self.legacy_path(env)
|
|
588
|
+
old = self._safe_read(legacy)
|
|
589
|
+
if isinstance(old, Change):
|
|
590
|
+
return [old] # an unreadable old entry could hold an --allow-device: fail closed rather than widen it
|
|
591
|
+
path = self.paths(env)[0]
|
|
592
|
+
current = self._safe_read(path)
|
|
593
|
+
if isinstance(current, Change):
|
|
594
|
+
return [current]
|
|
595
|
+
if old is not None and not reset_options:
|
|
596
|
+
options = {}
|
|
597
|
+
for where, entry in ((legacy, old), (path, current)):
|
|
598
|
+
if entry is None:
|
|
599
|
+
continue
|
|
600
|
+
try:
|
|
601
|
+
configured, environment = _configured_server(self, entry)
|
|
602
|
+
except ValueError as error:
|
|
603
|
+
return [Change(self.id, "error", str(where), message=f"{_home(where)}: {error}. "
|
|
604
|
+
"Review its Mobster entry, or use --reset-server-options")]
|
|
605
|
+
options[where] = (sorted(configured, key=repr), environment)
|
|
606
|
+
if current is None:
|
|
607
|
+
server = _merged_server(self, old, server, overrides)
|
|
608
|
+
elif options[legacy] != options[path]:
|
|
609
|
+
# Devin documents no precedence between the two files, so neither entry's options can win.
|
|
610
|
+
return [Change(self.id, "error", str(legacy), message=(
|
|
611
|
+
f"{_home(legacy)} and {_home(path)} hold Mobster entries with different options. Delete the "
|
|
612
|
+
"`mobster` entry you don't want from one of them, then run this again. Or run it with "
|
|
613
|
+
"--reset-server-options and the flags you want, such as --allow-device: that writes one fresh "
|
|
614
|
+
"entry and drops both entries' saved options"))]
|
|
615
|
+
changes = super().install(env, server, project, dry_run, overrides=overrides, reset_options=reset_options)
|
|
616
|
+
if old is not None and not any(change.action == "error" for change in changes):
|
|
617
|
+
changes.append(self._write(legacy, "removed", self.unedit, dry_run))
|
|
618
|
+
return changes
|
|
619
|
+
|
|
620
|
+
def remove(self, env, project=None, dry_run=False):
|
|
621
|
+
changes = super().remove(env, project, dry_run)
|
|
622
|
+
if project:
|
|
623
|
+
return changes
|
|
624
|
+
legacy = self.legacy_path(env)
|
|
625
|
+
old = self._safe_read(legacy)
|
|
626
|
+
if isinstance(old, Change):
|
|
627
|
+
changes.append(old)
|
|
628
|
+
elif old is not None:
|
|
629
|
+
changes.append(self._write(legacy, "removed", self.unedit, dry_run))
|
|
630
|
+
return changes
|
|
631
|
+
|
|
632
|
+
def status_paths(self, env, project=None):
|
|
633
|
+
return self.paths(env, project) + ([] if project else [self.legacy_path(env)])
|
|
634
|
+
|
|
635
|
+
def skills_dir(self, env, project=None):
|
|
636
|
+
if project:
|
|
637
|
+
return Path(project) / ".windsurf" / "skills"
|
|
638
|
+
return env.home / ".codeium" / "windsurf" / "skills"
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class CopilotCLI(Client):
|
|
642
|
+
# https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers (read 26 Oct 2026):
|
|
643
|
+
# {"mcpServers": {name: {"type": "local", command, args, env, "tools": ["*"]}}} in ~/.copilot/mcp-config.json
|
|
644
|
+
# ($COPILOT_HOME replaces ~/.copilot), or .github/mcp.json in a repository (.mcp.json is Claude Code's file, so
|
|
645
|
+
# it is left alone). `copilot mcp add` writes the same file but adds no "tools" key by itself. Skills:
|
|
646
|
+
# ~/.copilot/skills.
|
|
647
|
+
id, name, aliases = "copilot-cli", "Copilot CLI", ("copilot", "github-copilot-cli", "gh-copilot")
|
|
648
|
+
docs = "https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers"
|
|
649
|
+
|
|
650
|
+
def folder(self, env):
|
|
651
|
+
return env.var("COPILOT_HOME") or env.home / ".copilot"
|
|
652
|
+
|
|
653
|
+
def detected(self, env):
|
|
654
|
+
if env.find("copilot"):
|
|
655
|
+
return True, "the copilot command"
|
|
656
|
+
if self.folder(env).is_dir():
|
|
657
|
+
return True, _home(self.folder(env))
|
|
658
|
+
return False, "no copilot command or ~/.copilot"
|
|
659
|
+
|
|
660
|
+
def paths(self, env, project=None):
|
|
661
|
+
if project:
|
|
662
|
+
return [Path(project) / ".github" / "mcp.json"]
|
|
663
|
+
return [self.folder(env) / "mcp-config.json"]
|
|
664
|
+
|
|
665
|
+
def entry(self, server):
|
|
666
|
+
return {"type": "local", **super().entry(server), "env": dict(server.env), "tools": ["*"]}
|
|
667
|
+
|
|
668
|
+
def skills_dir(self, env, project=None):
|
|
669
|
+
return Path(project) / ".github" / "skills" if project else self.folder(env) / "skills"
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
class Zed(Client):
|
|
673
|
+
# https://zed.dev/docs/ai/mcp (read 7 Oct 2026): "context_servers": {name: {command, args, env}} in
|
|
674
|
+
# ~/.config/zed/settings.json, JSON with comments, or .zed/settings.json in a project. Skills:
|
|
675
|
+
# ~/.agents/skills. (/usr/local/zfs/bin/zed is the ZFS event daemon, so the zed command proves nothing.)
|
|
676
|
+
id, name, aliases = "zed", "Zed", ("zed-editor",)
|
|
677
|
+
docs = "https://zed.dev/docs/ai/mcp"
|
|
678
|
+
container = ("context_servers",)
|
|
679
|
+
|
|
680
|
+
def detected(self, env):
|
|
681
|
+
if env.app("Zed", "Zed Preview"):
|
|
682
|
+
return True, "Zed.app"
|
|
683
|
+
if (env.home / ".config" / "zed").is_dir():
|
|
684
|
+
return True, "~/.config/zed"
|
|
685
|
+
return False, "no Zed.app or ~/.config/zed"
|
|
686
|
+
|
|
687
|
+
def paths(self, env, project=None):
|
|
688
|
+
if project:
|
|
689
|
+
return [Path(project) / ".zed" / "settings.json"]
|
|
690
|
+
return [env.home / ".config" / "zed" / "settings.json"]
|
|
691
|
+
|
|
692
|
+
def entry(self, server):
|
|
693
|
+
return {"command": server.command, "args": list(server.args), "env": dict(server.env)}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class Goose(Client):
|
|
697
|
+
# https://goose-docs.ai/docs/guides/config-files (read 7 Oct 2026): ~/.config/goose/config.yaml
|
|
698
|
+
# ($GOOSE_PATH_ROOT/config when set), "extensions": {name: {enabled, type: stdio, name, cmd, args, envs,
|
|
699
|
+
# env_keys, timeout}}. Skills: ~/.agents/skills.
|
|
700
|
+
id, name, aliases = "goose", "Goose", ("block-goose",)
|
|
701
|
+
docs = "https://goose-docs.ai/docs/guides/config-files"
|
|
702
|
+
|
|
703
|
+
def folder(self, env):
|
|
704
|
+
root = env.var("GOOSE_PATH_ROOT")
|
|
705
|
+
return root / "config" if root else env.xdg_config() / "goose"
|
|
706
|
+
|
|
707
|
+
def detected(self, env):
|
|
708
|
+
if env.find("goose"):
|
|
709
|
+
return True, "the goose command"
|
|
710
|
+
if env.app("Goose"):
|
|
711
|
+
return True, "Goose.app"
|
|
712
|
+
if self.folder(env).is_dir():
|
|
713
|
+
return True, _home(self.folder(env))
|
|
714
|
+
return False, "no goose command or ~/.config/goose"
|
|
715
|
+
|
|
716
|
+
def paths(self, env, project=None):
|
|
717
|
+
return [] if project else [self.folder(env) / "config.yaml"]
|
|
718
|
+
|
|
719
|
+
def entry(self, server):
|
|
720
|
+
return {"enabled": True, "type": "stdio", "name": SERVER,
|
|
721
|
+
"description": "Check iOS screens and drive an iPhone with Mobster",
|
|
722
|
+
"cmd": server.command, "args": list(server.args), "envs": dict(server.env), "env_keys": [],
|
|
723
|
+
"timeout": GOOSE_TIMEOUT_S}
|
|
724
|
+
|
|
725
|
+
def command_of(self, entry):
|
|
726
|
+
return entry.get("cmd") if isinstance(entry.get("cmd"), str) else None
|
|
727
|
+
|
|
728
|
+
def read(self, path):
|
|
729
|
+
text = _read(path)
|
|
730
|
+
if text is None:
|
|
731
|
+
return None
|
|
732
|
+
extensions = textedit.yaml_loads(text).get("extensions")
|
|
733
|
+
return extensions.get(SERVER) if isinstance(extensions, dict) else None
|
|
734
|
+
|
|
735
|
+
def edit(self, text, server):
|
|
736
|
+
return textedit.yaml_set_entry(text or "", "extensions", SERVER, self.entry(server))
|
|
737
|
+
|
|
738
|
+
def unedit(self, text):
|
|
739
|
+
return textedit.yaml_remove_entry(text, "extensions", SERVER)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class Amp(Client):
|
|
743
|
+
# https://ampcode.com/docs/markdown/cli/settings and /customize/mcp (read 7 Oct 2026): "amp.mcpServers":
|
|
744
|
+
# {name: {command, args, env}} in ~/.config/amp/settings.json (or settings.jsonc), or .amp/settings.json in a
|
|
745
|
+
# workspace. "amp.mcpServers" is one key with a dot in it. Skills: ~/.config/agents/skills, ~/.agents/skills.
|
|
746
|
+
id, name, aliases = "amp", "Amp", ("ampcode", "sourcegraph-amp")
|
|
747
|
+
docs = "https://ampcode.com/docs/markdown/cli/settings"
|
|
748
|
+
container = ("amp.mcpServers",)
|
|
749
|
+
|
|
750
|
+
def folder(self, env):
|
|
751
|
+
return env.xdg_config() / "amp"
|
|
752
|
+
|
|
753
|
+
def detected(self, env):
|
|
754
|
+
if env.find("amp"):
|
|
755
|
+
return True, "the amp command"
|
|
756
|
+
if self.folder(env).is_dir():
|
|
757
|
+
return True, _home(self.folder(env))
|
|
758
|
+
return False, "no amp command or ~/.config/amp"
|
|
759
|
+
|
|
760
|
+
def paths(self, env, project=None):
|
|
761
|
+
folder = Path(project) / ".amp" if project else self.folder(env)
|
|
762
|
+
for name in ("settings.jsonc", "settings.json"):
|
|
763
|
+
if (folder / name).is_file():
|
|
764
|
+
return [folder / name]
|
|
765
|
+
return [folder / "settings.json"]
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class Cline(Client):
|
|
769
|
+
# https://docs.cline.bot/mcp/configuring-mcp-servers (read 7 Oct 2026): ~/.cline/data/settings/
|
|
770
|
+
# cline_mcp_settings.json, which the VS Code extension, the CLI and JetBrains share; {"mcpServers": {name:
|
|
771
|
+
# {command, args, env, disabled, autoApprove}}}. Older extensions read the file in VS Code's globalStorage,
|
|
772
|
+
# which is written too when it exists. Skills: ~/.cline/skills and ~/.agents/skills.
|
|
773
|
+
id, name, aliases = "cline", "Cline", ("cline-cli",)
|
|
774
|
+
docs = "https://docs.cline.bot/mcp/configuring-mcp-servers"
|
|
775
|
+
LEGACY = ("Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
776
|
+
|
|
777
|
+
def folder(self, env):
|
|
778
|
+
return env.var("CLINE_DIR") or env.home / ".cline"
|
|
779
|
+
|
|
780
|
+
def detected(self, env):
|
|
781
|
+
if env.find("cline"):
|
|
782
|
+
return True, "the cline command"
|
|
783
|
+
if self.folder(env).is_dir():
|
|
784
|
+
return True, _home(self.folder(env))
|
|
785
|
+
if env.support(*self.LEGACY[:4]).is_dir():
|
|
786
|
+
return True, "Cline in VS Code"
|
|
787
|
+
return False, "no cline command, ~/.cline or Cline extension"
|
|
788
|
+
|
|
789
|
+
def paths(self, env, project=None):
|
|
790
|
+
if project:
|
|
791
|
+
return []
|
|
792
|
+
override = env.var("CLINE_MCP_SETTINGS_PATH")
|
|
793
|
+
if override:
|
|
794
|
+
return [override]
|
|
795
|
+
data = env.var("CLINE_DATA_DIR") or self.folder(env) / "data"
|
|
796
|
+
paths = [data / "settings" / "cline_mcp_settings.json"]
|
|
797
|
+
if env.support(*self.LEGACY[:4]).is_dir():
|
|
798
|
+
paths.append(env.support(*self.LEGACY))
|
|
799
|
+
return paths
|
|
800
|
+
|
|
801
|
+
def entry(self, server):
|
|
802
|
+
# Never auto-approve: a phone tool acts on someone's real iPhone.
|
|
803
|
+
return {**super().entry(server), "disabled": False, "autoApprove": []}
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
class Omp(Client):
|
|
807
|
+
# oh-my-pi's docs/mcp-config.md (github.com/can1357/oh-my-pi, read 7 Oct 2026) and omp 18.2.8's own code:
|
|
808
|
+
# {"mcpServers": {name: {"type": "stdio", command, args, env}}} in ~/.omp/agent/mcp.json
|
|
809
|
+
# ($PI_CODING_AGENT_DIR, or $PI_CONFIG_DIR/agent), or .omp/mcp.json in a project. omp reads other clients'
|
|
810
|
+
# user configs only when told to, so it gets its own entry. Skills: ~/.agents/skills (on by default).
|
|
811
|
+
id, name, aliases = "omp", "oh-my-pi (omp)", ("oh-my-pi", "pi")
|
|
812
|
+
docs = "https://github.com/can1357/oh-my-pi/blob/main/docs/mcp-config.md"
|
|
813
|
+
|
|
814
|
+
def folder(self, env):
|
|
815
|
+
agent = env.var("PI_CODING_AGENT_DIR")
|
|
816
|
+
if agent:
|
|
817
|
+
return agent
|
|
818
|
+
return (env.var("PI_CONFIG_DIR") or env.home / ".omp") / "agent"
|
|
819
|
+
|
|
820
|
+
def detected(self, env):
|
|
821
|
+
if env.find("omp"):
|
|
822
|
+
return True, "the omp command"
|
|
823
|
+
if self.folder(env).is_dir():
|
|
824
|
+
return True, _home(self.folder(env))
|
|
825
|
+
return False, "no omp command or ~/.omp"
|
|
826
|
+
|
|
827
|
+
def paths(self, env, project=None):
|
|
828
|
+
return [(Path(project) / ".omp" if project else self.folder(env)) / "mcp.json"]
|
|
829
|
+
|
|
830
|
+
def entry(self, server):
|
|
831
|
+
return {"type": "stdio", **super().entry(server)}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class _XcodeClaudeFile(Client):
|
|
835
|
+
# Xcode starts its Claude Agent with CLAUDE_CONFIG_DIR set to ClaudeAgentConfig (strings in Xcode 26.4's
|
|
836
|
+
# IDEIntelligenceAgents, read 15 Oct 2026), and Claude Code reads $CLAUDE_CONFIG_DIR/.claude.json.
|
|
837
|
+
id, name = "xcode", "Xcode"
|
|
838
|
+
|
|
839
|
+
def paths(self, env, project=None):
|
|
840
|
+
return [] if project else [xcode_folder(env) / "ClaudeAgentConfig" / ".claude.json"]
|
|
841
|
+
|
|
842
|
+
def entry(self, server):
|
|
843
|
+
return {"type": "stdio", **super().entry(server), "env": dict(server.env)}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class _XcodeCodexFile(Codex):
|
|
847
|
+
# Xcode sets CODEX_HOME to its codex folder (the "Override CODEX_HOME" log line in the same framework);
|
|
848
|
+
# Codex reads $CODEX_HOME/config.toml. Xcode says Codex has no SSE servers, so stdio only.
|
|
849
|
+
id, name = "xcode", "Xcode"
|
|
850
|
+
|
|
851
|
+
def paths(self, env, project=None):
|
|
852
|
+
return [] if project else [xcode_folder(env) / "codex" / "config.toml"]
|
|
853
|
+
|
|
854
|
+
def via_cli(self, env, project):
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def xcode_folder(env):
|
|
859
|
+
return env.home / "Library" / "Developer" / "Xcode" / "CodingAssistant"
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class Xcode(Client):
|
|
863
|
+
# https://developer.apple.com/documentation/xcode/extending-and-customizing-agents (read 15 Oct 2026):
|
|
864
|
+
# configuration files for the agents that run inside Xcode go in ~/Library/Developer/Xcode/CodingAssistant/
|
|
865
|
+
# ClaudeAgentConfig, .../codex and .../gemini, and "only affect agents when you launch them in Xcode".
|
|
866
|
+
# Mobster goes into the first two, the ones whose file format is known. Gemini's isn't confirmed.
|
|
867
|
+
id, name, aliases = "xcode", "Xcode", ("xcode-agents", "xcode-agent")
|
|
868
|
+
docs = "https://developer.apple.com/documentation/xcode/extending-and-customizing-agents"
|
|
869
|
+
|
|
870
|
+
def __init__(self):
|
|
871
|
+
self.parts = (_XcodeClaudeFile(), _XcodeCodexFile())
|
|
872
|
+
|
|
873
|
+
def detected(self, env):
|
|
874
|
+
if env.app("Xcode"):
|
|
875
|
+
return True, "Xcode.app"
|
|
876
|
+
if xcode_folder(env).is_dir():
|
|
877
|
+
return True, "~/Library/Developer/Xcode/CodingAssistant"
|
|
878
|
+
return False, "no Xcode.app or ~/Library/Developer/Xcode/CodingAssistant"
|
|
879
|
+
|
|
880
|
+
def paths(self, env, project=None):
|
|
881
|
+
return [path for part in self.parts for path in part.paths(env, project)]
|
|
882
|
+
|
|
883
|
+
def install(self, env, server, project=None, dry_run=False, *, overrides=frozenset(), reset_options=False):
|
|
884
|
+
if project:
|
|
885
|
+
return [Change(self.id, "skipped", message="Xcode's agents have no project-level config")]
|
|
886
|
+
return [change for part in self.parts for change in part.install(
|
|
887
|
+
env, server, project, dry_run, overrides=overrides, reset_options=reset_options)]
|
|
888
|
+
|
|
889
|
+
def remove(self, env, project=None, dry_run=False):
|
|
890
|
+
if project:
|
|
891
|
+
return [Change(self.id, "skipped", message="Xcode's agents have no project-level config")]
|
|
892
|
+
return [change for part in self.parts for change in part.remove(env, project, dry_run)]
|
|
893
|
+
|
|
894
|
+
def status(self, env, project=None):
|
|
895
|
+
states = [part.status(env, project) for part in self.parts]
|
|
896
|
+
for state in states:
|
|
897
|
+
if state.get("configured") or state.get("error"):
|
|
898
|
+
return state
|
|
899
|
+
return states[0]
|
|
900
|
+
|
|
901
|
+
def skills_dir(self, env, project=None):
|
|
902
|
+
return None if project else xcode_folder(env) / "ClaudeAgentConfig" / "skills"
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
CLIENTS = [ClaudeCode(), Codex(), Cursor(), VSCode(), ClaudeDesktop(), OpenCode(), Gemini(), Windsurf(),
|
|
906
|
+
CopilotCLI(), Zed(), Goose(), Amp(), Cline(), Omp(), Xcode()]
|
|
907
|
+
BY_ID = {client.id: client for client in CLIENTS}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def lookup(name):
|
|
911
|
+
"""The client called ``name`` (its id, an alias or its display name, any case), or None."""
|
|
912
|
+
key = name.strip().lower().replace(" ", "-")
|
|
913
|
+
for client in CLIENTS:
|
|
914
|
+
if key in (client.id, client.name.lower().replace(" ", "-"), *client.aliases):
|
|
915
|
+
return client
|
|
916
|
+
return None
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def suggestion(name):
|
|
920
|
+
names = [client.id for client in CLIENTS] + [alias for client in CLIENTS for alias in client.aliases]
|
|
921
|
+
close = difflib.get_close_matches(name.lower(), names, n=1, cutoff=.6)
|
|
922
|
+
return lookup(close[0]).id if close else None
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# ------------------------------------------------------------------------------------- Xcode's own tools
|
|
926
|
+
|
|
927
|
+
BRIDGE = "xcode"
|
|
928
|
+
BRIDGE_ARGV = ["xcrun", "mcpbridge"]
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def mcpbridge(env):
|
|
932
|
+
"""The ``mcpbridge`` inside an installed Xcode (26.3 or later has it), or None."""
|
|
933
|
+
for folder in env.applications:
|
|
934
|
+
for app in sorted(Path(folder).glob("Xcode*.app")):
|
|
935
|
+
tool = app / "Contents" / "Developer" / "usr" / "bin" / "mcpbridge"
|
|
936
|
+
if tool.is_file():
|
|
937
|
+
return tool
|
|
938
|
+
return None
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def bridge_install(client, env, dry_run=False, remove=False):
|
|
942
|
+
"""Add (or with ``remove``, take out) Xcode's own MCP server, ``xcrun mcpbridge``."""
|
|
943
|
+
cid = client.id
|
|
944
|
+
paths = client.paths(env)
|
|
945
|
+
if cid not in ("claude-code", "codex") or not paths:
|
|
946
|
+
return Change(cid, "skipped", message="Xcode's tools are added to Claude Code and Codex only")
|
|
947
|
+
if not remove and not mcpbridge(env):
|
|
948
|
+
return Change(cid, "skipped", message="Xcode's tools need Xcode 26.3 or later, which has `xcrun mcpbridge`. "
|
|
949
|
+
"Install it, then run this again")
|
|
950
|
+
path = paths[0]
|
|
951
|
+
wanted = {"command": BRIDGE_ARGV[0], "args": BRIDGE_ARGV[1:]}
|
|
952
|
+
if cid == "claude-code":
|
|
953
|
+
wanted = {"type": "stdio", **wanted, "env": {}}
|
|
954
|
+
try:
|
|
955
|
+
text = _read(path)
|
|
956
|
+
if text is None:
|
|
957
|
+
current = None
|
|
958
|
+
elif cid == "codex":
|
|
959
|
+
tables = textedit.toml_loads(text).get("mcp_servers")
|
|
960
|
+
current = tables.get(BRIDGE) if isinstance(tables, dict) else None
|
|
961
|
+
else:
|
|
962
|
+
current = jsonc.get_member(text, ["mcpServers"], BRIDGE)
|
|
963
|
+
except (jsonc.JsoncError, textedit.EditError) as error:
|
|
964
|
+
return Change(cid, "error", str(path), message=f"{_home(path)} can't be read: {error}. Nothing was written")
|
|
965
|
+
except OSError as error:
|
|
966
|
+
return Change(cid, "error", str(path), message=f"{_home(path)} can't be read ({error.strerror})")
|
|
967
|
+
note = "Xcode's tools (xcrun mcpbridge)"
|
|
968
|
+
if remove:
|
|
969
|
+
if current is None:
|
|
970
|
+
return Change(cid, "absent", str(path), message=note)
|
|
971
|
+
if not same(current, wanted):
|
|
972
|
+
return Change(cid, "unchanged", str(path), message="the xcode entry runs something else, so it stays")
|
|
973
|
+
elif current is not None:
|
|
974
|
+
if not same(current, wanted):
|
|
975
|
+
return Change(cid, "unchanged", str(path), message="an xcode entry that runs something else is already "
|
|
976
|
+
"there, so it stays")
|
|
977
|
+
return Change(cid, "unchanged", str(path), message=note)
|
|
978
|
+
action = "removed" if remove else "added"
|
|
979
|
+
command = env.find("claude" if cid == "claude-code" else "codex")
|
|
980
|
+
if command and cid == "claude-code":
|
|
981
|
+
argv = [command, "mcp", "remove", "--scope", "user", BRIDGE] if remove else \
|
|
982
|
+
[command, "mcp", "add", "--scope", "user", "--transport", "stdio", BRIDGE, "--", *BRIDGE_ARGV]
|
|
983
|
+
elif command:
|
|
984
|
+
argv = [command, "mcp", "remove", BRIDGE] if remove else [command, "mcp", "add", BRIDGE, "--", *BRIDGE_ARGV]
|
|
985
|
+
else:
|
|
986
|
+
argv = None
|
|
987
|
+
if argv:
|
|
988
|
+
change = client._run_cli(env, path, action, argv, dry_run)
|
|
989
|
+
elif cid == "codex":
|
|
990
|
+
change = client._write(path, action, lambda t: textedit.toml_remove_table(t, ("mcp_servers", BRIDGE))
|
|
991
|
+
if remove else textedit.toml_set_table(t or "", ("mcp_servers", BRIDGE), wanted),
|
|
992
|
+
dry_run)
|
|
993
|
+
else:
|
|
994
|
+
change = client._write(path, action, lambda t: jsonc.remove_member(t, ["mcpServers"], BRIDGE) if remove
|
|
995
|
+
else jsonc.set_member(t or "", ["mcpServers"], BRIDGE, wanted), dry_run)
|
|
996
|
+
if not change.message:
|
|
997
|
+
change.message = note
|
|
998
|
+
return change
|