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,740 @@
|
|
|
1
|
+
"""`mobster chat`: talk to Mobster's agent in a conversation, from Terminal."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import queue
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..narrate import approval_title, verbs
|
|
12
|
+
from .chat_once import EXIT_COULDNT, EXIT_DONE, once, proposals_of, routed_as, where_to_remember, workflow_once
|
|
13
|
+
from .client import NO_THREADS, NOT_RUNNING, Client, NoService, ServiceError
|
|
14
|
+
from .recent import last_thread, remember_thread
|
|
15
|
+
from .term import Term, isatty
|
|
16
|
+
|
|
17
|
+
ATTACH_MAX = 25 * 1024 * 1024
|
|
18
|
+
HELP = ("/new start a new conversation /threads your recent conversations\n"
|
|
19
|
+
"/open ID continue one /stop stop the task /pause pause or continue it\n"
|
|
20
|
+
"/attach PATH add a file to your next message /quit leave (the task goes on)")
|
|
21
|
+
# A route the running Mobster doesn't have (404).
|
|
22
|
+
NO_PAUSE = "This Mobster can't pause a task yet."
|
|
23
|
+
NO_FILES = "This Mobster can't take files yet."
|
|
24
|
+
DECISIONS = {"approved": "Approved", "denied": "Declined", "redirected": "Declined; Mobster does what you said "
|
|
25
|
+
"instead", "timeout": "Nobody answered in time, so Mobster didn't do it", "stopped": "Stopped",
|
|
26
|
+
"answered": "Answered"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_arguments(parser, helpers):
|
|
30
|
+
bounded = helpers.get("bounded")
|
|
31
|
+
parser.add_argument("message", nargs="*", metavar="MESSAGE",
|
|
32
|
+
help="what to ask Mobster's agent; leave it out for a conversation in this terminal")
|
|
33
|
+
which = parser.add_mutually_exclusive_group()
|
|
34
|
+
which.add_argument("--thread", metavar="ID", help="continue this conversation (default: the one this terminal "
|
|
35
|
+
"used last)")
|
|
36
|
+
which.add_argument("--new", action="store_true", help="start a new conversation")
|
|
37
|
+
parser.add_argument("--device", metavar="NAME",
|
|
38
|
+
help="the iPhone or simulator: an id, UDID or name (default: the conversation's)")
|
|
39
|
+
parser.add_argument("--json", action="store_true",
|
|
40
|
+
help="print one JSON object when the task finishes or needs you; never answers an approval")
|
|
41
|
+
parser.add_argument("--keep-waiting", action="store_true",
|
|
42
|
+
help="with --json and the app or mobster serve running: wait while the task waits for you, "
|
|
43
|
+
"until it ends or --wait runs out")
|
|
44
|
+
parser.add_argument("--wait", type=bounded(float, 0, 3600) if bounded else float, default=None,
|
|
45
|
+
metavar="SECONDS", help="with the app or mobster serve running: how long to follow a task "
|
|
46
|
+
"(default 300); it goes on after")
|
|
47
|
+
parser.add_argument("--url", metavar="URL", help="Mobster's address (default: $MOBSTER_URL, else the running "
|
|
48
|
+
"Mac app's, else http://127.0.0.1:8765)")
|
|
49
|
+
parser.add_argument("--workflow", metavar="NAME", help=(
|
|
50
|
+
"run a saved workflow (its name or id) instead of a "
|
|
51
|
+
"message: in a new conversation, its own one, or --thread ID"))
|
|
52
|
+
parser.add_argument("--set", action="append", default=[], metavar="BLANK=VALUE",
|
|
53
|
+
help="with --workflow: a value for one of its blanks; repeat it for each")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Chat:
|
|
57
|
+
def __init__(self, client, term, *, thread_id=None, device=None, color=None, interactive=True):
|
|
58
|
+
from ..style import Palette, color_enabled
|
|
59
|
+
self.client, self.term = client, term
|
|
60
|
+
self.thread_id, self.device = thread_id, device
|
|
61
|
+
self.paint = Palette(color_enabled(term.stdout) if color is None else color)
|
|
62
|
+
self.interactive = interactive
|
|
63
|
+
self.events = queue.Queue()
|
|
64
|
+
self.run_id = None # the task being followed
|
|
65
|
+
self.pending = None # the run's approval or question, as GET /api/runs/{id} shows it
|
|
66
|
+
self.paused = False
|
|
67
|
+
self.attachments = [] # [(id, name)] for the next message
|
|
68
|
+
self.renderers = {}
|
|
69
|
+
self.outcomes = set() # runs whose outcome is printed
|
|
70
|
+
self.finished = {} # run id -> the run, once finished
|
|
71
|
+
self.followers = {}
|
|
72
|
+
self.quit = False
|
|
73
|
+
self.held = "" # the line you were typing when an approval came; back once it's answered
|
|
74
|
+
self.offers = [] # suggestions to remember that wait for y or n here, oldest first
|
|
75
|
+
self.escape = None # inside a terminal key sequence (an arrow key): None, "esc", "csi" or "ss3"
|
|
76
|
+
|
|
77
|
+
# -- printing
|
|
78
|
+
|
|
79
|
+
def say(self, text, *roles):
|
|
80
|
+
self.term.print(self.paint(text, *roles) if roles else text)
|
|
81
|
+
|
|
82
|
+
def note(self, text, glyph="!", role="amber", indent=" "):
|
|
83
|
+
"""A line whose glyph carries the colour and whose words stay the terminal's own colour, so it reads on
|
|
84
|
+
light and dark profiles alike."""
|
|
85
|
+
self.term.print(indent + self.paint(glyph, role, "bold") + " " + text)
|
|
86
|
+
|
|
87
|
+
def keys(self, *pairs):
|
|
88
|
+
""""[y] Send [n] Don't send" with each key in bold."""
|
|
89
|
+
return " ".join(self.paint(f"[{key}]", "bold") + f" {label}" for key, label in pairs)
|
|
90
|
+
|
|
91
|
+
def renderer(self, run_id):
|
|
92
|
+
renderer = self.renderers.get(run_id)
|
|
93
|
+
if renderer is None:
|
|
94
|
+
from ..console import ConsoleRenderer
|
|
95
|
+
term = self.term
|
|
96
|
+
|
|
97
|
+
class Lines(ConsoleRenderer):
|
|
98
|
+
def line(self, text=""):
|
|
99
|
+
term.print(text)
|
|
100
|
+
renderer = self.renderers[run_id] = Lines(term.stdout, color=bool(self.paint.codes), live=False)
|
|
101
|
+
return renderer
|
|
102
|
+
|
|
103
|
+
# -- following a task
|
|
104
|
+
|
|
105
|
+
def follow(self, run_id):
|
|
106
|
+
"""Stream the task's events into the queue (a thread of its own), from the start."""
|
|
107
|
+
self.run_id = run_id
|
|
108
|
+
if run_id in self.followers:
|
|
109
|
+
return
|
|
110
|
+
thread = threading.Thread(target=self._stream, args=(run_id,), daemon=True, name=f"mobster-chat-{run_id}")
|
|
111
|
+
self.followers[run_id] = thread
|
|
112
|
+
thread.start()
|
|
113
|
+
|
|
114
|
+
def _stream(self, run_id):
|
|
115
|
+
last, tries = None, 0
|
|
116
|
+
while True:
|
|
117
|
+
try:
|
|
118
|
+
for event in self.client.events(f"/api/runs/{run_id}/events", last, timeout=30):
|
|
119
|
+
tries = 0
|
|
120
|
+
if event is None:
|
|
121
|
+
continue
|
|
122
|
+
last = event.get("seq", last)
|
|
123
|
+
self.events.put(("event", run_id, event))
|
|
124
|
+
if event.get("event") == "run_finished":
|
|
125
|
+
self.events.put(("end", run_id, None))
|
|
126
|
+
return
|
|
127
|
+
return self._ended(run_id)
|
|
128
|
+
except (socket.timeout, TimeoutError):
|
|
129
|
+
continue
|
|
130
|
+
except ServiceError as error:
|
|
131
|
+
self.events.put(("lost", run_id, str(error)))
|
|
132
|
+
return
|
|
133
|
+
except NoService:
|
|
134
|
+
tries += 1
|
|
135
|
+
if tries > 5:
|
|
136
|
+
self.events.put(("lost", run_id, "Lost the connection to Mobster."))
|
|
137
|
+
return
|
|
138
|
+
time.sleep(min(2.0, 0.2 * tries))
|
|
139
|
+
|
|
140
|
+
def _ended(self, run_id):
|
|
141
|
+
self.events.put(("end", run_id, None))
|
|
142
|
+
|
|
143
|
+
def pump(self, timeout=0.0):
|
|
144
|
+
"""Handle what the followers queued; waits up to ``timeout`` for the first."""
|
|
145
|
+
try:
|
|
146
|
+
item = self.events.get(timeout=timeout) if timeout else self.events.get_nowait()
|
|
147
|
+
except queue.Empty:
|
|
148
|
+
return
|
|
149
|
+
while True:
|
|
150
|
+
self.handle(*item)
|
|
151
|
+
try:
|
|
152
|
+
item = self.events.get_nowait()
|
|
153
|
+
except queue.Empty:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
def handle(self, kind, run_id, event):
|
|
157
|
+
if kind == "lost":
|
|
158
|
+
self.note(str(event), indent="")
|
|
159
|
+
if run_id == self.run_id:
|
|
160
|
+
self.run_id = None
|
|
161
|
+
return
|
|
162
|
+
if kind == "end":
|
|
163
|
+
if run_id not in self.finished:
|
|
164
|
+
try:
|
|
165
|
+
self.finished[run_id] = self.client.run(run_id)
|
|
166
|
+
except (ServiceError, NoService):
|
|
167
|
+
self.finished[run_id] = {"status": "error"}
|
|
168
|
+
if run_id == self.run_id:
|
|
169
|
+
self.run_id, self.pending, self.paused = None, None, False
|
|
170
|
+
self.give_back()
|
|
171
|
+
return
|
|
172
|
+
name = event.get("event")
|
|
173
|
+
renderer = self.renderer(run_id)
|
|
174
|
+
if name == "step" and isinstance(event.get("text"), str):
|
|
175
|
+
# Mobster's agent says each step in a sentence ("Typed “Running late” in Message").
|
|
176
|
+
self.say(" " + self.paint("●", "green") + " " + event["text"])
|
|
177
|
+
return
|
|
178
|
+
if name == "plan" and isinstance(event.get("line"), str) and event["line"]:
|
|
179
|
+
self.say(f" {event['line']}", "faint")
|
|
180
|
+
return
|
|
181
|
+
if name in ("result", "run_finished"):
|
|
182
|
+
summary = event if name == "result" else (event.get("summary") if isinstance(event.get("summary"),
|
|
183
|
+
dict) else {})
|
|
184
|
+
if run_id not in self.outcomes:
|
|
185
|
+
self.outcomes.add(run_id)
|
|
186
|
+
renderer._flush_steps()
|
|
187
|
+
self.outcome({**summary, "status": summary.get("status") or event.get("status") or "error"})
|
|
188
|
+
return
|
|
189
|
+
if name not in ("approval_requested", "approval_resolved", "user_message", "steer_applied", "steer_unread"):
|
|
190
|
+
renderer(event) # the run's events, one line per step (console.py)
|
|
191
|
+
return
|
|
192
|
+
renderer.narrator.feed(event)
|
|
193
|
+
if name == "approval_requested":
|
|
194
|
+
self.ask(run_id, event)
|
|
195
|
+
elif name == "approval_resolved":
|
|
196
|
+
self.pending = None
|
|
197
|
+
decision = str(event.get("decision") or "")
|
|
198
|
+
text = DECISIONS.get(decision) or ("Answered" if decision.startswith("choice:") else decision)
|
|
199
|
+
self.say(f" ⎿ {text}", "faint")
|
|
200
|
+
self.give_back()
|
|
201
|
+
elif name == "steer_applied":
|
|
202
|
+
self.say(" – Mobster read your message", "faint")
|
|
203
|
+
elif name == "steer_unread":
|
|
204
|
+
self.note("Mobster finished before reading your message. Send it again to run it as a new task.")
|
|
205
|
+
elif name == "user_message" and event.get("source") not in ("cli",):
|
|
206
|
+
where = {"app": "in Mobster", "voice": "by voice", "tui": "in the terminal UI", "mcp": "from your agent"}
|
|
207
|
+
self.say(f" ↳ You wrote {where.get(event.get('source'), 'elsewhere')}: “{event.get('text')}”")
|
|
208
|
+
|
|
209
|
+
def outcome(self, summary):
|
|
210
|
+
"""The task's end: its status, time and cost, then its answer (or why it stopped)."""
|
|
211
|
+
from ..narrate import completion_message, seconds, status_glyph, status_label, status_tone
|
|
212
|
+
status = summary.get("status") or "error"
|
|
213
|
+
tone = status_tone(status)
|
|
214
|
+
role = {"success": "green", "error": "red", "warning": "amber", "review": "accent"}.get(tone, "muted")
|
|
215
|
+
glyph = status_glyph(tone)
|
|
216
|
+
facts = [seconds(summary.get("elapsed_ms"))] if summary.get("elapsed_ms") else []
|
|
217
|
+
cost = summary.get("costUsd")
|
|
218
|
+
if isinstance(cost, (int, float)) and cost > 0:
|
|
219
|
+
facts.append(f"${cost:.4f}" if cost < 0.1 else f"${cost:.2f}")
|
|
220
|
+
self.say(self.paint(glyph, role, "bold") + " " + self.paint(status_label(status), "bold")
|
|
221
|
+
+ (self.paint(" " + ", ".join(facts), "faint") if facts else ""))
|
|
222
|
+
data = summary.get("data")
|
|
223
|
+
if isinstance(data, (dict, list)):
|
|
224
|
+
body = json.dumps(data, indent=2, ensure_ascii=False)
|
|
225
|
+
else:
|
|
226
|
+
body = data if isinstance(data, str) and data.strip() else summary.get("answer")
|
|
227
|
+
if body and status not in ("approval_denied", "stopped"):
|
|
228
|
+
for line in str(body).splitlines():
|
|
229
|
+
self.say(" " + line)
|
|
230
|
+
else:
|
|
231
|
+
message = completion_message(status, summary.get("reason"), summary.get("data_status"), summary)
|
|
232
|
+
if message:
|
|
233
|
+
self.say(" " + message)
|
|
234
|
+
if self.term.prompting:
|
|
235
|
+
self.say("") # a conversation: one blank line between tasks
|
|
236
|
+
|
|
237
|
+
def ask(self, run_id, event):
|
|
238
|
+
try:
|
|
239
|
+
approval = self.client.run(run_id).get("approval")
|
|
240
|
+
except (ServiceError, NoService):
|
|
241
|
+
approval = None
|
|
242
|
+
if not approval or approval.get("id") != event.get("approval_id"):
|
|
243
|
+
return
|
|
244
|
+
self.pending = dict(approval, runId=run_id)
|
|
245
|
+
if approval.get("kind") == "clarify":
|
|
246
|
+
self.note(self.paint(str(approval.get("question") or approval.get("label") or "Mobster has a question"),
|
|
247
|
+
"bold"), glyph="?", role="accent", indent="")
|
|
248
|
+
for number, choice in enumerate(approval.get("choices") or (), 1):
|
|
249
|
+
self.say(" " + self.keys((str(number), str(choice.get("label")))))
|
|
250
|
+
self.say(" Type your answer" + (", or its number" if approval.get("choices") else "")
|
|
251
|
+
+ ". Mobster waits 10 minutes, then stops.", "faint")
|
|
252
|
+
return
|
|
253
|
+
# A line half typed when the approval came (a message meant for the task) waits until it's answered, so its
|
|
254
|
+
# letters never become the answer.
|
|
255
|
+
with self.term.lock:
|
|
256
|
+
if self.term.buffer:
|
|
257
|
+
self.held, self.term.buffer = self.held or self.term.buffer, ""
|
|
258
|
+
self.show_approval(approval)
|
|
259
|
+
if self.interactive:
|
|
260
|
+
yes, no = verbs(approval)
|
|
261
|
+
choices = [c for c in approval.get("choices") or () if isinstance(c, dict) and c.get("id")]
|
|
262
|
+
if self.client.app_session():
|
|
263
|
+
# On its own line: the keys below it never wrap into it on a narrow terminal.
|
|
264
|
+
self.say(f" Choose {yes} or {no} in Mobster.")
|
|
265
|
+
self.say(" " + self.keys(("n", no), ("x", "Stop the task")))
|
|
266
|
+
else:
|
|
267
|
+
self.say(" " + self.keys(("y", yes), ("n", no), ("x", "Stop the task")))
|
|
268
|
+
for number, choice in enumerate(choices, 1):
|
|
269
|
+
self.say(" " + self.keys((str(number), str(choice.get("label")))))
|
|
270
|
+
self.say(" Type the " + ("letter or number" if choices and not self.client._app_session else "letter")
|
|
271
|
+
+ ", then Return.", "faint")
|
|
272
|
+
# Only a key pressed after the exact text is on screen answers it: anything typed before is dropped.
|
|
273
|
+
self.term.discard_typeahead()
|
|
274
|
+
self.escape = None
|
|
275
|
+
|
|
276
|
+
def give_back(self):
|
|
277
|
+
"""The line you were typing when an approval came, back at the prompt once nothing waits."""
|
|
278
|
+
if self.held and not self.pending:
|
|
279
|
+
with self.term.lock:
|
|
280
|
+
self.term.buffer, self.held = self.held + self.term.buffer, ""
|
|
281
|
+
self.term.show()
|
|
282
|
+
|
|
283
|
+
def show_approval(self, approval):
|
|
284
|
+
self.note(self.paint(approval_title(approval), "bold"), glyph="?", indent="")
|
|
285
|
+
if approval.get("text"):
|
|
286
|
+
self.say(f" “{approval['text']}”")
|
|
287
|
+
|
|
288
|
+
# -- something to remember
|
|
289
|
+
|
|
290
|
+
def offer(self, proposals):
|
|
291
|
+
"""A message that only asked Mobster to remember something: no task runs, and each suggestion shows here."""
|
|
292
|
+
proposals = [p for p in proposals or () if isinstance(p, dict) and p.get("id") and p.get("text")]
|
|
293
|
+
if not proposals:
|
|
294
|
+
self.say("Mobster already has this.")
|
|
295
|
+
return
|
|
296
|
+
if not self.interactive:
|
|
297
|
+
for proposal in proposals:
|
|
298
|
+
self.show_offer(proposal, ask=False)
|
|
299
|
+
self.say(" " + where_to_remember(proposal), "faint")
|
|
300
|
+
return
|
|
301
|
+
asking = bool(self.offers)
|
|
302
|
+
self.offers.extend(proposals)
|
|
303
|
+
if not asking:
|
|
304
|
+
self.show_offer(self.offers[0])
|
|
305
|
+
|
|
306
|
+
def show_offer(self, proposal, ask=True):
|
|
307
|
+
self.note(self.paint("Remember this?", "bold") + f" “{proposal['text']}”", glyph="?", role="accent", indent="")
|
|
308
|
+
scope = proposal.get("scope") or "global"
|
|
309
|
+
if scope == "global":
|
|
310
|
+
use = ("It goes with every task (pinned)." if proposal.get("pin") else
|
|
311
|
+
"Mobster will use it in tasks it fits.")
|
|
312
|
+
else:
|
|
313
|
+
where = ("tasks for this saved profile and workflow" if scope.startswith("profile:") and
|
|
314
|
+
"/workflow:" in scope else "tasks for this saved profile" if scope.startswith("profile:")
|
|
315
|
+
else "tasks in this workflow" if scope.startswith("workflow:") else "eligible tasks")
|
|
316
|
+
use = (f"Pinned: it goes with {where}." if proposal.get("pin") else
|
|
317
|
+
f"Mobster will use it in {where} when it fits.")
|
|
318
|
+
self.say(f" {use} It stays on this Mac.", "faint")
|
|
319
|
+
if ask:
|
|
320
|
+
self.say(" " + self.keys(("y", "Remember"), ("n", "Not now")))
|
|
321
|
+
self.say(" Type the letter, then Return.", "faint")
|
|
322
|
+
# As with an approval, only a key pressed after the words are on screen answers.
|
|
323
|
+
self.term.discard_typeahead()
|
|
324
|
+
self.escape = None
|
|
325
|
+
|
|
326
|
+
def answer_offer(self, line):
|
|
327
|
+
"""A line typed while a suggestion waits: exactly y or n answers it, a /command runs, and other words are
|
|
328
|
+
neither sent nor taken as an answer."""
|
|
329
|
+
word = line.strip().lower()
|
|
330
|
+
if word.startswith("/"):
|
|
331
|
+
return False
|
|
332
|
+
if word not in ("y", "n"):
|
|
333
|
+
self.say(" Type y or n first.")
|
|
334
|
+
return True
|
|
335
|
+
from urllib.parse import quote
|
|
336
|
+
proposal = self.offers.pop(0)
|
|
337
|
+
try:
|
|
338
|
+
self.client.call("POST", f"/api/memory/proposals/{quote(str(proposal['id']), safe='')}",
|
|
339
|
+
{"accept": word == "y"})
|
|
340
|
+
except ServiceError as error:
|
|
341
|
+
self.note(str(error)) # answered in the Mac app meanwhile, or expired: the service's sentence
|
|
342
|
+
except NoService:
|
|
343
|
+
self.note(NOT_RUNNING)
|
|
344
|
+
else:
|
|
345
|
+
self.say(" ⎿ " + ("Mobster will remember this." if word == "y" else "Not remembered."), "faint")
|
|
346
|
+
if self.offers:
|
|
347
|
+
self.show_offer(self.offers[0])
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
# -- your input
|
|
351
|
+
|
|
352
|
+
def decide(self, line):
|
|
353
|
+
"""A line typed while an approval or a question waits: True when it answered (or stopped)."""
|
|
354
|
+
pending = self.pending
|
|
355
|
+
if not pending:
|
|
356
|
+
return self.answer_offer(line) if self.offers else False
|
|
357
|
+
run_id, word = pending["runId"], line.strip().lower()
|
|
358
|
+
if pending.get("kind") == "clarify":
|
|
359
|
+
choices = pending.get("choices") or []
|
|
360
|
+
if word.isdigit() and 1 <= int(word) <= len(choices):
|
|
361
|
+
return self.answer(run_id, {"id": pending["id"], "approve": True,
|
|
362
|
+
"choice": choices[int(word) - 1]["id"]})
|
|
363
|
+
return False
|
|
364
|
+
choices = [c for c in pending.get("choices") or () if isinstance(c, dict) and c.get("id")]
|
|
365
|
+
if word == "y" or (word.isdigit() and 1 <= int(word) <= len(choices)):
|
|
366
|
+
if self.client.app_session():
|
|
367
|
+
self.note(f"Choose {verbs(pending)[0]} in Mobster: the app holds approvals while it's open.")
|
|
368
|
+
return True
|
|
369
|
+
if word != "y":
|
|
370
|
+
return self.answer(run_id, {"id": pending["id"], "approve": True,
|
|
371
|
+
"choice": choices[int(word) - 1]["id"]})
|
|
372
|
+
return self.answer(run_id, {"id": pending["id"], "approve": True})
|
|
373
|
+
if word == "n":
|
|
374
|
+
return self.answer(run_id, {"id": pending["id"], "approve": False})
|
|
375
|
+
if word == "x":
|
|
376
|
+
self.stop()
|
|
377
|
+
return True
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def answer(self, run_id, body):
|
|
381
|
+
try:
|
|
382
|
+
self.client.call("POST", f"/api/runs/{run_id}/approval", body)
|
|
383
|
+
except ServiceError as error:
|
|
384
|
+
if error.code == "app_only":
|
|
385
|
+
self.client._app_session = True
|
|
386
|
+
self.note(f"Choose {verbs(self.pending or {})[0]} in Mobster: the app holds approvals while it's open.")
|
|
387
|
+
else:
|
|
388
|
+
self.note(str(error))
|
|
389
|
+
except NoService:
|
|
390
|
+
self.note(NOT_RUNNING)
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
def stop(self):
|
|
394
|
+
if not self.run_id:
|
|
395
|
+
self.say("Nothing is running.", "faint")
|
|
396
|
+
return
|
|
397
|
+
try:
|
|
398
|
+
self.client.call("POST", f"/api/runs/{self.run_id}/stop", {})
|
|
399
|
+
self.say(" Stopping…", "faint")
|
|
400
|
+
except (ServiceError, NoService) as error:
|
|
401
|
+
self.note(str(error))
|
|
402
|
+
|
|
403
|
+
def send(self, text):
|
|
404
|
+
"""A message: a new task, a steer, or an answer, as Mobster routes it. Returns the response, or None."""
|
|
405
|
+
if self.pending and self.pending.get("kind") != "clarify":
|
|
406
|
+
yes, no = verbs(self.pending)
|
|
407
|
+
hint = (f"Choose {yes} or {no} in Mobster, or type n or x here."
|
|
408
|
+
if self.client._app_session else "Type y, n or x first.")
|
|
409
|
+
self.note("A typed message never approves anything.")
|
|
410
|
+
self.say(f" {hint}") # its own line: a narrow terminal never wraps it back to column 0
|
|
411
|
+
return None
|
|
412
|
+
body = {"text": text}
|
|
413
|
+
if self.device:
|
|
414
|
+
body["device"] = self.device
|
|
415
|
+
if self.attachments:
|
|
416
|
+
body["attachmentIds"] = [ident for ident, _name in self.attachments]
|
|
417
|
+
try:
|
|
418
|
+
self.thread_id, response = self.client.send(self.thread_id, body)
|
|
419
|
+
except ServiceError as error:
|
|
420
|
+
if error.status == 404 and error.code == "thread_not_found":
|
|
421
|
+
self.note("That conversation is gone here. Your next message starts a new one.")
|
|
422
|
+
self.thread_id = None
|
|
423
|
+
else:
|
|
424
|
+
self.note(str(error))
|
|
425
|
+
return None
|
|
426
|
+
except NoService:
|
|
427
|
+
self.note(NOT_RUNNING)
|
|
428
|
+
return None
|
|
429
|
+
remember_thread(self.client.url, self.thread_id)
|
|
430
|
+
self.attachments = []
|
|
431
|
+
routed = routed_as(response)
|
|
432
|
+
if routed == "steer":
|
|
433
|
+
self.say(" ↳ Sent while Mobster was working", "faint")
|
|
434
|
+
run_id = (response.get("item") or {}).get("runId")
|
|
435
|
+
if run_id and run_id != self.run_id:
|
|
436
|
+
self.follow(run_id)
|
|
437
|
+
elif routed == "answer":
|
|
438
|
+
self.pending = None
|
|
439
|
+
elif routed == "remember":
|
|
440
|
+
self.offer(proposals_of(self.client, self.thread_id, response))
|
|
441
|
+
else:
|
|
442
|
+
run = response.get("run") or {}
|
|
443
|
+
if run.get("id"):
|
|
444
|
+
where = run.get("deviceName")
|
|
445
|
+
if not self.term.prompting: # one message: its first line (a conversation shows what you typed)
|
|
446
|
+
self.renderer(run["id"]).start(text, f"on {where}" if where else "")
|
|
447
|
+
elif where:
|
|
448
|
+
self.say(f" on {where}", "faint")
|
|
449
|
+
self.follow(run["id"])
|
|
450
|
+
return response
|
|
451
|
+
|
|
452
|
+
def command(self, line):
|
|
453
|
+
name, _, rest = line.partition(" ")
|
|
454
|
+
rest = rest.strip()
|
|
455
|
+
if name in ("/quit", "/exit"):
|
|
456
|
+
self.quit = True
|
|
457
|
+
elif name == "/help":
|
|
458
|
+
for row in HELP.splitlines():
|
|
459
|
+
self.say(row)
|
|
460
|
+
elif name == "/new":
|
|
461
|
+
self.thread_id = None
|
|
462
|
+
self.say("New conversation: your next message starts it.", "faint")
|
|
463
|
+
elif name == "/threads":
|
|
464
|
+
self.list_threads()
|
|
465
|
+
elif name == "/open":
|
|
466
|
+
self.open(rest)
|
|
467
|
+
elif name == "/stop":
|
|
468
|
+
self.stop()
|
|
469
|
+
elif name == "/pause":
|
|
470
|
+
self.pause()
|
|
471
|
+
elif name == "/attach":
|
|
472
|
+
self.attach(rest)
|
|
473
|
+
else:
|
|
474
|
+
self.note(f"Unknown command {name}. /help lists them.", indent="")
|
|
475
|
+
|
|
476
|
+
def list_threads(self):
|
|
477
|
+
try:
|
|
478
|
+
threads = self.client.call("GET", "/api/threads?limit=10")["threads"]
|
|
479
|
+
except (ServiceError, NoService) as error:
|
|
480
|
+
self.note(str(error), indent="")
|
|
481
|
+
return
|
|
482
|
+
if not threads:
|
|
483
|
+
self.say("No conversations yet.", "faint")
|
|
484
|
+
for thread in threads:
|
|
485
|
+
mark = "●" if thread.get("status") in ("running", "waiting") else " "
|
|
486
|
+
here = " (this one)" if thread["id"] == self.thread_id else ""
|
|
487
|
+
self.say(f"{mark} {self.paint(thread['id'], 'faint')} {ago(thread.get('updatedAt')):>8} "
|
|
488
|
+
f"{thread.get('title')}{here}")
|
|
489
|
+
|
|
490
|
+
def open(self, thread_id):
|
|
491
|
+
if not thread_id:
|
|
492
|
+
self.note("Open which? /threads lists them.", indent="")
|
|
493
|
+
return
|
|
494
|
+
try:
|
|
495
|
+
answer = self.client.thread(thread_id)
|
|
496
|
+
except ServiceError as error:
|
|
497
|
+
self.note(str(error), indent="")
|
|
498
|
+
return
|
|
499
|
+
except NoService:
|
|
500
|
+
self.note(NOT_RUNNING, indent="")
|
|
501
|
+
return
|
|
502
|
+
thread = answer["thread"]
|
|
503
|
+
self.thread_id = thread["id"]
|
|
504
|
+
remember_thread(self.client.url, self.thread_id)
|
|
505
|
+
self.say(f"Conversation “{thread.get('title')}”" + (f" on {thread['deviceName']}"
|
|
506
|
+
if thread.get("deviceName") else ""), "bold")
|
|
507
|
+
for line in transcript(answer.get("items") or [], limit=6):
|
|
508
|
+
self.say(line, "faint")
|
|
509
|
+
running = thread.get("lastRunId")
|
|
510
|
+
run = (answer.get("runs") or {}).get(running) if running else None
|
|
511
|
+
if run and run.get("finishedAt") is None:
|
|
512
|
+
self.follow(running)
|
|
513
|
+
|
|
514
|
+
def pause(self):
|
|
515
|
+
if not self.run_id:
|
|
516
|
+
self.say("Nothing is running.", "faint")
|
|
517
|
+
return
|
|
518
|
+
try:
|
|
519
|
+
answer = self.client.call("POST", f"/api/runs/{self.run_id}/pause", {"paused": not self.paused})
|
|
520
|
+
self.paused = bool(answer.get("paused", not self.paused))
|
|
521
|
+
self.say(" Paused. /pause again to continue." if self.paused else " Continuing.", "faint")
|
|
522
|
+
except ServiceError as error:
|
|
523
|
+
# 404: the running Mobster has no pause yet (an older app, or `serve` without the harness).
|
|
524
|
+
self.note(NO_PAUSE if error.status == 404 else str(error))
|
|
525
|
+
except NoService:
|
|
526
|
+
self.note(NOT_RUNNING)
|
|
527
|
+
|
|
528
|
+
def attach(self, path):
|
|
529
|
+
if not path:
|
|
530
|
+
self.note("Attach which file? /attach PATH", indent="")
|
|
531
|
+
return
|
|
532
|
+
file = Path(path).expanduser()
|
|
533
|
+
try:
|
|
534
|
+
size = file.stat().st_size
|
|
535
|
+
if size > ATTACH_MAX:
|
|
536
|
+
self.note("A file can be at most 25 MB.", indent="")
|
|
537
|
+
return
|
|
538
|
+
data = file.read_bytes()
|
|
539
|
+
except OSError:
|
|
540
|
+
self.note(f"Can't read {path}.", indent="")
|
|
541
|
+
return
|
|
542
|
+
from urllib.parse import quote
|
|
543
|
+
try:
|
|
544
|
+
answer = self.client.call("POST", f"/api/attachments?name={quote(file.name)}", raw=data,
|
|
545
|
+
content_type="application/octet-stream", timeout=120)
|
|
546
|
+
except ServiceError as error:
|
|
547
|
+
self.note(NO_FILES if error.status == 404 else str(error), indent="")
|
|
548
|
+
return
|
|
549
|
+
except NoService:
|
|
550
|
+
self.note(NOT_RUNNING, indent="")
|
|
551
|
+
return
|
|
552
|
+
attachment = answer.get("attachment") or {}
|
|
553
|
+
if attachment.get("id"):
|
|
554
|
+
self.attachments.append((attachment["id"], attachment.get("name") or file.name))
|
|
555
|
+
self.say(f"Attached {attachment.get('name') or file.name}. It goes with your next message.", "faint")
|
|
556
|
+
|
|
557
|
+
# -- the loop
|
|
558
|
+
|
|
559
|
+
def repl(self):
|
|
560
|
+
term = self.term
|
|
561
|
+
self.say(self.paint("Mobster", "accent", "bold") + self.paint(" conversation in Terminal /help for "
|
|
562
|
+
"commands ctrl+d to leave", "faint"))
|
|
563
|
+
if self.thread_id:
|
|
564
|
+
self.open(self.thread_id)
|
|
565
|
+
term.show()
|
|
566
|
+
while not self.quit:
|
|
567
|
+
try:
|
|
568
|
+
self.pump()
|
|
569
|
+
data = term.keys(0.1)
|
|
570
|
+
if data is None:
|
|
571
|
+
break
|
|
572
|
+
self.feed(data)
|
|
573
|
+
except KeyboardInterrupt:
|
|
574
|
+
if term.buffer:
|
|
575
|
+
term.buffer = ""
|
|
576
|
+
term.show()
|
|
577
|
+
elif self.run_id:
|
|
578
|
+
self.stop()
|
|
579
|
+
else:
|
|
580
|
+
break
|
|
581
|
+
term.clear()
|
|
582
|
+
return EXIT_DONE
|
|
583
|
+
|
|
584
|
+
def feed(self, data):
|
|
585
|
+
"""What one read of the terminal brought, key by key."""
|
|
586
|
+
for ch in data:
|
|
587
|
+
self.type(ch)
|
|
588
|
+
if self.quit:
|
|
589
|
+
break
|
|
590
|
+
if self.escape == "esc":
|
|
591
|
+
self.escape = None # Escape on its own, not the start of an arrow key's sequence
|
|
592
|
+
|
|
593
|
+
def type(self, ch):
|
|
594
|
+
"""One key typed on the terminal (whole lines elsewhere)."""
|
|
595
|
+
term = self.term
|
|
596
|
+
if self.escape is not None:
|
|
597
|
+
# The rest of a key's escape sequence (an arrow is ESC [ A, Option-arrow ESC [ 1 ; 3 D, F1 ESC O P):
|
|
598
|
+
# dropped whole, so it never ends up in a message as "[A".
|
|
599
|
+
if self.escape == "esc":
|
|
600
|
+
self.escape = "csi" if ch == "[" else "ss3" if ch == "O" else None
|
|
601
|
+
elif self.escape == "ss3" or "\x40" <= ch <= "\x7e":
|
|
602
|
+
self.escape = None
|
|
603
|
+
return
|
|
604
|
+
if ch == "\x1b":
|
|
605
|
+
self.escape = "esc"
|
|
606
|
+
return
|
|
607
|
+
if ch in ("\r", "\n"):
|
|
608
|
+
line, term.buffer = term.buffer.strip(), ""
|
|
609
|
+
if term.tty:
|
|
610
|
+
term.clear()
|
|
611
|
+
if line:
|
|
612
|
+
term.write(self.paint("› ", "accent") + line + "\n")
|
|
613
|
+
self.submit(line)
|
|
614
|
+
term.show()
|
|
615
|
+
elif ch in ("\x7f", "\b"):
|
|
616
|
+
term.buffer = term.buffer[:-1]
|
|
617
|
+
term.show()
|
|
618
|
+
elif ch == "\x15": # ctrl+u
|
|
619
|
+
term.buffer = ""
|
|
620
|
+
term.show()
|
|
621
|
+
elif ch == "\x04": # ctrl+d
|
|
622
|
+
if not term.buffer:
|
|
623
|
+
self.quit = True
|
|
624
|
+
elif ch < " " and ch != "\t":
|
|
625
|
+
pass # other control keys: ignored
|
|
626
|
+
else:
|
|
627
|
+
term.buffer += ch
|
|
628
|
+
if term.tty:
|
|
629
|
+
term.write(ch)
|
|
630
|
+
term.shown = True
|
|
631
|
+
|
|
632
|
+
def submit(self, line):
|
|
633
|
+
if not line:
|
|
634
|
+
return
|
|
635
|
+
if self.decide(line):
|
|
636
|
+
return
|
|
637
|
+
if line.startswith("/"):
|
|
638
|
+
self.command(line)
|
|
639
|
+
else:
|
|
640
|
+
self.send(line)
|
|
641
|
+
|
|
642
|
+
def ago(ms):
|
|
643
|
+
if not ms:
|
|
644
|
+
return ""
|
|
645
|
+
seconds = max(0, time.time() - ms / 1000)
|
|
646
|
+
for unit, size in (("d", 86400), ("h", 3600), ("min", 60)):
|
|
647
|
+
if seconds >= size:
|
|
648
|
+
return f"{int(seconds // size)} {unit} ago"
|
|
649
|
+
return "just now"
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def transcript(items, limit=6):
|
|
653
|
+
"""The last exchanges of a thread, one line each, for /open."""
|
|
654
|
+
lines = []
|
|
655
|
+
for item in items:
|
|
656
|
+
if item.get("kind") == "user":
|
|
657
|
+
mark = {"steer": "↳ ", "answer": "↳ ", "unread": "↳ (not read) "}.get(item.get("routed"), "› ")
|
|
658
|
+
lines.append(f"{mark}{item.get('text')}")
|
|
659
|
+
elif item.get("kind") == "run" and item.get("result"):
|
|
660
|
+
from ..narrate import status_label
|
|
661
|
+
result = item["result"]
|
|
662
|
+
lines.append(f" {status_label(result.get('status'))}" + (f": {result['answer']}" if result.get("answer")
|
|
663
|
+
else ""))
|
|
664
|
+
elif item.get("kind") == "clarify":
|
|
665
|
+
lines.append(f" ? {item.get('question')}" + (f" → {item['answer']}" if item.get("answer") else ""))
|
|
666
|
+
return lines[-limit:]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def run(args, stdin=None, stdout=None, stderr=None, client=None):
|
|
670
|
+
stdin, stdout, stderr = stdin or sys.stdin, stdout or sys.stdout, stderr or sys.stderr
|
|
671
|
+
as_json = bool(args.json)
|
|
672
|
+
|
|
673
|
+
def fail(message, code="couldnt_run"):
|
|
674
|
+
if as_json:
|
|
675
|
+
print(json.dumps({"status": "couldnt_run", "error": message, "code": code}), file=stdout, flush=True)
|
|
676
|
+
else:
|
|
677
|
+
print(message, file=stderr, flush=True)
|
|
678
|
+
return EXIT_COULDNT
|
|
679
|
+
|
|
680
|
+
if client is None:
|
|
681
|
+
try:
|
|
682
|
+
client = Client.connect(args.url, origin="cli")
|
|
683
|
+
except NoService as error:
|
|
684
|
+
return fail(str(error), "not_running")
|
|
685
|
+
except ServiceError as error:
|
|
686
|
+
return fail(str(error), error.code or "refused")
|
|
687
|
+
if not client.has_threads:
|
|
688
|
+
return fail(NO_THREADS, "no_threads")
|
|
689
|
+
text = " ".join(args.message).strip()
|
|
690
|
+
workflow = getattr(args, "workflow", None)
|
|
691
|
+
if workflow and text:
|
|
692
|
+
return fail("Give a message or --workflow, not both.", "usage")
|
|
693
|
+
if getattr(args, "set", None) and not workflow:
|
|
694
|
+
return fail("--set fills a workflow's blanks: use it with --workflow.", "usage")
|
|
695
|
+
if getattr(args, "keep_waiting", False) and not as_json:
|
|
696
|
+
return fail("--keep-waiting goes with --json.", "usage")
|
|
697
|
+
wait = 300.0 if args.wait is None else args.wait
|
|
698
|
+
if not text and not workflow and not isatty(stdin):
|
|
699
|
+
text = stdin.read().strip()
|
|
700
|
+
if not text:
|
|
701
|
+
return fail("Say what Mobster should do: mobster chat \"What's on my calendar tomorrow?\"", "usage")
|
|
702
|
+
thread_id = None
|
|
703
|
+
if args.thread:
|
|
704
|
+
thread_id = args.thread.strip().lower()
|
|
705
|
+
try:
|
|
706
|
+
client.thread(thread_id)
|
|
707
|
+
except ServiceError as error:
|
|
708
|
+
return fail(str(error), error.code or "thread_not_found")
|
|
709
|
+
except NoService as error:
|
|
710
|
+
return fail(str(error), "not_running")
|
|
711
|
+
elif not args.new and not workflow:
|
|
712
|
+
remembered = last_thread(client.url)
|
|
713
|
+
if remembered:
|
|
714
|
+
try:
|
|
715
|
+
thread = client.thread(remembered)["thread"]
|
|
716
|
+
thread_id = None if thread.get("archived") else remembered
|
|
717
|
+
except (ServiceError, NoService):
|
|
718
|
+
thread_id = None
|
|
719
|
+
if text and len(text) > 4000:
|
|
720
|
+
return fail("A message can be at most 4,000 characters.", "too_long")
|
|
721
|
+
term = Term(stdin, stdout if not as_json else stderr)
|
|
722
|
+
term.prompting = not text and not workflow
|
|
723
|
+
with term:
|
|
724
|
+
chat = Chat(client, term, thread_id=thread_id, device=args.device, interactive=term.tty and not as_json)
|
|
725
|
+
if workflow:
|
|
726
|
+
try:
|
|
727
|
+
return workflow_once(client, chat, args, as_json=as_json, wait=wait, out=stdout, fail=fail)
|
|
728
|
+
except NoService as error:
|
|
729
|
+
return fail(str(error), "not_running")
|
|
730
|
+
if text:
|
|
731
|
+
if thread_id and not as_json:
|
|
732
|
+
chat.say(f"Continuing your last conversation ({thread_id}); --new starts another.", "faint")
|
|
733
|
+
try:
|
|
734
|
+
return once(client, chat, text, as_json=as_json, wait=wait, out=stdout,
|
|
735
|
+
keep=bool(getattr(args, "keep_waiting", False)))
|
|
736
|
+
except NoService as error:
|
|
737
|
+
return fail(str(error), "not_running")
|
|
738
|
+
if not term.tty:
|
|
739
|
+
return fail("Say what Mobster should do: mobster chat \"What's on my calendar tomorrow?\"", "usage")
|
|
740
|
+
return chat.repl()
|