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,806 @@
|
|
|
1
|
+
"""What Mobster remembers, in one SQLite file on this Mac (SPEC §3.3).
|
|
2
|
+
|
|
3
|
+
``user_data_dir()/memory/memory.sqlite3``: the folder is 0700 and the file 0600. WAL and a short-lived connection
|
|
4
|
+
per call, so the Mac app's agent, the terminal UI, `mobster memory` and MCP share it safely. ``MOBSTER_MEMORY_DIR``
|
|
5
|
+
moves it (tests). With neither that nor a home folder (a test that cleared the environment), there is no store:
|
|
6
|
+
nothing is read or written, and every caller treats memory as empty and off.
|
|
7
|
+
|
|
8
|
+
A fact has a kind, a source, a scope, a confidence and a fingerprint (model.py). Two ways in:
|
|
9
|
+
|
|
10
|
+
- ``add_fact`` and ``resolve_proposal``: what the person wrote, or accepted from a suggestion of their own words.
|
|
11
|
+
- ``learn`` (and ``stage`` + ``commit_run``): what Mobster learned from use. A run stages what it learned, and the
|
|
12
|
+
facts are written only when the run finishes with a result the person didn't dispute (``commit_run``). They show
|
|
13
|
+
under "New since you last looked" until the person keeps or forgets them.
|
|
14
|
+
|
|
15
|
+
Forgetting a fact removes it and keeps its fingerprint for 90 days, so nothing relearns it. Never secrets, whoever
|
|
16
|
+
writes: a fact or a suggestion that ``secret_filter.is_secret`` flags is refused. Errors are ``api_errors.APIError``
|
|
17
|
+
with a plain sentence, so the HTTP routes and the CLI say the same thing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sqlite3
|
|
23
|
+
import stat
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import uuid
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from ..api_errors import APIError
|
|
31
|
+
from ..catalog import app_name
|
|
32
|
+
from ..paths import user_data_dir
|
|
33
|
+
from . import model
|
|
34
|
+
from .model import FACT_CHARS as FACT_CHARS
|
|
35
|
+
from .model import (
|
|
36
|
+
clean_confidence,
|
|
37
|
+
clean_key,
|
|
38
|
+
clean_kind,
|
|
39
|
+
clean_ref,
|
|
40
|
+
clean_scope,
|
|
41
|
+
clean_source,
|
|
42
|
+
clean_text,
|
|
43
|
+
digest,
|
|
44
|
+
fingerprint,
|
|
45
|
+
scope_parts,
|
|
46
|
+
)
|
|
47
|
+
from .model import normalize as normalize
|
|
48
|
+
|
|
49
|
+
SCHEMA_VERSION = 2
|
|
50
|
+
FILE = "memory.sqlite3"
|
|
51
|
+
|
|
52
|
+
MAX_FACTS = 1000 # all facts; at the limit the least used learned fact makes room, a person's never does
|
|
53
|
+
MAX_PINNED = 10 # pinned facts ship with every task, so they're few and short
|
|
54
|
+
PINNED_CHARS = 1000
|
|
55
|
+
MAX_PENDING = 50 # suggestions waiting for an answer; the oldest expire first
|
|
56
|
+
CONFIRM = "delete everything"
|
|
57
|
+
PROPOSAL_DAYS = 14 # a suggestion nobody answered expires
|
|
58
|
+
QUIET_DAYS = 90 # a suggestion answered "Not now", or a fact forgotten, isn't offered again for this long
|
|
59
|
+
STALE_DAYS = 90 # a fact nobody used for this long is offered for removal
|
|
60
|
+
STAGED_DAYS = 2 # what a run learned is dropped if the run never finishes
|
|
61
|
+
USE_DAYS = 30 # how long the facts a run used stay readable from the run
|
|
62
|
+
DAY = 86400.0
|
|
63
|
+
|
|
64
|
+
NEWER = "This was saved by a newer Mobster. Update Mobster to use it."
|
|
65
|
+
UNAVAILABLE = "Mobster can't open what it remembers on this Mac right now. Try again."
|
|
66
|
+
GONE = "Mobster doesn't remember that anymore."
|
|
67
|
+
|
|
68
|
+
SETTINGS = {"useInTasks": True, "suggest": True} # suggest also turns learning from use on and off
|
|
69
|
+
PROPOSAL_STATES = ("pending", "accepted", "dismissed", "expired")
|
|
70
|
+
|
|
71
|
+
SCHEMA = """
|
|
72
|
+
CREATE TABLE IF NOT EXISTS facts(id TEXT PRIMARY KEY, scope TEXT NOT NULL,
|
|
73
|
+
text TEXT NOT NULL CHECK(length(text) <= 300), kind TEXT NOT NULL, source TEXT NOT NULL,
|
|
74
|
+
ref TEXT NOT NULL DEFAULT '{}', run_id TEXT, confidence REAL NOT NULL DEFAULT 1, key TEXT,
|
|
75
|
+
fingerprint TEXT NOT NULL UNIQUE, pinned INTEGER NOT NULL DEFAULT 0, kept INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
edited INTEGER NOT NULL DEFAULT 0, created_at REAL NOT NULL, updated_at REAL NOT NULL, last_used_at REAL,
|
|
77
|
+
uses INTEGER NOT NULL DEFAULT 0);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS facts_run ON facts(run_id);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS forgotten(fingerprint TEXT PRIMARY KEY, forgotten_at REAL NOT NULL);
|
|
80
|
+
CREATE TABLE IF NOT EXISTS staged(id TEXT PRIMARY KEY, run_id TEXT NOT NULL, payload TEXT NOT NULL,
|
|
81
|
+
created_at REAL NOT NULL);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS staged_run ON staged(run_id);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS run_use(run_id TEXT NOT NULL, fact_id TEXT NOT NULL, at REAL NOT NULL,
|
|
84
|
+
PRIMARY KEY(run_id, fact_id));
|
|
85
|
+
CREATE TABLE IF NOT EXISTS projects(id TEXT PRIMARY KEY, path TEXT NOT NULL);
|
|
86
|
+
CREATE TABLE IF NOT EXISTS proposals(id TEXT PRIMARY KEY, text TEXT NOT NULL, scope TEXT NOT NULL, thread_id TEXT,
|
|
87
|
+
run_id TEXT, status TEXT NOT NULL CHECK(status IN ('pending','accepted','dismissed','expired')),
|
|
88
|
+
created_at REAL, resolved_at REAL, digest TEXT NOT NULL, pin INTEGER NOT NULL DEFAULT 0, item_id TEXT,
|
|
89
|
+
fact_id TEXT);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS proposals_status ON proposals(status, created_at);
|
|
91
|
+
CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def memory_dir():
|
|
96
|
+
"""The store's folder, or None when there is nowhere safe to keep it (no home folder in the environment)."""
|
|
97
|
+
value = os.environ.get("MOBSTER_MEMORY_DIR", "").strip()
|
|
98
|
+
if value:
|
|
99
|
+
return Path(value).expanduser()
|
|
100
|
+
if os.environ.get("HOME") or getattr(sys, "frozen", False):
|
|
101
|
+
return user_data_dir() / "memory"
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def default_store():
|
|
106
|
+
"""The store at ``memory_dir()``, or None. Creating it does no I/O."""
|
|
107
|
+
folder = memory_dir()
|
|
108
|
+
return MemoryStore(folder) if folder is not None else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def new_id():
|
|
112
|
+
return uuid.uuid4().hex[:12]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def millis(seconds):
|
|
116
|
+
return None if seconds is None else round(seconds * 1000)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _scope_view(scope):
|
|
120
|
+
kind, ident = scope_parts(scope)
|
|
121
|
+
return {"kind": kind, "id": ident, "app": ({"bundleId": ident, "name": app_name(ident) or ident}
|
|
122
|
+
if kind == "app" else None)}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _fact(row, looked=0.0, now=0.0):
|
|
126
|
+
"""A row as the API shows it. ``new``: learned since the person last looked and not kept. ``stale``: unpinned,
|
|
127
|
+
not kept and unused for 90 days, so offered for removal."""
|
|
128
|
+
where = _scope_view(row["scope"])
|
|
129
|
+
learned = row["source"] != "user"
|
|
130
|
+
return {"id": row["id"], "text": row["text"], "scope": row["scope"], "scopeKind": where["kind"],
|
|
131
|
+
"scopeId": where["id"], "app": where["app"], "kind": row["kind"], "source": row["source"],
|
|
132
|
+
"ref": json.loads(row["ref"] or "{}"), "confidence": row["confidence"], "pinned": bool(row["pinned"]),
|
|
133
|
+
"kept": bool(row["kept"]), "edited": bool(row["edited"]), "key": row["key"],
|
|
134
|
+
"new": learned and not row["kept"] and row["created_at"] > looked,
|
|
135
|
+
"stale": not row["pinned"] and not row["kept"]
|
|
136
|
+
and (row["last_used_at"] or row["created_at"]) < now - STALE_DAYS * DAY,
|
|
137
|
+
"createdAt": millis(row["created_at"]), "updatedAt": millis(row["updated_at"]),
|
|
138
|
+
"lastUsedAt": millis(row["last_used_at"]), "uses": row["uses"]}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _proposal(row):
|
|
142
|
+
where = _scope_view(row["scope"])
|
|
143
|
+
return {"id": row["id"], "text": row["text"], "scope": row["scope"], "app": where["app"],
|
|
144
|
+
"threadId": row["thread_id"], "runId": row["run_id"], "status": row["status"], "pin": bool(row["pin"]),
|
|
145
|
+
"createdAt": millis(row["created_at"]), "resolvedAt": millis(row["resolved_at"]),
|
|
146
|
+
"expiresAt": millis(row["created_at"] + PROPOSAL_DAYS * DAY) if row["status"] == "pending" else None,
|
|
147
|
+
"factId": row["fact_id"]}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def prepare(text, kind, scope, source, *, ref=None, confidence=None, key=None):
|
|
151
|
+
"""The checked parts of a fact, or APIError 400: its text (no secrets), kind, scope, source (only a person's
|
|
152
|
+
words may be a rule or a preference), reference, confidence and key."""
|
|
153
|
+
kind = clean_kind(kind)
|
|
154
|
+
return {"text": clean_text(text), "kind": kind, "scope": clean_scope(scope), "source": clean_source(source, kind),
|
|
155
|
+
"ref": clean_ref(ref), "confidence": clean_confidence(confidence), "key": clean_key(key)}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class MemoryStore:
|
|
159
|
+
"""Facts, suggestions and the memory settings. Every method opens and closes its own connection."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, folder, *, clock=time.time):
|
|
162
|
+
self.folder = Path(folder)
|
|
163
|
+
self.path = self.folder / FILE
|
|
164
|
+
self.clock = clock
|
|
165
|
+
|
|
166
|
+
def exists(self):
|
|
167
|
+
return self.path.is_file()
|
|
168
|
+
|
|
169
|
+
def _prepare(self, write):
|
|
170
|
+
if write:
|
|
171
|
+
self.folder.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
directory = os.open(self.folder, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
173
|
+
try:
|
|
174
|
+
if os.fstat(directory).st_uid != os.getuid():
|
|
175
|
+
raise OSError("Memory folder has an unexpected owner")
|
|
176
|
+
os.fchmod(directory, 0o700)
|
|
177
|
+
finally:
|
|
178
|
+
os.close(directory)
|
|
179
|
+
flags = os.O_RDWR | os.O_NOFOLLOW | os.O_NONBLOCK | (os.O_CREAT if write else 0)
|
|
180
|
+
fd = os.open(self.path, flags, 0o600)
|
|
181
|
+
try:
|
|
182
|
+
info = os.fstat(fd)
|
|
183
|
+
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid():
|
|
184
|
+
raise OSError("Memory store has an unexpected owner or type")
|
|
185
|
+
os.fchmod(fd, 0o600)
|
|
186
|
+
finally:
|
|
187
|
+
os.close(fd)
|
|
188
|
+
|
|
189
|
+
@contextmanager
|
|
190
|
+
def _connect(self, write=False):
|
|
191
|
+
"""A connection, or None for a read when there is no file yet (reading never creates the store). A file
|
|
192
|
+
SQLite can't read is moved aside (``memory.sqlite3.corrupt-<time>``) and memory starts empty."""
|
|
193
|
+
try:
|
|
194
|
+
if not write and not os.path.lexists(self.path):
|
|
195
|
+
yield None
|
|
196
|
+
return
|
|
197
|
+
self._prepare(write)
|
|
198
|
+
conn = self._open()
|
|
199
|
+
except (OSError, sqlite3.Error) as error:
|
|
200
|
+
raise APIError(UNAVAILABLE, 503, "memory_unavailable") from error
|
|
201
|
+
try:
|
|
202
|
+
yield conn
|
|
203
|
+
except sqlite3.Error as error:
|
|
204
|
+
raise APIError(UNAVAILABLE, 503, "memory_unavailable") from error
|
|
205
|
+
finally:
|
|
206
|
+
conn.close()
|
|
207
|
+
|
|
208
|
+
def _open(self, again=True):
|
|
209
|
+
conn = sqlite3.connect(self.path, timeout=2.0, isolation_level=None, check_same_thread=False)
|
|
210
|
+
conn.row_factory = sqlite3.Row
|
|
211
|
+
try:
|
|
212
|
+
conn.execute("PRAGMA busy_timeout = 2000")
|
|
213
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
214
|
+
# A deleted fact, or a suggestion's words once answered, is overwritten in the file, not only unlinked:
|
|
215
|
+
# some SQLite builds (not every Python's) leave freed text readable until the page is reused.
|
|
216
|
+
conn.execute("PRAGMA secure_delete = ON")
|
|
217
|
+
self._migrate(conn)
|
|
218
|
+
return conn
|
|
219
|
+
except sqlite3.DatabaseError as error:
|
|
220
|
+
conn.close()
|
|
221
|
+
if not again or not any(word in str(error).lower() for word in ("not a database", "malformed", "corrupt")):
|
|
222
|
+
raise
|
|
223
|
+
except BaseException:
|
|
224
|
+
conn.close()
|
|
225
|
+
raise
|
|
226
|
+
stamp = time.strftime("%Y%m%d-%H%M%S", time.gmtime(self.clock()))
|
|
227
|
+
for suffix in ("", "-wal", "-shm"):
|
|
228
|
+
try:
|
|
229
|
+
os.replace(f"{self.path}{suffix}", f"{self.path}.corrupt-{stamp}{suffix}")
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
pass
|
|
232
|
+
self._prepare(True)
|
|
233
|
+
return self._open(again=False)
|
|
234
|
+
|
|
235
|
+
def _migrate(self, conn):
|
|
236
|
+
version = conn.execute("PRAGMA user_version").fetchone()[0]
|
|
237
|
+
if version > SCHEMA_VERSION:
|
|
238
|
+
raise APIError(NEWER, 503, "newer_data")
|
|
239
|
+
if version == SCHEMA_VERSION:
|
|
240
|
+
return
|
|
241
|
+
# Switching to WAL takes the whole file, and SQLite answers "database is locked" at once (its busy timeout
|
|
242
|
+
# doesn't apply) while another store, here or in another process, is creating it: try again until it's done.
|
|
243
|
+
deadline = time.monotonic() + 2
|
|
244
|
+
while conn.execute("PRAGMA journal_mode").fetchone()[0] != "wal":
|
|
245
|
+
try:
|
|
246
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
247
|
+
except sqlite3.OperationalError:
|
|
248
|
+
if time.monotonic() > deadline:
|
|
249
|
+
raise
|
|
250
|
+
time.sleep(.02)
|
|
251
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
252
|
+
try:
|
|
253
|
+
version = conn.execute("PRAGMA user_version").fetchone()[0]
|
|
254
|
+
if version < SCHEMA_VERSION:
|
|
255
|
+
if version == 1:
|
|
256
|
+
conn.execute("ALTER TABLE facts RENAME TO facts_v1")
|
|
257
|
+
# Statement by statement: executescript would commit the open transaction first.
|
|
258
|
+
for statement in (part.strip() for part in SCHEMA.split(";")):
|
|
259
|
+
if statement:
|
|
260
|
+
conn.execute(statement)
|
|
261
|
+
if version == 1:
|
|
262
|
+
self._upgrade_facts(conn)
|
|
263
|
+
conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
264
|
+
conn.execute("COMMIT")
|
|
265
|
+
except BaseException:
|
|
266
|
+
conn.execute("ROLLBACK")
|
|
267
|
+
raise
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _upgrade_facts(conn):
|
|
271
|
+
"""Version 1 held only what the person wrote: each fact becomes a "value" (a pinned one a "preference")
|
|
272
|
+
from the user, with the fingerprint of its words."""
|
|
273
|
+
for row in conn.execute("SELECT * FROM facts_v1").fetchall():
|
|
274
|
+
conn.execute(
|
|
275
|
+
"INSERT OR IGNORE INTO facts(id, scope, text, kind, source, confidence, fingerprint, pinned, "
|
|
276
|
+
"created_at, updated_at, last_used_at, uses) VALUES (?, ?, ?, ?, 'user', 1, ?, ?, ?, ?, ?, ?)",
|
|
277
|
+
(row["id"], row["scope"], row["text"], "preference" if row["pinned"] else "value",
|
|
278
|
+
fingerprint(row["scope"], row["text"]), row["pinned"], row["created_at"], row["updated_at"],
|
|
279
|
+
row["last_used_at"], row["uses"]))
|
|
280
|
+
conn.execute("DROP TABLE facts_v1")
|
|
281
|
+
|
|
282
|
+
@contextmanager
|
|
283
|
+
def _write(self):
|
|
284
|
+
with self._connect(write=True) as conn:
|
|
285
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
286
|
+
try:
|
|
287
|
+
yield conn
|
|
288
|
+
conn.execute("COMMIT")
|
|
289
|
+
except BaseException:
|
|
290
|
+
conn.execute("ROLLBACK")
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
def _looked(self, conn):
|
|
294
|
+
row = conn.execute("SELECT value FROM settings WHERE key = 'lookedAt'").fetchone()
|
|
295
|
+
try:
|
|
296
|
+
return float(row["value"]) if row else 0.0
|
|
297
|
+
except ValueError:
|
|
298
|
+
return 0.0
|
|
299
|
+
|
|
300
|
+
def _view(self, conn, rows):
|
|
301
|
+
looked, now = self._looked(conn), self.clock()
|
|
302
|
+
return [_fact(row, looked, now) for row in rows]
|
|
303
|
+
|
|
304
|
+
def _one(self, conn, fact_id):
|
|
305
|
+
row = conn.execute("SELECT * FROM facts WHERE id = ?", (fact_id,)).fetchone()
|
|
306
|
+
if row is None:
|
|
307
|
+
raise APIError(GONE, 404, "not_found")
|
|
308
|
+
return self._view(conn, [row])[0]
|
|
309
|
+
|
|
310
|
+
def list_facts(self, scope=None, q=None, kind=None, source=None, new=False):
|
|
311
|
+
"""Pinned first, then the newest. ``scope``: "global", "app:<bundle>", "project:<id>" and so on, or several
|
|
312
|
+
separated by commas; ``kind`` and ``source`` narrow to one; ``new``: only what Mobster learned since the
|
|
313
|
+
person last looked and they haven't kept; ``q``: words that must all appear."""
|
|
314
|
+
scopes = [clean_scope(part.strip()) for part in str(scope).split(",") if part.strip()] if scope else []
|
|
315
|
+
if kind is not None:
|
|
316
|
+
kind = clean_kind(kind)
|
|
317
|
+
if source is not None and source not in model.SOURCES:
|
|
318
|
+
raise APIError(f"A fact's source is one of: {', '.join(model.SOURCES)}.", 400, "invalid_source")
|
|
319
|
+
with self._connect() as conn:
|
|
320
|
+
if conn is None:
|
|
321
|
+
return []
|
|
322
|
+
rows = conn.execute("SELECT * FROM facts ORDER BY pinned DESC, updated_at DESC, rowid DESC").fetchall()
|
|
323
|
+
facts = self._view(conn, rows)
|
|
324
|
+
if scopes:
|
|
325
|
+
facts = [fact for fact in facts if fact["scope"] in scopes]
|
|
326
|
+
if kind:
|
|
327
|
+
facts = [fact for fact in facts if fact["kind"] == kind]
|
|
328
|
+
if source:
|
|
329
|
+
facts = [fact for fact in facts if fact["source"] == source]
|
|
330
|
+
if new:
|
|
331
|
+
facts = [fact for fact in facts if fact["new"]]
|
|
332
|
+
words = [word for word in str(q or "").lower().split() if word]
|
|
333
|
+
if words:
|
|
334
|
+
facts = [fact for fact in facts
|
|
335
|
+
if all(word in (fact["text"] + " " + ((fact["app"] or {}).get("name") or "")).lower()
|
|
336
|
+
for word in words)]
|
|
337
|
+
return facts
|
|
338
|
+
|
|
339
|
+
def get_fact(self, fact_id):
|
|
340
|
+
with self._connect() as conn:
|
|
341
|
+
if conn is None:
|
|
342
|
+
raise APIError(GONE, 404, "not_found")
|
|
343
|
+
return self._one(conn, fact_id)
|
|
344
|
+
|
|
345
|
+
def count(self):
|
|
346
|
+
with self._connect() as conn:
|
|
347
|
+
return conn.execute("SELECT count(*) FROM facts").fetchone()[0] if conn else 0
|
|
348
|
+
|
|
349
|
+
def _check_pins(self, conn, adding_chars, excluding=None):
|
|
350
|
+
rows = conn.execute("SELECT id, text FROM facts WHERE pinned = 1").fetchall()
|
|
351
|
+
rows = [row for row in rows if row["id"] != excluding]
|
|
352
|
+
if len(rows) >= MAX_PINNED:
|
|
353
|
+
raise APIError(f"You can pin up to {MAX_PINNED} things. Unpin one first.", 409, "too_many_pinned",
|
|
354
|
+
limit=MAX_PINNED)
|
|
355
|
+
if sum(len(row["text"]) for row in rows) + adding_chars > PINNED_CHARS:
|
|
356
|
+
raise APIError(f"Pinned things can be {PINNED_CHARS:,} characters together. Unpin one first.", 409,
|
|
357
|
+
"pinned_too_long", limit=PINNED_CHARS)
|
|
358
|
+
|
|
359
|
+
def _make_room(self, conn):
|
|
360
|
+
"""True when there is room for one more fact: at the limit the learned, unpinned, unkept fact used least
|
|
361
|
+
long ago goes. What a person wrote or kept never does."""
|
|
362
|
+
if conn.execute("SELECT count(*) FROM facts").fetchone()[0] < MAX_FACTS:
|
|
363
|
+
return True
|
|
364
|
+
oldest = conn.execute("SELECT id FROM facts WHERE source != 'user' AND pinned = 0 AND kept = 0 "
|
|
365
|
+
"ORDER BY COALESCE(last_used_at, created_at), rowid LIMIT 1").fetchone()
|
|
366
|
+
if oldest is None:
|
|
367
|
+
return False
|
|
368
|
+
conn.execute("DELETE FROM facts WHERE id = ?", (oldest["id"],))
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def _insert(self, conn, parts, *, pinned=False, run_id=None):
|
|
372
|
+
"""Insert a prepared fact. APIError 409 ``duplicate`` (with the fact), ``memory_full`` or a pin limit."""
|
|
373
|
+
print_ = fingerprint(parts["scope"], parts["text"], parts["key"])
|
|
374
|
+
same = conn.execute("SELECT * FROM facts WHERE fingerprint = ?", (print_,)).fetchone()
|
|
375
|
+
if same is not None:
|
|
376
|
+
raise APIError("Mobster already remembers this.", 409, "duplicate", fact=self._view(conn, [same])[0])
|
|
377
|
+
if not self._make_room(conn):
|
|
378
|
+
raise APIError(f"Mobster remembers up to {MAX_FACTS} things. Delete some first.", 409, "memory_full",
|
|
379
|
+
limit=MAX_FACTS)
|
|
380
|
+
if pinned:
|
|
381
|
+
self._check_pins(conn, len(parts["text"]))
|
|
382
|
+
now = self.clock()
|
|
383
|
+
fact_id = new_id()
|
|
384
|
+
conn.execute("INSERT INTO facts(id, scope, text, kind, source, ref, run_id, confidence, key, fingerprint, "
|
|
385
|
+
"pinned, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
386
|
+
(fact_id, parts["scope"], parts["text"], parts["kind"], parts["source"], json.dumps(parts["ref"]),
|
|
387
|
+
run_id, parts["confidence"], parts["key"], print_, int(bool(pinned)), now, now))
|
|
388
|
+
return self._one(conn, fact_id)
|
|
389
|
+
|
|
390
|
+
def add_fact(self, text, scope="global", pinned=False, kind=None):
|
|
391
|
+
"""A fact the person wrote themselves; saying it again lifts a Forget."""
|
|
392
|
+
parts = prepare(text, kind or ("preference" if pinned else "value"), scope, "user")
|
|
393
|
+
with self._write() as conn:
|
|
394
|
+
fact = self._insert(conn, parts, pinned=pinned)
|
|
395
|
+
conn.execute("DELETE FROM forgotten WHERE fingerprint = ?",
|
|
396
|
+
(fingerprint(parts["scope"], parts["text"], parts["key"]),))
|
|
397
|
+
return fact
|
|
398
|
+
|
|
399
|
+
def update_fact(self, fact_id, text=None, scope=None, pinned=None, kind=None):
|
|
400
|
+
"""The person's edit: a fact they edit is theirs from then on (a run won't overwrite its words) and counts as
|
|
401
|
+
reviewed."""
|
|
402
|
+
clean = clean_text(text) if text is not None else None
|
|
403
|
+
scope = clean_scope(scope) if scope is not None else None
|
|
404
|
+
with self._write() as conn:
|
|
405
|
+
row = conn.execute("SELECT * FROM facts WHERE id = ?", (fact_id,)).fetchone()
|
|
406
|
+
if row is None:
|
|
407
|
+
raise APIError(GONE, 404, "not_found")
|
|
408
|
+
new_kind = clean_kind(kind) if kind is not None else row["kind"]
|
|
409
|
+
if new_kind != row["kind"] and new_kind in model.INSTRUCTIONAL and row["source"] not in model.INSTRUCTORS:
|
|
410
|
+
clean_source(row["source"], new_kind) # raises not_an_instruction
|
|
411
|
+
new_text = clean if clean is not None else row["text"]
|
|
412
|
+
new_scope = scope if scope is not None else row["scope"]
|
|
413
|
+
new_pinned = bool(row["pinned"]) if pinned is None else bool(pinned)
|
|
414
|
+
new_print = fingerprint(new_scope, new_text, row["key"])
|
|
415
|
+
if new_print != row["fingerprint"]:
|
|
416
|
+
other = conn.execute("SELECT * FROM facts WHERE fingerprint = ? AND id != ?",
|
|
417
|
+
(new_print, fact_id)).fetchone()
|
|
418
|
+
if other is not None:
|
|
419
|
+
raise APIError("Mobster already remembers this.", 409, "duplicate",
|
|
420
|
+
fact=self._view(conn, [other])[0])
|
|
421
|
+
if new_pinned and (not row["pinned"] or len(new_text) > len(row["text"])):
|
|
422
|
+
self._check_pins(conn, len(new_text), excluding=fact_id)
|
|
423
|
+
changed = (new_text, new_scope, new_kind) != (row["text"], row["scope"], row["kind"])
|
|
424
|
+
conn.execute("UPDATE facts SET text = ?, scope = ?, kind = ?, pinned = ?, fingerprint = ?, "
|
|
425
|
+
"edited = ?, kept = ?, updated_at = ? WHERE id = ?",
|
|
426
|
+
(new_text, new_scope, new_kind, int(new_pinned), new_print, int(changed or row["edited"]),
|
|
427
|
+
int(changed or row["kept"]), self.clock(), fact_id))
|
|
428
|
+
return self._one(conn, fact_id)
|
|
429
|
+
|
|
430
|
+
def keep(self, fact_id):
|
|
431
|
+
"""Keep a learned fact: it leaves "New since you last looked" and is never offered for removal."""
|
|
432
|
+
with self._write() as conn:
|
|
433
|
+
if conn.execute("UPDATE facts SET kept = 1 WHERE id = ?", (fact_id,)).rowcount == 0:
|
|
434
|
+
raise APIError(GONE, 404, "not_found")
|
|
435
|
+
return self._one(conn, fact_id)
|
|
436
|
+
|
|
437
|
+
def forget(self, fact_id):
|
|
438
|
+
"""Remove a fact and keep its fingerprint for 90 days, so no run or check teaches it again. Returns the fact."""
|
|
439
|
+
with self._write() as conn:
|
|
440
|
+
fact = self._one(conn, fact_id)
|
|
441
|
+
row = conn.execute("SELECT fingerprint FROM facts WHERE id = ?", (fact_id,)).fetchone()
|
|
442
|
+
conn.execute("DELETE FROM facts WHERE id = ?", (fact_id,))
|
|
443
|
+
conn.execute("INSERT OR REPLACE INTO forgotten(fingerprint, forgotten_at) VALUES (?, ?)",
|
|
444
|
+
(row["fingerprint"], self.clock()))
|
|
445
|
+
conn.execute("DELETE FROM run_use WHERE fact_id = ?", (fact_id,))
|
|
446
|
+
if fact["scopeKind"] == "project":
|
|
447
|
+
from . import project
|
|
448
|
+
project.export_file(self, fact["scopeId"])
|
|
449
|
+
return fact
|
|
450
|
+
|
|
451
|
+
def mark_looked(self):
|
|
452
|
+
"""The person has opened the page: what was new is no longer new. Returns the time (milliseconds)."""
|
|
453
|
+
now = self.clock()
|
|
454
|
+
with self._write() as conn:
|
|
455
|
+
conn.execute("INSERT INTO settings(key, value) VALUES ('lookedAt', ?) "
|
|
456
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value", (repr(now),))
|
|
457
|
+
return millis(now)
|
|
458
|
+
|
|
459
|
+
def all_facts(self):
|
|
460
|
+
"""Every fact as a light row (retrieval). Never creates the store."""
|
|
461
|
+
with self._connect() as conn:
|
|
462
|
+
if conn is None:
|
|
463
|
+
return []
|
|
464
|
+
return [dict(row) for row in conn.execute(
|
|
465
|
+
"SELECT id, scope, text, kind, source, confidence, pinned, kept, updated_at, last_used_at, uses "
|
|
466
|
+
"FROM facts")]
|
|
467
|
+
|
|
468
|
+
def mark_used(self, ids, run_id=None):
|
|
469
|
+
"""Record that a task used these facts (best effort: a busy file never holds up a task)."""
|
|
470
|
+
if not ids:
|
|
471
|
+
return
|
|
472
|
+
try:
|
|
473
|
+
with self._write() as conn:
|
|
474
|
+
now = self.clock()
|
|
475
|
+
conn.executemany("UPDATE facts SET last_used_at = ?, uses = uses + 1 WHERE id = ?",
|
|
476
|
+
[(now, fact_id) for fact_id in ids])
|
|
477
|
+
if run_id:
|
|
478
|
+
conn.execute("DELETE FROM run_use WHERE at < ?", (now - USE_DAYS * DAY,))
|
|
479
|
+
conn.executemany("INSERT OR IGNORE INTO run_use(run_id, fact_id, at) VALUES (?, ?, ?)",
|
|
480
|
+
[(run_id, fact_id, now) for fact_id in ids])
|
|
481
|
+
except APIError:
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
def learn(self, text, kind, scope, source, *, ref=None, run_id=None, key=None, confidence=0.7,
|
|
485
|
+
authoritative=False):
|
|
486
|
+
"""A fact Mobster learned from use (a run, a lesson, a check, the repo, a profile). Returns
|
|
487
|
+
``{"status", "fact"}``, never raises for a refusal: ``added``, ``updated`` (a ``key``'s value changed),
|
|
488
|
+
``exists``, ``forgotten`` (the person forgot it lately), ``off`` (Suggest things to remember is off),
|
|
489
|
+
``full`` or ``refused`` (a secret, an unchecked kind, a rule from a screen, bad input). 503 still raises.
|
|
490
|
+
``authoritative``: the person just changed the thing this fact states (a profile's voice), so it overrides an
|
|
491
|
+
edit and a Forget."""
|
|
492
|
+
try:
|
|
493
|
+
if source == "user":
|
|
494
|
+
raise APIError("What you write yourself is added with add_fact.", 400, "invalid_source")
|
|
495
|
+
parts = prepare(text, kind, scope, source, ref=ref, confidence=confidence, key=key)
|
|
496
|
+
except APIError as error:
|
|
497
|
+
if error.status != 400:
|
|
498
|
+
raise
|
|
499
|
+
return {"status": "refused", "code": error.code, "fact": None}
|
|
500
|
+
with self._write() as conn:
|
|
501
|
+
return self._learn(conn, parts, run_id, authoritative)
|
|
502
|
+
|
|
503
|
+
def _learn(self, conn, parts, run_id, authoritative=False):
|
|
504
|
+
row = conn.execute("SELECT value FROM settings WHERE key = 'suggest'").fetchone()
|
|
505
|
+
if row is not None and row["value"] != "1":
|
|
506
|
+
return {"status": "off", "fact": None}
|
|
507
|
+
now = self.clock()
|
|
508
|
+
print_ = fingerprint(parts["scope"], parts["text"], parts["key"])
|
|
509
|
+
conn.execute("DELETE FROM forgotten WHERE forgotten_at < ?", (now - QUIET_DAYS * DAY,))
|
|
510
|
+
if authoritative:
|
|
511
|
+
conn.execute("DELETE FROM forgotten WHERE fingerprint = ?", (print_,))
|
|
512
|
+
elif conn.execute("SELECT 1 FROM forgotten WHERE fingerprint = ?", (print_,)).fetchone():
|
|
513
|
+
return {"status": "forgotten", "fact": None}
|
|
514
|
+
same = conn.execute("SELECT * FROM facts WHERE fingerprint = ?", (print_,)).fetchone()
|
|
515
|
+
if same is not None:
|
|
516
|
+
status = "exists"
|
|
517
|
+
if (parts["key"] and (authoritative or not (same["edited"] or same["kept"] or same["pinned"]))
|
|
518
|
+
and same["source"] != "user" and same["kind"] == parts["kind"]
|
|
519
|
+
and same["text"] != parts["text"]):
|
|
520
|
+
conn.execute("UPDATE facts SET text = ?, confidence = ?, source = ?, ref = ?, run_id = ?, kept = 0, "
|
|
521
|
+
"updated_at = ? WHERE id = ?", (parts["text"], parts["confidence"], parts["source"],
|
|
522
|
+
json.dumps(parts["ref"]), run_id, now, same["id"]))
|
|
523
|
+
status = "updated"
|
|
524
|
+
return {"status": status, "fact": self._one(conn, same["id"])}
|
|
525
|
+
try:
|
|
526
|
+
fact = self._insert(conn, parts, run_id=run_id)
|
|
527
|
+
except APIError as error:
|
|
528
|
+
if error.code == "memory_full":
|
|
529
|
+
return {"status": "full", "fact": None}
|
|
530
|
+
raise
|
|
531
|
+
return {"status": "added", "fact": fact}
|
|
532
|
+
|
|
533
|
+
def drop(self, scope, key):
|
|
534
|
+
"""Remove the fact a ``key`` names in a scope, without keeping its fingerprint: the thing it stated is gone
|
|
535
|
+
(a profile's voice cleared, a profile removed). Returns how many went."""
|
|
536
|
+
scope, key = clean_scope(scope), clean_key(key)
|
|
537
|
+
with self._write() as conn:
|
|
538
|
+
return conn.execute("DELETE FROM facts WHERE fingerprint = ?", (fingerprint(scope, "", key),)).rowcount
|
|
539
|
+
|
|
540
|
+
def drop_scope(self, scope):
|
|
541
|
+
"""Remove every fact in a scope that Mobster wrote for it (a removed profile's), not the person's own."""
|
|
542
|
+
scope = clean_scope(scope)
|
|
543
|
+
with self._write() as conn:
|
|
544
|
+
return conn.execute("DELETE FROM facts WHERE scope = ? AND source != 'user'", (scope,)).rowcount
|
|
545
|
+
|
|
546
|
+
def link_project(self, project_id, path):
|
|
547
|
+
"""A project's id and its folder, so its facts can travel in ``.mobster/memory.yaml``. Facts written under
|
|
548
|
+
the id derived from the folder (before the project had one) move to ``project_id``. Returns how many moved."""
|
|
549
|
+
scope = clean_scope(f"project:{project_id}")
|
|
550
|
+
if not isinstance(path, str) or not path or len(path) > 1000:
|
|
551
|
+
raise APIError("A project's folder is a path.", 400, "invalid_path")
|
|
552
|
+
path = os.path.realpath(path)
|
|
553
|
+
from . import project
|
|
554
|
+
old = f"project:{project.derived_id(path)}"
|
|
555
|
+
with self._write() as conn:
|
|
556
|
+
conn.execute("INSERT INTO projects(id, path) VALUES (?, ?) ON CONFLICT(id) "
|
|
557
|
+
"DO UPDATE SET path = excluded.path",
|
|
558
|
+
(project_id, path))
|
|
559
|
+
moved = 0
|
|
560
|
+
if old != scope:
|
|
561
|
+
for row in conn.execute("SELECT id, text, key FROM facts WHERE scope = ?", (old,)).fetchall():
|
|
562
|
+
new_print = fingerprint(scope, row["text"], row["key"])
|
|
563
|
+
if conn.execute("SELECT 1 FROM facts WHERE fingerprint = ?", (new_print,)).fetchone():
|
|
564
|
+
continue
|
|
565
|
+
conn.execute("UPDATE facts SET scope = ?, fingerprint = ? WHERE id = ?",
|
|
566
|
+
(scope, new_print, row["id"]))
|
|
567
|
+
moved += 1
|
|
568
|
+
conn.execute("DELETE FROM projects WHERE id = ?", (old.partition(":")[2],))
|
|
569
|
+
return moved
|
|
570
|
+
|
|
571
|
+
def project_path(self, project_id):
|
|
572
|
+
with self._connect() as conn:
|
|
573
|
+
row = conn.execute("SELECT path FROM projects WHERE id = ?", (project_id,)).fetchone() if conn else None
|
|
574
|
+
return row["path"] if row else None
|
|
575
|
+
|
|
576
|
+
def project_id_for(self, path):
|
|
577
|
+
with self._connect() as conn:
|
|
578
|
+
row = conn.execute("SELECT id FROM projects WHERE path = ?", (path,)).fetchone() if conn else None
|
|
579
|
+
return row["id"] if row else None
|
|
580
|
+
|
|
581
|
+
def stage(self, run_id, items):
|
|
582
|
+
"""What a run learned, held until it finishes. ``items``: dicts with ``learn``'s arguments (text, kind, scope,
|
|
583
|
+
source, and optionally ref, key, confidence). Each is checked now, so a secret is never even held. Returns
|
|
584
|
+
how many were held."""
|
|
585
|
+
held = []
|
|
586
|
+
for item in items:
|
|
587
|
+
try:
|
|
588
|
+
parts = prepare(item.get("text"), item.get("kind"), item.get("scope"), item.get("source"),
|
|
589
|
+
ref=item.get("ref"), confidence=item.get("confidence", 0.7), key=item.get("key"))
|
|
590
|
+
except APIError as error:
|
|
591
|
+
if error.status != 400:
|
|
592
|
+
raise
|
|
593
|
+
continue
|
|
594
|
+
held.append(parts)
|
|
595
|
+
if not held:
|
|
596
|
+
return 0
|
|
597
|
+
with self._write() as conn:
|
|
598
|
+
now = self.clock()
|
|
599
|
+
conn.execute("DELETE FROM staged WHERE created_at < ?", (now - STAGED_DAYS * DAY,))
|
|
600
|
+
room = 50 - conn.execute("SELECT count(*) FROM staged WHERE run_id = ?", (run_id,)).fetchone()[0]
|
|
601
|
+
for parts in held[:max(0, room)]:
|
|
602
|
+
conn.execute("INSERT INTO staged(id, run_id, payload, created_at) VALUES (?, ?, ?, ?)",
|
|
603
|
+
(new_id(), run_id, json.dumps(parts), now))
|
|
604
|
+
return len(held[:max(0, room)])
|
|
605
|
+
|
|
606
|
+
def commit_run(self, run_id, *, keep=True):
|
|
607
|
+
"""The run finished with a result the person didn't dispute: what it staged becomes facts (``keep=False``
|
|
608
|
+
drops it instead). Returns each one's ``learn`` result."""
|
|
609
|
+
with self._write() as conn:
|
|
610
|
+
rows = conn.execute("SELECT * FROM staged WHERE run_id = ? ORDER BY created_at, rowid",
|
|
611
|
+
(run_id,)).fetchall()
|
|
612
|
+
conn.execute("DELETE FROM staged WHERE run_id = ?", (run_id,))
|
|
613
|
+
if not keep:
|
|
614
|
+
return []
|
|
615
|
+
return [self._learn(conn, json.loads(row["payload"]), run_id) for row in rows]
|
|
616
|
+
|
|
617
|
+
def run_memory(self, run_id):
|
|
618
|
+
"""What a run used and what it learned, for its view: {"used": [...], "learned": [...], "pending": n}."""
|
|
619
|
+
with self._connect() as conn:
|
|
620
|
+
if conn is None:
|
|
621
|
+
return {"used": [], "learned": [], "pending": 0}
|
|
622
|
+
used = conn.execute("SELECT f.* FROM facts f JOIN run_use u ON u.fact_id = f.id WHERE u.run_id = ? "
|
|
623
|
+
"ORDER BY u.rowid", (run_id,)).fetchall()
|
|
624
|
+
learned = conn.execute("SELECT * FROM facts WHERE run_id = ? ORDER BY created_at, rowid",
|
|
625
|
+
(run_id,)).fetchall()
|
|
626
|
+
pending = conn.execute("SELECT count(*) FROM staged WHERE run_id = ?", (run_id,)).fetchone()[0]
|
|
627
|
+
return {"used": self._view(conn, used), "learned": self._view(conn, learned), "pending": pending}
|
|
628
|
+
|
|
629
|
+
def _expire(self, conn):
|
|
630
|
+
now = self.clock()
|
|
631
|
+
conn.execute("UPDATE proposals SET status = 'expired', resolved_at = ?, text = '' "
|
|
632
|
+
"WHERE status = 'pending' AND created_at < ?", (now, now - PROPOSAL_DAYS * DAY))
|
|
633
|
+
conn.execute("DELETE FROM proposals WHERE status IN ('dismissed', 'expired') AND resolved_at < ?",
|
|
634
|
+
(now - QUIET_DAYS * DAY,))
|
|
635
|
+
conn.execute("DELETE FROM proposals WHERE status = 'accepted' AND fact_id NOT IN (SELECT id FROM facts)")
|
|
636
|
+
|
|
637
|
+
def propose(self, text, scope="global", *, thread_id=None, run_id=None, pin=False):
|
|
638
|
+
"""A suggestion from the person's own words, or None when it isn't worth asking; never stores a fact."""
|
|
639
|
+
try:
|
|
640
|
+
text, scope = clean_text(text), clean_scope(scope)
|
|
641
|
+
except APIError:
|
|
642
|
+
return None
|
|
643
|
+
key = digest(text)
|
|
644
|
+
with self._write() as conn:
|
|
645
|
+
self._expire(conn)
|
|
646
|
+
if conn.execute("SELECT 1 FROM proposals WHERE digest = ? AND scope = ? "
|
|
647
|
+
"AND status IN ('pending', 'dismissed', 'expired')",
|
|
648
|
+
(key, scope)).fetchone():
|
|
649
|
+
return None
|
|
650
|
+
if conn.execute("SELECT 1 FROM facts WHERE fingerprint = ?", (fingerprint(scope, text),)).fetchone():
|
|
651
|
+
return None
|
|
652
|
+
pending = conn.execute("SELECT id FROM proposals WHERE status = 'pending' ORDER BY created_at, rowid"
|
|
653
|
+
).fetchall()
|
|
654
|
+
for row in pending[:max(0, len(pending) - MAX_PENDING + 1)]:
|
|
655
|
+
conn.execute("UPDATE proposals SET status = 'expired', resolved_at = ?, text = '' WHERE id = ?",
|
|
656
|
+
(self.clock(), row["id"]))
|
|
657
|
+
proposal_id = new_id()
|
|
658
|
+
conn.execute("INSERT INTO proposals(id, text, scope, thread_id, run_id, status, created_at, digest, pin) "
|
|
659
|
+
"VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)",
|
|
660
|
+
(proposal_id, text, scope, thread_id, run_id, self.clock(), key, int(bool(pin))))
|
|
661
|
+
return _proposal(conn.execute("SELECT * FROM proposals WHERE id = ?", (proposal_id,)).fetchone())
|
|
662
|
+
|
|
663
|
+
def set_proposal_item(self, proposal_id, item_id):
|
|
664
|
+
"""The thread item that shows this suggestion (conversations' sink), so answering it updates the card."""
|
|
665
|
+
with self._write() as conn:
|
|
666
|
+
conn.execute("UPDATE proposals SET item_id = ? WHERE id = ?", (str(item_id)[:64], proposal_id))
|
|
667
|
+
|
|
668
|
+
def proposal_item(self, proposal_id):
|
|
669
|
+
"""(thread id, item id) of the card that shows the suggestion, or (None, None)."""
|
|
670
|
+
with self._connect() as conn:
|
|
671
|
+
row = conn.execute("SELECT thread_id, item_id FROM proposals WHERE id = ?",
|
|
672
|
+
(proposal_id,)).fetchone() if conn else None
|
|
673
|
+
return (row["thread_id"], row["item_id"]) if row else (None, None)
|
|
674
|
+
|
|
675
|
+
def list_proposals(self, thread_id=None, status="pending"):
|
|
676
|
+
if status not in PROPOSAL_STATES + ("all",):
|
|
677
|
+
raise APIError("Status is pending, accepted, dismissed, expired or all.", 400, "invalid_status")
|
|
678
|
+
if not self.exists():
|
|
679
|
+
return []
|
|
680
|
+
with self._write() as conn:
|
|
681
|
+
self._expire(conn)
|
|
682
|
+
rows = conn.execute("SELECT * FROM proposals ORDER BY created_at DESC, rowid DESC").fetchall()
|
|
683
|
+
out = [_proposal(row) for row in rows]
|
|
684
|
+
if status != "all":
|
|
685
|
+
out = [p for p in out if p["status"] == status]
|
|
686
|
+
if thread_id:
|
|
687
|
+
out = [p for p in out if p["threadId"] == thread_id]
|
|
688
|
+
return out
|
|
689
|
+
|
|
690
|
+
def resolve_proposal(self, proposal_id, accept, text=None, pinned=None):
|
|
691
|
+
"""The person's answer: ``accept`` stores the fact (with their edit, ``text``); otherwise "Not now"."""
|
|
692
|
+
if not isinstance(accept, bool):
|
|
693
|
+
raise APIError("Say whether to remember it (accept true or false).", 400, "invalid_answer")
|
|
694
|
+
edited = clean_text(text) if (accept and text is not None) else None
|
|
695
|
+
with self._write() as conn:
|
|
696
|
+
self._expire(conn)
|
|
697
|
+
row = conn.execute("SELECT * FROM proposals WHERE id = ?", (proposal_id,)).fetchone()
|
|
698
|
+
if row is None:
|
|
699
|
+
raise APIError("That suggestion is gone.", 404, "not_found")
|
|
700
|
+
if row["status"] != "pending":
|
|
701
|
+
raise APIError("That suggestion was already answered." if row["status"] != "expired"
|
|
702
|
+
else "That suggestion expired.", 409, "not_pending", proposal=_proposal(row))
|
|
703
|
+
now = self.clock()
|
|
704
|
+
fact = None
|
|
705
|
+
if accept:
|
|
706
|
+
pin = bool(row["pin"]) if pinned is None else bool(pinned)
|
|
707
|
+
fact_text = edited or row["text"]
|
|
708
|
+
try:
|
|
709
|
+
parts = prepare(fact_text, "preference" if pin else "value", row["scope"], "user")
|
|
710
|
+
fact = self._insert(conn, parts, pinned=pin)
|
|
711
|
+
except APIError as error:
|
|
712
|
+
if error.code == "duplicate":
|
|
713
|
+
fact = error.details["fact"]
|
|
714
|
+
elif error.code in ("too_many_pinned", "pinned_too_long") and pinned is None:
|
|
715
|
+
parts = prepare(fact_text, "value", row["scope"], "user")
|
|
716
|
+
fact = self._insert(conn, parts, pinned=False)
|
|
717
|
+
else:
|
|
718
|
+
raise
|
|
719
|
+
conn.execute("DELETE FROM forgotten WHERE fingerprint = ?",
|
|
720
|
+
(fingerprint(row["scope"], fact["text"]),))
|
|
721
|
+
conn.execute("UPDATE proposals SET status = 'accepted', resolved_at = ?, text = '', fact_id = ? "
|
|
722
|
+
"WHERE id = ?", (now, fact["id"], proposal_id))
|
|
723
|
+
else:
|
|
724
|
+
conn.execute("UPDATE proposals SET status = 'dismissed', resolved_at = ?, text = '' WHERE id = ?",
|
|
725
|
+
(now, proposal_id))
|
|
726
|
+
return _proposal(conn.execute("SELECT * FROM proposals WHERE id = ?", (proposal_id,)).fetchone()), fact
|
|
727
|
+
|
|
728
|
+
def settings(self):
|
|
729
|
+
values = dict(SETTINGS)
|
|
730
|
+
with self._connect() as conn:
|
|
731
|
+
if conn is not None:
|
|
732
|
+
for row in conn.execute("SELECT key, value FROM settings"):
|
|
733
|
+
if row["key"] in values:
|
|
734
|
+
values[row["key"]] = row["value"] == "1"
|
|
735
|
+
return values
|
|
736
|
+
|
|
737
|
+
def update_settings(self, changes):
|
|
738
|
+
if not isinstance(changes, dict) or not changes:
|
|
739
|
+
raise APIError("Change useInTasks or suggest.", 400, "invalid_settings")
|
|
740
|
+
for key, value in changes.items():
|
|
741
|
+
if key not in SETTINGS:
|
|
742
|
+
raise APIError(f"Unknown memory setting {key}.", 400, "invalid_settings")
|
|
743
|
+
if not isinstance(value, bool):
|
|
744
|
+
raise APIError(f"{key} is true or false.", 400, "invalid_settings")
|
|
745
|
+
with self._write() as conn:
|
|
746
|
+
conn.executemany("INSERT INTO settings(key, value) VALUES (?, ?) "
|
|
747
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
748
|
+
[(key, "1" if value else "0") for key, value in changes.items()])
|
|
749
|
+
return self.settings()
|
|
750
|
+
|
|
751
|
+
def clear(self):
|
|
752
|
+
"""Delete every fact, suggestion, forgotten fingerprint and project link (settings stay). Returns how many
|
|
753
|
+
facts and suggestions went."""
|
|
754
|
+
for leftover in self.folder.glob(FILE + ".corrupt-*"):
|
|
755
|
+
leftover.unlink(missing_ok=True)
|
|
756
|
+
if not self.exists():
|
|
757
|
+
return {"facts": 0, "proposals": 0}
|
|
758
|
+
with self._write() as conn:
|
|
759
|
+
facts = conn.execute("DELETE FROM facts").rowcount
|
|
760
|
+
proposals = conn.execute("DELETE FROM proposals").rowcount
|
|
761
|
+
for table in ("forgotten", "staged", "run_use", "projects"):
|
|
762
|
+
conn.execute(f"DELETE FROM {table}")
|
|
763
|
+
conn.execute("DELETE FROM settings WHERE key = 'lookedAt'")
|
|
764
|
+
# Older builds created these unused tables. Clearing memory erases anything left in them too.
|
|
765
|
+
conn.execute("DROP TABLE IF EXISTS routine_versions")
|
|
766
|
+
conn.execute("DROP TABLE IF EXISTS routines")
|
|
767
|
+
with self._connect() as conn:
|
|
768
|
+
try:
|
|
769
|
+
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
770
|
+
conn.execute("VACUUM")
|
|
771
|
+
except sqlite3.Error:
|
|
772
|
+
pass
|
|
773
|
+
return {"facts": facts, "proposals": proposals}
|
|
774
|
+
|
|
775
|
+
def export_json(self):
|
|
776
|
+
"""Everything Mobster remembers, as data: every fact with its kind, source, scope and last use."""
|
|
777
|
+
return {"version": SCHEMA_VERSION, "exportedAt": millis(self.clock()), "facts": self.list_facts()}
|
|
778
|
+
|
|
779
|
+
def export_markdown(self):
|
|
780
|
+
"""Everything Mobster remembers, as Markdown a person can read and keep."""
|
|
781
|
+
facts = self.list_facts()
|
|
782
|
+
lines = ["# What Mobster remembers", "",
|
|
783
|
+
"Saved on this Mac by Mobster. Pinned things go with eligible tasks in their scope; the rest go "
|
|
784
|
+
"with eligible tasks that fit.", ""]
|
|
785
|
+
if not facts:
|
|
786
|
+
lines.append("Nothing yet.")
|
|
787
|
+
groups = {}
|
|
788
|
+
for fact in facts:
|
|
789
|
+
kind = fact["scopeKind"]
|
|
790
|
+
title = ("In every app" if kind == "global"
|
|
791
|
+
else f"In {fact['app']['name'] or fact['scopeId']}" if kind == "app"
|
|
792
|
+
else f"Saved profile {fact['scopeId'].split('/workflow:')[0]} and workflow "
|
|
793
|
+
f"{fact['scopeId'].split('/workflow:')[1]}" if kind == "profile_workflow"
|
|
794
|
+
else f"Saved profile {fact['scopeId']}" if kind == "profile"
|
|
795
|
+
else f"Workflow {fact['scopeId']}" if kind == "workflow"
|
|
796
|
+
else f"Unknown scope {fact['scope']}" if kind == "unknown"
|
|
797
|
+
else f"{kind.capitalize()} {fact['scopeId']}")
|
|
798
|
+
groups.setdefault((0 if kind == "global" else 1, title.lower(), title), []).append(fact)
|
|
799
|
+
for (_, _, title), members in sorted(groups.items(), key=lambda pair: pair[0]):
|
|
800
|
+
lines += [f"## {title}", ""]
|
|
801
|
+
for fact in members:
|
|
802
|
+
marks = [mark for mark, on in (("pinned", fact["pinned"]),
|
|
803
|
+
("from " + fact["source"], fact["source"] != "user")) if on]
|
|
804
|
+
lines.append(f"- {fact['text']}{' (' + ', '.join(marks) + ')' if marks else ''}")
|
|
805
|
+
lines.append("")
|
|
806
|
+
return "\n".join(lines).rstrip() + "\n"
|