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,396 @@
|
|
|
1
|
+
"""The Microsoft 365 connector's tools, answered by Mobster with Microsoft Graph and your sign-in (a delegated token),
|
|
2
|
+
because Microsoft runs no MCP server for Outlook, Calendar, OneDrive, Contacts and Teams chats that a Mobster app can
|
|
3
|
+
sign in to."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from urllib.parse import quote, urlencode
|
|
10
|
+
|
|
11
|
+
from ..outlets import net, webhook
|
|
12
|
+
from . import client as mcp
|
|
13
|
+
from .api_session import ApiSession, Refusal, count_schema, required_text, short, tool_spec
|
|
14
|
+
|
|
15
|
+
API = "https://graph.microsoft.com/v1.0"
|
|
16
|
+
NAME = "Microsoft 365"
|
|
17
|
+
LIMIT_TOP = 50
|
|
18
|
+
BODY_CHARS = 8000 # what is kept of one message's text
|
|
19
|
+
TEXT_CHARS = 2000 # what is kept of one chat message's text
|
|
20
|
+
SEND_CHARS = 28_000 # Teams takes about 28 KB of message
|
|
21
|
+
READ_LIMIT = 4 * 1024 * 1024
|
|
22
|
+
ID = re.compile(r"[A-Za-z0-9_=:.@!%+\-]{1,512}")
|
|
23
|
+
EMAIL = re.compile(r"[^@\s<>,;\"']{1,64}@[A-Za-z0-9.-]{1,200}\.[A-Za-z]{2,24}")
|
|
24
|
+
WHEN = re.compile(r"\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?(Z|[+-]\d{2}:\d{2})?")
|
|
25
|
+
ZONE = re.compile(r"[A-Za-z0-9_/ +.\-]{1,60}")
|
|
26
|
+
FOLDERS = ("inbox", "sentitems", "drafts", "archive", "deleteditems")
|
|
27
|
+
DENIED = frozenset({"ErrorAccessDenied", "Authorization_RequestDenied", "AccessDenied",
|
|
28
|
+
"ErrorInsufficientPermissionsToDelegate", "Forbidden"})
|
|
29
|
+
REFUSED = {
|
|
30
|
+
"ErrorItemNotFound": "Microsoft has no item with that id. Use an id from a list or a search",
|
|
31
|
+
"ResourceNotFound": "Microsoft has nothing at that id. Use an id from a list or a search",
|
|
32
|
+
"ErrorInvalidIdMalformed": "That id isn't one Microsoft gave. Use an id from a list or a search",
|
|
33
|
+
"ErrorQuotaExceeded": "Your mailbox is full",
|
|
34
|
+
"ErrorMessageSizeExceeded": "The message is too big to send. Shorten it",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ADDRESSES = {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 20,
|
|
39
|
+
"description": "Email addresses."}
|
|
40
|
+
WHEN_FIELD = {"type": "string", "description": "A date or a date and time, such as 2026-10-20 or 2026-10-20T14:00:00Z."}
|
|
41
|
+
TOOLS = (
|
|
42
|
+
tool_spec("list_mail", "List Outlook mail",
|
|
43
|
+
"Your Outlook messages, newest first: each one's id, subject, sender, time, read state and a preview. "
|
|
44
|
+
"Give query to search instead (Outlook's search words work: from:sam, subject:invoice); a search is "
|
|
45
|
+
"ordered by match.",
|
|
46
|
+
{"folder": {"type": "string", "enum": list(FOLDERS), "description": "inbox if left out. Not used by a "
|
|
47
|
+
"search, which covers all folders."},
|
|
48
|
+
"query": {"type": "string", "description": "Words to search for."},
|
|
49
|
+
"limit": count_schema(LIMIT_TOP, 10)}),
|
|
50
|
+
tool_spec("read_message", "Read an Outlook message",
|
|
51
|
+
"One message's sender, recipients, subject, time and text, from an id that list_mail gave.",
|
|
52
|
+
{"id": {"type": "string", "description": "The message's id."}}, required=("id",)),
|
|
53
|
+
tool_spec("list_events", "List my calendar events",
|
|
54
|
+
"Your calendar between two times, in time order: each event's id, subject, start, end (UTC), place, "
|
|
55
|
+
"organizer and attendees.",
|
|
56
|
+
{"start": WHEN_FIELD | {"description": "From when. Now if left out."},
|
|
57
|
+
"end": WHEN_FIELD | {"description": "Until when. 7 days after start if left out."},
|
|
58
|
+
"limit": count_schema(LIMIT_TOP, 25)}),
|
|
59
|
+
tool_spec("create_event", "Create a calendar event",
|
|
60
|
+
"Add an event to your calendar. With attendees, Outlook emails them an invitation as you.",
|
|
61
|
+
{"subject": {"type": "string"}, "start": {"type": "string",
|
|
62
|
+
"description": "Local time, 2026-10-20T14:00:00."},
|
|
63
|
+
"end": {"type": "string", "description": "Local time, 2026-10-20T15:00:00."},
|
|
64
|
+
"timezone": {"type": "string", "description": "A time zone name such as "
|
|
65
|
+
"Europe/London; UTC if left out."},
|
|
66
|
+
"attendees": ADDRESSES, "location": {"type": "string"}, "body": {"type": "string"}},
|
|
67
|
+
required=("subject", "start", "end"), read_only=False),
|
|
68
|
+
tool_spec("search_files", "Search OneDrive files",
|
|
69
|
+
"Your OneDrive files and folders matching words in their name or content: each one's id, name, link, "
|
|
70
|
+
"size and time changed.",
|
|
71
|
+
{"query": {"type": "string", "description": "What to search for."}, "limit": count_schema(LIMIT_TOP, 10)},
|
|
72
|
+
required=("query",)),
|
|
73
|
+
tool_spec("list_contacts", "List my contacts",
|
|
74
|
+
"Your Outlook contacts: each one's name, email addresses, company and phone. Give query to search them.",
|
|
75
|
+
{"query": {"type": "string", "description": "A name or an address to search for."},
|
|
76
|
+
"limit": count_schema(LIMIT_TOP, 25)}),
|
|
77
|
+
tool_spec("list_chats", "List my Teams chats",
|
|
78
|
+
"Your Teams chats, most recently active first: each one's id, topic, kind and last activity.",
|
|
79
|
+
{"limit": count_schema(LIMIT_TOP, 20)}),
|
|
80
|
+
tool_spec("read_chat", "Read a Teams chat",
|
|
81
|
+
"A Teams chat's latest messages, newest first: each one's sender, time and text.",
|
|
82
|
+
{"chat_id": {"type": "string", "description": "A chat's id from list_chats."},
|
|
83
|
+
"limit": count_schema(LIMIT_TOP, 20)},
|
|
84
|
+
required=("chat_id",)),
|
|
85
|
+
tool_spec("send_mail", "Send an email",
|
|
86
|
+
"Send an email as you from Outlook, in plain text. A copy goes in Sent Items.",
|
|
87
|
+
{"to": ADDRESSES, "cc": ADDRESSES, "subject": {"type": "string"}, "body": {"type": "string"}},
|
|
88
|
+
required=("to", "subject", "body"), read_only=False),
|
|
89
|
+
tool_spec("send_chat_message", "Send a Teams chat message",
|
|
90
|
+
"Post a message as you in a Teams chat, in plain text.",
|
|
91
|
+
{"chat_id": {"type": "string", "description": "A chat's id from list_chats."},
|
|
92
|
+
"text": {"type": "string"}},
|
|
93
|
+
required=("chat_id", "text"), read_only=False),
|
|
94
|
+
)
|
|
95
|
+
WRITES = frozenset(t["name"] for t in TOOLS if not t["annotations"]["readOnlyHint"])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def connect(*, token=None, timeout=mcp.CONNECT_TIMEOUT, name=NAME, call=None):
|
|
99
|
+
"""A ready GraphSession for ``token``. AuthNeeded without one, or when Microsoft refuses it."""
|
|
100
|
+
session = GraphSession(token, name=name, call=call)
|
|
101
|
+
session.initialize(timeout=timeout)
|
|
102
|
+
return session
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GraphSession(ApiSession):
|
|
106
|
+
"""The MCP session's surface (client.Session) over Microsoft Graph."""
|
|
107
|
+
|
|
108
|
+
NAME, TOOLS = NAME, TOOLS
|
|
109
|
+
|
|
110
|
+
def _api(self, method, path, deadline, query=None, body=None, headers=None):
|
|
111
|
+
"""Graph's JSON answer ({} for a 2xx with none) or a ``Refusal``. Raises AuthNeeded, NotSent and, for a write,
|
|
112
|
+
OutcomeUnknown (a 5xx or no answer: it may have happened, and it's never sent again)."""
|
|
113
|
+
if not self.token:
|
|
114
|
+
raise mcp.AuthNeeded(f"{self.name} isn't signed in.")
|
|
115
|
+
write = method != "GET"
|
|
116
|
+
url = f"{API}{path}" + (f"?{urlencode(query, quote_via=quote)}" if query else "")
|
|
117
|
+
send = {"Authorization": f"Bearer {self.token}", "Accept": "application/json", "User-Agent": webhook.USER_AGENT,
|
|
118
|
+
**(headers or {})}
|
|
119
|
+
data = None
|
|
120
|
+
if body is not None:
|
|
121
|
+
data = json.dumps(body, ensure_ascii=False).encode()
|
|
122
|
+
send["Content-Type"] = "application/json"
|
|
123
|
+
try:
|
|
124
|
+
reply = self.call(method, url, data, send, timeout=max(1.0, deadline - time.monotonic()),
|
|
125
|
+
read_limit=READ_LIMIT)
|
|
126
|
+
except net.NetError as error:
|
|
127
|
+
if error.unsent:
|
|
128
|
+
raise mcp.NotSent(error.message) from None
|
|
129
|
+
if write:
|
|
130
|
+
raise mcp.OutcomeUnknown(f"{error.message} It may have gone through: check Microsoft 365 before you do "
|
|
131
|
+
"it again.") from None
|
|
132
|
+
raise mcp.OutcomeUnknown(error.message) from None
|
|
133
|
+
if reply.status == 429:
|
|
134
|
+
wait = reply.retry_after if str(reply.retry_after or "").isdigit() else "a few"
|
|
135
|
+
return Refusal(f"Microsoft asked Mobster to wait {wait} seconds. Try again then.")
|
|
136
|
+
if reply.status >= 500:
|
|
137
|
+
if write:
|
|
138
|
+
raise mcp.OutcomeUnknown(f"Microsoft answered {reply.status}. It may have gone through: check "
|
|
139
|
+
"Microsoft 365 before you do it again.")
|
|
140
|
+
return Refusal(f"Microsoft answered {reply.status}. Try again in a minute.")
|
|
141
|
+
try:
|
|
142
|
+
answer = json.loads(reply.body.decode("utf-8")) if reply.body else {}
|
|
143
|
+
except (ValueError, UnicodeDecodeError):
|
|
144
|
+
answer = None
|
|
145
|
+
if 200 <= reply.status < 300:
|
|
146
|
+
if isinstance(answer, dict):
|
|
147
|
+
return answer
|
|
148
|
+
# A write Microsoft answered 2xx happened, whatever its body says; a read has nothing to show.
|
|
149
|
+
return {} if write else Refusal("Microsoft's answer couldn't be read. Try again in a minute.")
|
|
150
|
+
error = answer.get("error") if isinstance(answer, dict) else None
|
|
151
|
+
code = str(error.get("code") if isinstance(error, dict) else "")[:60]
|
|
152
|
+
if reply.status == 401:
|
|
153
|
+
raise mcp.AuthNeeded(f"Microsoft signed Mobster out ({code or 401}). Sign in to {self.name} again in "
|
|
154
|
+
"Settings › Connectors.")
|
|
155
|
+
if reply.status == 403 or code in DENIED:
|
|
156
|
+
return Refusal(f"Microsoft refused ({code or 403}). Your {self.name} sign-in can't do this: sign in again "
|
|
157
|
+
f"in Settings › Connectors and allow what Microsoft asks, or ask your admin.")
|
|
158
|
+
if code in REFUSED:
|
|
159
|
+
return Refusal(f"Microsoft refused ({code}). {REFUSED[code]}.")
|
|
160
|
+
said = re.sub(r"\s+", " ", str(error.get("message") or "") if isinstance(error, dict) else "")[:200]
|
|
161
|
+
return Refusal(f"Microsoft refused ({code or reply.status})." + (f" {said}" if said else ""))
|
|
162
|
+
|
|
163
|
+
def initialize(self, timeout=mcp.CONNECT_TIMEOUT):
|
|
164
|
+
"""GET /me: the token works (it needs only User.Read). AuthNeeded or NotSent when it doesn't."""
|
|
165
|
+
answer = self._api("GET", "/me", time.monotonic() + timeout, {"$select": "displayName,userPrincipalName"})
|
|
166
|
+
if isinstance(answer, Refusal):
|
|
167
|
+
raise mcp.NotSent(answer.message)
|
|
168
|
+
self.server_info = {"name": NAME, "user": str(answer.get("userPrincipalName") or "")[:100]}
|
|
169
|
+
self.ready = True
|
|
170
|
+
return {"serverInfo": self.server_info}
|
|
171
|
+
|
|
172
|
+
def ping(self, timeout=mcp.PING_TIMEOUT):
|
|
173
|
+
if not self.ready:
|
|
174
|
+
raise mcp.NotSent(f"{self.name} isn't connected.")
|
|
175
|
+
answer = self._api("GET", "/me", time.monotonic() + timeout, {"$select": "id"})
|
|
176
|
+
if isinstance(answer, Refusal):
|
|
177
|
+
raise mcp.NotSent(answer.message)
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
def _list_mail(self, args, deadline):
|
|
181
|
+
top = _limit(args.get("limit"), 10)
|
|
182
|
+
select = "id,subject,from,receivedDateTime,isRead,bodyPreview,webLink"
|
|
183
|
+
query = args.get("query")
|
|
184
|
+
if query is not None:
|
|
185
|
+
words = " ".join(required_text(query, "query", 300).replace('"', " ").split())
|
|
186
|
+
answer = self._api("GET", "/me/messages", deadline,
|
|
187
|
+
{"$search": f'"{words}"', "$top": top, "$select": select})
|
|
188
|
+
else:
|
|
189
|
+
folder = _choice(args.get("folder"), FOLDERS, "folder")
|
|
190
|
+
answer = self._api("GET", f"/me/mailFolders/{folder}/messages", deadline,
|
|
191
|
+
{"$top": top, "$orderby": "receivedDateTime desc", "$select": select})
|
|
192
|
+
if isinstance(answer, Refusal):
|
|
193
|
+
return answer
|
|
194
|
+
return {"messages": [{"id": short(m.get("id"), 512), "subject": short(m.get("subject"), 300),
|
|
195
|
+
"from": _address(m.get("from")), "received": short(m.get("receivedDateTime"), 40),
|
|
196
|
+
"read": bool(m.get("isRead")), "preview": short(m.get("bodyPreview"), 300),
|
|
197
|
+
"link": short(m.get("webLink"), 500)}
|
|
198
|
+
for m in _items(answer)],
|
|
199
|
+
"more": "@odata.nextLink" in answer}
|
|
200
|
+
|
|
201
|
+
def _read_message(self, args, deadline):
|
|
202
|
+
ident = _id(args.get("id"), "id")
|
|
203
|
+
answer = self._api("GET", f"/me/messages/{quote(ident, safe='')}", deadline,
|
|
204
|
+
{"$select": "id,subject,from,toRecipients,ccRecipients,receivedDateTime,body,webLink"},
|
|
205
|
+
headers={"Prefer": 'outlook.body-content-type="text"'})
|
|
206
|
+
if isinstance(answer, Refusal):
|
|
207
|
+
return answer
|
|
208
|
+
body = answer.get("body") if isinstance(answer.get("body"), dict) else {}
|
|
209
|
+
return {"id": short(answer.get("id"), 512), "subject": short(answer.get("subject"), 300),
|
|
210
|
+
"from": _address(answer.get("from")), "to": [_address(r) for r in answer.get("toRecipients") or ()],
|
|
211
|
+
"cc": [_address(r) for r in answer.get("ccRecipients") or ()],
|
|
212
|
+
"received": short(answer.get("receivedDateTime"), 40), "text": short(body.get("content"), BODY_CHARS),
|
|
213
|
+
"link": short(answer.get("webLink"), 500)}
|
|
214
|
+
|
|
215
|
+
def _list_events(self, args, deadline):
|
|
216
|
+
start = _when(args.get("start"), "start") or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
217
|
+
end = _when(args.get("end"), "end")
|
|
218
|
+
if end is None:
|
|
219
|
+
first = datetime.fromisoformat(_utc(start).replace("Z", "+00:00"))
|
|
220
|
+
end = (first + timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
221
|
+
answer = self._api("GET", "/me/calendarView", deadline,
|
|
222
|
+
{"startDateTime": _utc(start), "endDateTime": _utc(end),
|
|
223
|
+
"$top": _limit(args.get("limit"), 25), "$orderby": "start/dateTime",
|
|
224
|
+
"$select": "id,subject,start,end,location,organizer,attendees,webLink,isCancelled"},
|
|
225
|
+
headers={"Prefer": 'outlook.timezone="UTC"'})
|
|
226
|
+
if isinstance(answer, Refusal):
|
|
227
|
+
return answer
|
|
228
|
+
return {"events": [{"id": short(e.get("id"), 512), "subject": short(e.get("subject"), 300),
|
|
229
|
+
"start": short((e.get("start") or {}).get("dateTime"), 40),
|
|
230
|
+
"end": short((e.get("end") or {}).get("dateTime"), 40),
|
|
231
|
+
"place": short((e.get("location") or {}).get("displayName"), 200),
|
|
232
|
+
"organizer": _address(e.get("organizer")),
|
|
233
|
+
"attendees": [_address(a) for a in (e.get("attendees") or ())[:20]],
|
|
234
|
+
"cancelled": bool(e.get("isCancelled")), "link": short(e.get("webLink"), 500)}
|
|
235
|
+
for e in _items(answer)],
|
|
236
|
+
"more": "@odata.nextLink" in answer}
|
|
237
|
+
|
|
238
|
+
def _search_files(self, args, deadline):
|
|
239
|
+
words = required_text(args.get("query"), "query", 300).replace("'", "''")
|
|
240
|
+
answer = self._api("GET", f"/me/drive/root/search(q='{quote(words, safe='')}')", deadline,
|
|
241
|
+
{"$top": _limit(args.get("limit"), 10),
|
|
242
|
+
"$select": "id,name,webUrl,size,lastModifiedDateTime,file,folder"})
|
|
243
|
+
if isinstance(answer, Refusal):
|
|
244
|
+
return answer
|
|
245
|
+
return {"files": [{"id": short(f.get("id"), 512), "name": short(f.get("name"), 300),
|
|
246
|
+
"kind": "folder" if f.get("folder") else "file", "size": f.get("size")
|
|
247
|
+
if isinstance(f.get("size"), int) else None,
|
|
248
|
+
"modified": short(f.get("lastModifiedDateTime"), 40), "link": short(f.get("webUrl"), 500)}
|
|
249
|
+
for f in _items(answer)],
|
|
250
|
+
"more": "@odata.nextLink" in answer}
|
|
251
|
+
|
|
252
|
+
def _list_contacts(self, args, deadline):
|
|
253
|
+
query = {"$top": _limit(args.get("limit"), 25),
|
|
254
|
+
"$select": "id,displayName,emailAddresses,companyName,mobilePhone"}
|
|
255
|
+
if args.get("query") is not None:
|
|
256
|
+
query["$search"] = '"' + " ".join(
|
|
257
|
+
required_text(args["query"], "query", 200).replace('"', " ").split()) + '"'
|
|
258
|
+
answer = self._api("GET", "/me/contacts", deadline, query)
|
|
259
|
+
if isinstance(answer, Refusal):
|
|
260
|
+
return answer
|
|
261
|
+
return {"contacts": [{"id": short(c.get("id"), 512), "name": short(c.get("displayName"), 200),
|
|
262
|
+
"emails": [short(a.get("address"), 200) for a in (c.get("emailAddresses") or ())[:5]
|
|
263
|
+
if isinstance(a, dict)],
|
|
264
|
+
"company": short(c.get("companyName"), 200), "phone": short(c.get("mobilePhone"), 40)}
|
|
265
|
+
for c in _items(answer)],
|
|
266
|
+
"more": "@odata.nextLink" in answer}
|
|
267
|
+
|
|
268
|
+
def _list_chats(self, args, deadline):
|
|
269
|
+
answer = self._api("GET", "/me/chats", deadline, {"$top": _limit(args.get("limit"), 20),
|
|
270
|
+
"$orderby": "lastMessagePreview/createdDateTime desc",
|
|
271
|
+
"$select": "id,topic,chatType,lastUpdatedDateTime"})
|
|
272
|
+
if isinstance(answer, Refusal):
|
|
273
|
+
return answer
|
|
274
|
+
return {"chats": [{"id": short(c.get("id"), 512), "topic": short(c.get("topic"), 200),
|
|
275
|
+
"kind": short(c.get("chatType"), 20), "updated": short(c.get("lastUpdatedDateTime"), 40)}
|
|
276
|
+
for c in _items(answer)],
|
|
277
|
+
"more": "@odata.nextLink" in answer}
|
|
278
|
+
|
|
279
|
+
def _read_chat(self, args, deadline):
|
|
280
|
+
chat = _id(args.get("chat_id"), "chat_id")
|
|
281
|
+
answer = self._api("GET", f"/me/chats/{quote(chat, safe='')}/messages", deadline,
|
|
282
|
+
{"$top": _limit(args.get("limit"), 20)})
|
|
283
|
+
if isinstance(answer, Refusal):
|
|
284
|
+
return answer
|
|
285
|
+
found = []
|
|
286
|
+
for m in _items(answer):
|
|
287
|
+
sender = (((m.get("from") or {}).get("user") or {}).get("displayName") if isinstance(m.get("from"), dict)
|
|
288
|
+
else None)
|
|
289
|
+
body = m.get("body") if isinstance(m.get("body"), dict) else {}
|
|
290
|
+
found.append({"id": short(m.get("id"), 80), "from": short(sender, 200),
|
|
291
|
+
"time": short(m.get("createdDateTime"), 40),
|
|
292
|
+
"text": short(re.sub(r"<[^>]+>", "", str(body.get("content") or ""))
|
|
293
|
+
if body.get("contentType") == "html"
|
|
294
|
+
else body.get("content"), TEXT_CHARS)})
|
|
295
|
+
return {"chatId": chat, "messages": found, "more": "@odata.nextLink" in answer}
|
|
296
|
+
|
|
297
|
+
def _send_mail(self, args, deadline):
|
|
298
|
+
message = {"subject": required_text(args.get("subject"), "subject", 300),
|
|
299
|
+
"body": {"contentType": "Text", "content": required_text(args.get("body"), "body", 100_000)},
|
|
300
|
+
"toRecipients": _recipients(args.get("to"), "to", required=True)}
|
|
301
|
+
copied = _recipients(args.get("cc"), "cc")
|
|
302
|
+
if copied:
|
|
303
|
+
message["ccRecipients"] = copied
|
|
304
|
+
answer = self._api("POST", "/me/sendMail", deadline, body={"message": message, "saveToSentItems": True})
|
|
305
|
+
return answer if isinstance(answer, Refusal) else {"sent": True}
|
|
306
|
+
|
|
307
|
+
def _create_event(self, args, deadline):
|
|
308
|
+
zone = args.get("timezone", "UTC")
|
|
309
|
+
if not isinstance(zone, str) or not ZONE.fullmatch(zone):
|
|
310
|
+
raise ValueError("timezone is a time zone name such as Europe/London")
|
|
311
|
+
event = {"subject": required_text(args.get("subject"), "subject", 300),
|
|
312
|
+
"start": {"dateTime": _local(args.get("start"), "start"), "timeZone": zone},
|
|
313
|
+
"end": {"dateTime": _local(args.get("end"), "end"), "timeZone": zone}}
|
|
314
|
+
if args.get("location") is not None:
|
|
315
|
+
event["location"] = {"displayName": required_text(args["location"], "location", 200)}
|
|
316
|
+
if args.get("body") is not None:
|
|
317
|
+
event["body"] = {"contentType": "Text", "content": required_text(args["body"], "body", 20_000)}
|
|
318
|
+
guests = _recipients(args.get("attendees"), "attendees")
|
|
319
|
+
if guests:
|
|
320
|
+
event["attendees"] = [{**g, "type": "required"} for g in guests]
|
|
321
|
+
answer = self._api("POST", "/me/events", deadline, body=event)
|
|
322
|
+
return answer if isinstance(answer, Refusal) else {"created": True, "id": short(answer.get("id"), 512),
|
|
323
|
+
"link": short(answer.get("webLink"), 500)}
|
|
324
|
+
|
|
325
|
+
def _send_chat_message(self, args, deadline):
|
|
326
|
+
chat = _id(args.get("chat_id"), "chat_id")
|
|
327
|
+
text = required_text(args.get("text"), "text", SEND_CHARS)
|
|
328
|
+
answer = self._api("POST", f"/me/chats/{quote(chat, safe='')}/messages", deadline,
|
|
329
|
+
body={"body": {"contentType": "text", "content": text}})
|
|
330
|
+
return (answer if isinstance(answer, Refusal)
|
|
331
|
+
else {"posted": True, "chatId": chat, "id": short(answer.get("id"), 80)})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _items(answer):
|
|
335
|
+
return [item for item in answer.get("value") or () if isinstance(item, dict)]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _address(value):
|
|
339
|
+
box = value.get("emailAddress") if isinstance(value, dict) else None
|
|
340
|
+
if not isinstance(box, dict):
|
|
341
|
+
return None
|
|
342
|
+
return {"name": short(box.get("name"), 200), "address": short(box.get("address"), 200)}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _id(value, field):
|
|
346
|
+
text = required_text(value, field, 512)
|
|
347
|
+
if not ID.fullmatch(text):
|
|
348
|
+
raise ValueError(f"{field} is an id that a list or a search gave")
|
|
349
|
+
return text
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _limit(value, default):
|
|
353
|
+
if value is None:
|
|
354
|
+
return default
|
|
355
|
+
if type(value) is not int or not 1 <= value <= LIMIT_TOP:
|
|
356
|
+
raise ValueError(f"limit is a whole number from 1 to {LIMIT_TOP}")
|
|
357
|
+
return value
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _choice(value, choices, field):
|
|
361
|
+
if value is None:
|
|
362
|
+
return choices[0]
|
|
363
|
+
if value not in choices:
|
|
364
|
+
raise ValueError(f"{field} is {' or '.join(choices)}")
|
|
365
|
+
return value
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _recipients(value, field, required=False):
|
|
369
|
+
if value is None and not required:
|
|
370
|
+
return []
|
|
371
|
+
if not isinstance(value, list) or not value or len(value) > 20 or not all(isinstance(a, str) for a in value):
|
|
372
|
+
raise ValueError(f"{field} is a list of 1 to 20 email addresses")
|
|
373
|
+
found = [a.strip() for a in value]
|
|
374
|
+
if not all(EMAIL.fullmatch(a) for a in found):
|
|
375
|
+
raise ValueError(f"{field} has an address that isn't an email address")
|
|
376
|
+
return [{"emailAddress": {"address": a}} for a in found]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _when(value, field):
|
|
380
|
+
if value is None:
|
|
381
|
+
return None
|
|
382
|
+
if not isinstance(value, str) or not WHEN.fullmatch(value.strip()):
|
|
383
|
+
raise ValueError(f"{field} is a date or a date and time, such as 2026-10-20T14:00:00Z")
|
|
384
|
+
return value.strip()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _utc(value):
|
|
388
|
+
"""A date or time as Graph's calendarView wants it: with a zone (UTC when it has none)."""
|
|
389
|
+
text = value if len(value) > 10 else value + "T00:00:00"
|
|
390
|
+
return text if re.search(r"(Z|[+-]\d{2}:\d{2})$", text) else text + "Z"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _local(value, field):
|
|
394
|
+
if not isinstance(value, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?", value.strip()):
|
|
395
|
+
raise ValueError(f"{field} is a local time such as 2026-10-20T14:00:00")
|
|
396
|
+
return value.strip()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Import the MCP servers you already set up in Claude Desktop, Claude Code, Cursor or Codex: Settings ›
|
|
2
|
+
Connectors lists each one with an Add button (GET /api/connectors/imports), and Add makes it a connector (POST
|
|
3
|
+
/api/connectors/imports) without retyping its command, URL or keys."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..integrations import clients as agent_clients
|
|
10
|
+
from ..integrations import jsonc, textedit
|
|
11
|
+
from ..secret_filter import redact
|
|
12
|
+
from . import client as mcp
|
|
13
|
+
|
|
14
|
+
SOURCES = (("claude-desktop", agent_clients.ClaudeDesktop), ("claude-code", agent_clients.ClaudeCode),
|
|
15
|
+
("cursor", agent_clients.Cursor), ("codex", agent_clients.Codex))
|
|
16
|
+
MAX_SERVERS = 60 # per file
|
|
17
|
+
MAX_BYTES = 4_000_000 # a config larger than this is skipped (~/.claude.json can grow; it holds history too)
|
|
18
|
+
SELF = ("mobster", "mobster-agent")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read(path):
|
|
22
|
+
try:
|
|
23
|
+
if not path.is_file() or path.stat().st_size > MAX_BYTES:
|
|
24
|
+
return None
|
|
25
|
+
return path.read_text(encoding="utf-8")
|
|
26
|
+
except (OSError, UnicodeDecodeError):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _servers(client, path):
|
|
31
|
+
"""{name: entry} from one app's file, or {} when it has none or can't be read."""
|
|
32
|
+
text = _read(path)
|
|
33
|
+
if text is None:
|
|
34
|
+
return {}
|
|
35
|
+
try:
|
|
36
|
+
if isinstance(client, agent_clients.Codex):
|
|
37
|
+
servers = textedit.toml_loads(text).get("mcp_servers")
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
data = json.loads(text) # fast for a large ~/.claude.json
|
|
41
|
+
except ValueError:
|
|
42
|
+
data = jsonc.loads(text) # comments and trailing commas (Cursor allows them)
|
|
43
|
+
servers = data
|
|
44
|
+
for key in client.container:
|
|
45
|
+
servers = servers.get(key) if isinstance(servers, dict) else None
|
|
46
|
+
except (ValueError, jsonc.JsoncError, textedit.EditError):
|
|
47
|
+
return {}
|
|
48
|
+
if not isinstance(servers, dict):
|
|
49
|
+
return {}
|
|
50
|
+
return {str(k): v for k, v in list(servers.items())[:MAX_SERVERS] if isinstance(v, dict)}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_mobster(name, entry):
|
|
54
|
+
if name.casefold() in SELF:
|
|
55
|
+
return True
|
|
56
|
+
command = entry.get("command")
|
|
57
|
+
if isinstance(command, str) and os.path.basename(command) in SELF:
|
|
58
|
+
return True
|
|
59
|
+
args = entry.get("args") if isinstance(entry.get("args"), list) else []
|
|
60
|
+
return "mobile_agent" in args and "mcp" in args
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _token_of(entry):
|
|
64
|
+
"""A bearer token from an entry's headers (Authorization: Bearer …), else None; and whether other headers were left
|
|
65
|
+
out."""
|
|
66
|
+
headers = entry.get("headers") if isinstance(entry.get("headers"), dict) else {}
|
|
67
|
+
token, others = None, False
|
|
68
|
+
for key, value in headers.items():
|
|
69
|
+
if str(key).casefold() == "authorization" and isinstance(value, str) and value.startswith("Bearer "):
|
|
70
|
+
token = value[7:].strip() or None
|
|
71
|
+
else:
|
|
72
|
+
others = True
|
|
73
|
+
if isinstance(entry.get("bearer_token"), str): # Codex's inline form
|
|
74
|
+
token = entry["bearer_token"].strip() or token
|
|
75
|
+
return token, others
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def describe(entry):
|
|
79
|
+
"""What Add would make of ``entry``: {"transport", "command", "args", "url", "envNames", "token", "headers",
|
|
80
|
+
"problem"}."""
|
|
81
|
+
command, url = entry.get("command"), entry.get("url") or entry.get("serverUrl")
|
|
82
|
+
kind = str(entry.get("type") or entry.get("transport") or "").casefold()
|
|
83
|
+
out = {"transport": None, "command": None, "args": [], "url": None, "envNames": [], "token": False,
|
|
84
|
+
"headers": False, "problem": None}
|
|
85
|
+
if isinstance(command, str) and command.strip():
|
|
86
|
+
args = entry.get("args") if isinstance(entry.get("args"), list) else []
|
|
87
|
+
env = entry.get("env") if isinstance(entry.get("env"), dict) else {}
|
|
88
|
+
out.update(transport="stdio", command=command.strip(), args=[str(a) for a in args],
|
|
89
|
+
envNames=sorted(str(k) for k in env))
|
|
90
|
+
return out
|
|
91
|
+
if isinstance(url, str) and url.strip():
|
|
92
|
+
out["url"] = url.strip()
|
|
93
|
+
if kind == "sse" or url.rstrip("/").endswith("/sse"):
|
|
94
|
+
out["problem"] = "It uses SSE, an older MCP transport Mobster doesn't speak. Use the server's /mcp URL."
|
|
95
|
+
return out
|
|
96
|
+
try:
|
|
97
|
+
mcp.validate_url(url)
|
|
98
|
+
except ValueError as error:
|
|
99
|
+
out["problem"] = f"{error}."
|
|
100
|
+
return out
|
|
101
|
+
token, others = _token_of(entry)
|
|
102
|
+
out.update(transport="http", token=bool(token), headers=others)
|
|
103
|
+
return out
|
|
104
|
+
out["problem"] = "It has no command or URL Mobster can use."
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def found(env=None):
|
|
109
|
+
"""Every server the other apps have, as (source id, source name, name, entry) tuples, Mobster left out."""
|
|
110
|
+
env = env or agent_clients.Env.current()
|
|
111
|
+
out = []
|
|
112
|
+
for source, cls in SOURCES:
|
|
113
|
+
client = cls()
|
|
114
|
+
for path in client.paths(env):
|
|
115
|
+
for name, entry in _servers(client, Path(path)).items():
|
|
116
|
+
if not _is_mobster(name, entry):
|
|
117
|
+
out.append((source, client.name, name, entry))
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def public(found_servers, connectors):
|
|
122
|
+
"""The list for GET /api/connectors/imports. ``connectors``: the existing rows (to say which are added). It never
|
|
123
|
+
carries a value: env values and tokens stay on this Mac and go straight to the Keychain on Add."""
|
|
124
|
+
rows = []
|
|
125
|
+
for source, source_name, name, entry in found_servers:
|
|
126
|
+
info = describe(entry)
|
|
127
|
+
added = next((c for c in connectors if (info["transport"] == "stdio" and c["transport"] == "stdio" and
|
|
128
|
+
os.path.basename(c.get("command") or "") ==
|
|
129
|
+
os.path.basename(info["command"] or "") and
|
|
130
|
+
list(c.get("args") or []) == info["args"])
|
|
131
|
+
or (info["transport"] == "http" and c.get("url") and info["url"] and
|
|
132
|
+
mcp.hint(c["url"]) == mcp.hint(info["url"]))), None)
|
|
133
|
+
rows.append({
|
|
134
|
+
"source": source, "sourceName": source_name, "key": name[:80], "name": suggested_name(name),
|
|
135
|
+
"transport": info["transport"],
|
|
136
|
+
"command": os.path.basename(info["command"]) if info["command"] else None,
|
|
137
|
+
"args": [redact(a)[:200] for a in info["args"][:40]],
|
|
138
|
+
"url": mcp.hint(info["url"]) if info["url"] and not info["problem"] else None,
|
|
139
|
+
"envNames": info["envNames"][:20], "token": info["token"], "headers": info["headers"],
|
|
140
|
+
"problem": info["problem"], "added": added["id"] if added else None})
|
|
141
|
+
return rows
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def suggested_name(key):
|
|
145
|
+
"""A connector name from a config's key: "brave-search" stays, cut to 40 characters."""
|
|
146
|
+
text = " ".join(str(key).split()) or "Server"
|
|
147
|
+
return text[:40]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def body_for(found_servers, source, key, name=None):
|
|
151
|
+
"""The POST /api/connectors body for one found server, with its secrets (env values, a header's bearer token) read
|
|
152
|
+
now from the app's own file."""
|
|
153
|
+
match = next((entry for src, _, k, entry in found_servers if src == source and k == key), None)
|
|
154
|
+
if match is None:
|
|
155
|
+
raise ValueError("That server isn't in the app's settings any more. Look again")
|
|
156
|
+
info = describe(match)
|
|
157
|
+
if info["problem"]:
|
|
158
|
+
raise ValueError(info["problem"].rstrip("."))
|
|
159
|
+
body = {"name": name or suggested_name(key)}
|
|
160
|
+
if info["transport"] == "stdio":
|
|
161
|
+
env = match.get("env") if isinstance(match.get("env"), dict) else {}
|
|
162
|
+
body.update(command=info["command"], args=info["args"])
|
|
163
|
+
if env:
|
|
164
|
+
body["env"] = {str(k): str(v) for k, v in env.items()}
|
|
165
|
+
else:
|
|
166
|
+
body["url"] = info["url"]
|
|
167
|
+
token, _ = _token_of(match)
|
|
168
|
+
if token:
|
|
169
|
+
body["token"] = token
|
|
170
|
+
return body
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""File a finished result as an issue through a connected tracker (GitHub, Linear or Jira): one tool call of the
|
|
2
|
+
connector the person already connected, through the same ConnectorService.call a workflow's connector step uses."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# preset -> [(tool, the arguments for it)], the first tool the connector offers wins. ``target`` is what only the
|
|
7
|
+
# person knows (GitHub's owner and repo, Linear's team, Jira's cloudId and projectKey); the title and text are ours.
|
|
8
|
+
TRACKERS = {
|
|
9
|
+
"github": (("issue_write", lambda t, title, text: {"method": "create", **t, "title": title, "body": text}),
|
|
10
|
+
("create_issue", lambda t, title, text: {**t, "title": title, "body": text})),
|
|
11
|
+
"linear": (("save_issue", lambda t, title, text: {**t, "title": title, "description": text}),
|
|
12
|
+
("create_issue", lambda t, title, text: {**t, "title": title, "description": text})),
|
|
13
|
+
"jira": (("createJiraIssue", lambda t, title, text: {"issueTypeName": "Bug", **t, "summary": title,
|
|
14
|
+
"description": text}),),
|
|
15
|
+
}
|
|
16
|
+
TARGET_KEYS = {"owner", "repo", "team", "teamId", "project", "projectId", "cloudId", "projectKey", "issueTypeName",
|
|
17
|
+
"labels"}
|
|
18
|
+
URL = re.compile(r"https://[^\s\"'<>)]+")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def trackers(rows):
|
|
22
|
+
"""The enabled connectors that are one of the trackers, in the order they were added."""
|
|
23
|
+
return [row for row in rows if row.get("enabled") and row.get("preset") in TRACKERS]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pick(rows, wanted):
|
|
27
|
+
"""The tracker connector ``wanted`` names (its id, name or preset), else the only one connected.
|
|
28
|
+
Returns (row, None) or (None, (code, message)) when no suitable one is connected."""
|
|
29
|
+
found = trackers(rows)
|
|
30
|
+
if not found:
|
|
31
|
+
return None, ("no_tracker", "No issue tracker is connected. Connect GitHub, Linear or Jira in Settings › "
|
|
32
|
+
"Connectors, then file the issue.")
|
|
33
|
+
if wanted:
|
|
34
|
+
wanted = str(wanted).lower()
|
|
35
|
+
found = [row for row in found if wanted in (row["id"].lower(), row["name"].lower(), row["preset"])]
|
|
36
|
+
if not found:
|
|
37
|
+
return None, ("no_tracker", f"{wanted} isn't a connected issue tracker. Pick one that is connected.")
|
|
38
|
+
if len(found) > 1:
|
|
39
|
+
return None, ("tracker_ambiguous", "Several issue trackers are connected: " +
|
|
40
|
+
", ".join(row["name"] for row in found) + ". Name the one to use.")
|
|
41
|
+
return found[0], None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def tool_for(row, target, title, text):
|
|
45
|
+
"""(tool, arguments) for the first create-issue tool the connector offers, else None."""
|
|
46
|
+
offered = {tool["name"] for tool in row.get("tools") or ()}
|
|
47
|
+
for tool, build in TRACKERS[row["preset"]]:
|
|
48
|
+
if tool in offered:
|
|
49
|
+
return tool, build(target, title, text)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def clean_target(value):
|
|
54
|
+
if value is None:
|
|
55
|
+
return {}
|
|
56
|
+
if not isinstance(value, dict) or set(value) - TARGET_KEYS:
|
|
57
|
+
raise ValueError("target holds only " + ", ".join(sorted(TARGET_KEYS)))
|
|
58
|
+
if not all(isinstance(item, (str, list)) for item in value.values()):
|
|
59
|
+
raise ValueError("Every target value is text (labels is a list)")
|
|
60
|
+
return dict(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def link_of(text):
|
|
64
|
+
"""The first https link in the tracker's answer (the issue's address), or None."""
|
|
65
|
+
found = URL.search(text or "")
|
|
66
|
+
return found.group(0).rstrip(".,") if found else None
|