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.
Files changed (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. 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"