otto-cli-agent 0.1.1__tar.gz → 0.1.2__tar.gz

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 (323) hide show
  1. otto_cli_agent-0.1.2/.gitattributes +3 -0
  2. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/CONTRIBUTING.md +1 -1
  3. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/PKG-INFO +19 -10
  4. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/README.md +46 -7
  5. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/README.md +7 -1
  6. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/lessons.py +11 -3
  7. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/main.py +2 -0
  8. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/output.py +4 -2
  9. otto_cli_agent-0.1.2/agent/cli/serve.py +127 -0
  10. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/sessions.py +3 -5
  11. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/setup_screen.py +4 -17
  12. otto_cli_agent-0.1.2/agent/config/README.md +15 -0
  13. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/config/envfile.py +27 -8
  14. otto_cli_agent-0.1.2/agent/config/home.py +74 -0
  15. otto_cli_agent-0.1.2/agent/embed.py +1030 -0
  16. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/embeddings.py +13 -1
  17. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/lessons.py +222 -31
  18. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/sessions.py +152 -28
  19. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/store.py +18 -3
  20. otto_cli_agent-0.1.2/agent/phone/README.md +86 -0
  21. otto_cli_agent-0.1.2/agent/phone/__init__.py +29 -0
  22. otto_cli_agent-0.1.2/agent/phone/assets/app_notes/com.android.settings.md +2 -0
  23. otto_cli_agent-0.1.2/agent/phone/assets/app_notes/in.amazon.mShop.android.shopping.md +7 -0
  24. otto_cli_agent-0.1.2/agent/phone/assets/guard_pages.json +862 -0
  25. otto_cli_agent-0.1.2/agent/phone/assets/guard_rules.json +121 -0
  26. otto_cli_agent-0.1.2/agent/phone/backend.py +175 -0
  27. otto_cli_agent-0.1.2/agent/phone/digest.py +367 -0
  28. otto_cli_agent-0.1.2/agent/phone/guard.py +753 -0
  29. otto_cli_agent-0.1.2/agent/phone/notes.py +220 -0
  30. otto_cli_agent-0.1.2/agent/phone/tools.py +817 -0
  31. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/README.md +18 -2
  32. otto_cli_agent-0.1.2/agent/pipeline/_python_session_shim.py +298 -0
  33. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/nodes.py +396 -26
  34. otto_cli_agent-0.1.2/agent/pipeline/profile.py +53 -0
  35. otto_cli_agent-0.1.2/agent/pipeline/python_session.py +657 -0
  36. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/research.py +11 -2
  37. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/run.py +15 -3
  38. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/toolkit.py +70 -8
  39. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/tools.py +66 -3
  40. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/health.py +32 -0
  41. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/__init__.py +6 -1
  42. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/anthropic_provider.py +2 -1
  43. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/base.py +31 -0
  44. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/inception_provider.py +2 -1
  45. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/openai_provider.py +2 -1
  46. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/outcomes.py +4 -1
  47. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/overrides.py +93 -0
  48. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/router.py +48 -5
  49. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/setup.py +24 -0
  50. otto_cli_agent-0.1.2/agent/server/README.md +40 -0
  51. otto_cli_agent-0.1.2/agent/server/__init__.py +9 -0
  52. otto_cli_agent-0.1.2/agent/server/app.py +743 -0
  53. otto_cli_agent-0.1.2/agent/server/protocol.py +131 -0
  54. otto_cli_agent-0.1.2/agent/server/proxy.py +111 -0
  55. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/HISTORY.md +7 -1
  56. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/index.md +1 -1
  57. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/pypi.md +8 -3
  58. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/pyproject.toml +26 -8
  59. otto_cli_agent-0.1.2/scripts/phone_page_fixture.py +153 -0
  60. otto_cli_agent-0.1.2/scripts/phone_page_manifest.py +58 -0
  61. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/README.md +2 -2
  62. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/conftest.py +24 -0
  63. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_address_form_pincode.json +180 -0
  64. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_address_step.json +155 -0
  65. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_buy_now_sheet.json +101 -0
  66. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_cart.json +1164 -0
  67. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_cart_pens.json +1280 -0
  68. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_payment.json +807 -0
  69. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_payment_scrolled.json +1037 -0
  70. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_upsell.json +1736 -0
  71. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_filters.json +743 -0
  72. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_home.json +1427 -0
  73. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_order_review.json +137 -0
  74. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_order_review_scrolled.json +116 -0
  75. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product.json +1116 -0
  76. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product_buybox.json +1183 -0
  77. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product_ids.json +157 -0
  78. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_after_review.json +97 -0
  79. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_offers.json +1522 -0
  80. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_search.json +1199 -0
  81. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_sorted.json +1522 -0
  82. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_search_suggestions.json +896 -0
  83. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sign_in.json +99 -0
  84. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sort_price.json +800 -0
  85. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sort_sheet.json +667 -0
  86. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_upi_pin_screen.json +40 -0
  87. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/bank_pin_numpw.json +61 -0
  88. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/blinkit_cart_pay.json +97 -0
  89. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/blinkit_search.json +116 -0
  90. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/chat_with_otp.json +78 -0
  91. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/phonepe_upi_pin.json +59 -0
  92. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/settings_display.json +79 -0
  93. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_onboarding_continue.json +78 -0
  94. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_order_summary_continue.json +97 -0
  95. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_otp_boxes.json +147 -0
  96. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_otp_caption.json +59 -0
  97. otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_qty_total_pay.json +98 -0
  98. otto_cli_agent-0.1.2/tests/phone_fakes.py +151 -0
  99. otto_cli_agent-0.1.2/tests/test_background_distil.py +149 -0
  100. otto_cli_agent-0.1.2/tests/test_embed.py +381 -0
  101. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_embedding_backends.py +18 -0
  102. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_health.py +33 -0
  103. otto_cli_agent-0.1.2/tests/test_home.py +132 -0
  104. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_lessons.py +92 -1
  105. otto_cli_agent-0.1.2/tests/test_lessons_cli.py +54 -0
  106. otto_cli_agent-0.1.2/tests/test_optional_vendor.py +51 -0
  107. otto_cli_agent-0.1.2/tests/test_phone_app_notes.py +137 -0
  108. otto_cli_agent-0.1.2/tests/test_phone_backend.py +101 -0
  109. otto_cli_agent-0.1.2/tests/test_phone_call_budget.py +131 -0
  110. otto_cli_agent-0.1.2/tests/test_phone_decision.py +395 -0
  111. otto_cli_agent-0.1.2/tests/test_phone_digest.py +69 -0
  112. otto_cli_agent-0.1.2/tests/test_phone_do.py +228 -0
  113. otto_cli_agent-0.1.2/tests/test_phone_element_ids.py +183 -0
  114. otto_cli_agent-0.1.2/tests/test_phone_fold.py +205 -0
  115. otto_cli_agent-0.1.2/tests/test_phone_guard.py +278 -0
  116. otto_cli_agent-0.1.2/tests/test_phone_learned_notes.py +177 -0
  117. otto_cli_agent-0.1.2/tests/test_phone_page_corpus.py +110 -0
  118. otto_cli_agent-0.1.2/tests/test_phone_prompt.py +141 -0
  119. otto_cli_agent-0.1.2/tests/test_phone_seats.py +211 -0
  120. otto_cli_agent-0.1.2/tests/test_phone_shopping_digest.py +85 -0
  121. otto_cli_agent-0.1.2/tests/test_phone_tools.py +549 -0
  122. otto_cli_agent-0.1.2/tests/test_profile.py +84 -0
  123. otto_cli_agent-0.1.2/tests/test_provider_connection_detail.py +58 -0
  124. otto_cli_agent-0.1.2/tests/test_python_session.py +837 -0
  125. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_research_workflow.py +13 -0
  126. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_router.py +34 -0
  127. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_run_scoped_toolkit.py +63 -0
  128. otto_cli_agent-0.1.2/tests/test_seed_lessons.py +105 -0
  129. otto_cli_agent-0.1.2/tests/test_serve.py +1018 -0
  130. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_sessions.py +83 -0
  131. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/uv.lock +32 -13
  132. otto_cli_agent-0.1.1/agent/config/README.md +0 -7
  133. otto_cli_agent-0.1.1/tests/test_lessons_cli.py +0 -20
  134. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  135. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  136. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  137. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/workflows/publish.yml +0 -0
  138. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/workflows/tests.yml +0 -0
  139. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.gitignore +0 -0
  140. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.python-version +0 -0
  141. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/CODE_OF_CONDUCT.md +0 -0
  142. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/LICENSE +0 -0
  143. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/SECURITY.md +0 -0
  144. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/__init__.py +0 -0
  145. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/README.md +0 -0
  146. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/__init__.py +0 -0
  147. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/art.py +0 -0
  148. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/chat.py +0 -0
  149. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/clipboard.py +0 -0
  150. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/context.py +0 -0
  151. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/doctor.py +0 -0
  152. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/errors.py +0 -0
  153. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval.py +0 -0
  154. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_claw.py +0 -0
  155. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_compaction.py +0 -0
  156. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_hle.py +0 -0
  157. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_memory.py +0 -0
  158. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_swe.py +0 -0
  159. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/modals.py +0 -0
  160. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/models.py +0 -0
  161. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/route.py +0 -0
  162. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/shell.py +0 -0
  163. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/tui.py +0 -0
  164. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/ui.py +0 -0
  165. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/usage_panel.py +0 -0
  166. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/config/__init__.py +0 -0
  167. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/README.md +0 -0
  168. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/__init__.py +0 -0
  169. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/claw_bench.py +0 -0
  170. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/compaction_bench.py +0 -0
  171. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/README.md +0 -0
  172. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/README.md +0 -0
  173. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/llm_judge-gemini.patch +0 -0
  174. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/otto.yaml +0 -0
  175. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/failures.py +0 -0
  176. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/README.md +0 -0
  177. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_01.json +0 -0
  178. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_02.json +0 -0
  179. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_03.json +0 -0
  180. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_04.json +0 -0
  181. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_05.json +0 -0
  182. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_06.json +0 -0
  183. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_01.json +0 -0
  184. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_02.json +0 -0
  185. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_03.json +0 -0
  186. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_04.json +0 -0
  187. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_05.json +0 -0
  188. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_06.json +0 -0
  189. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_gcp_01.json +0 -0
  190. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_ksp_01.json +0 -0
  191. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_binpacking_01.json +0 -0
  192. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_clique_01.json +0 -0
  193. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_setcover_01.json +0 -0
  194. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_subsetsum_01.json +0 -0
  195. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_tsp_01.json +0 -0
  196. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_tsp_02.json +0 -0
  197. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/hle_bench.py +0 -0
  198. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/langfuse_sync.py +0 -0
  199. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/memory_bench.py +0 -0
  200. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/runner.py +0 -0
  201. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/single_agent.py +0 -0
  202. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/swe_bench.py +0 -0
  203. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/terminal_bench.py +0 -0
  204. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/README.md +0 -0
  205. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/__init__.py +0 -0
  206. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/hashing.py +0 -0
  207. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/queue.py +0 -0
  208. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/retrieval.py +0 -0
  209. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/session.py +0 -0
  210. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/tokens.py +0 -0
  211. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/wiring.py +0 -0
  212. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/__init__.py +0 -0
  213. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/browsing.py +0 -0
  214. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/budget.py +0 -0
  215. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/codemap.py +0 -0
  216. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/evidence.py +0 -0
  217. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/execution.py +0 -0
  218. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/modes.py +0 -0
  219. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/native.py +0 -0
  220. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/pricing.py +0 -0
  221. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/progress.py +0 -0
  222. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/rag.py +0 -0
  223. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/screen.py +0 -0
  224. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/state.py +0 -0
  225. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/tracing.py +0 -0
  226. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/usage.py +0 -0
  227. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/vision.py +0 -0
  228. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/walkthrough.py +0 -0
  229. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/workspace.py +0 -0
  230. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/README.md +0 -0
  231. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/__init__.py +0 -0
  232. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/automap.py +0 -0
  233. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/README.md +0 -0
  234. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/custom.py +0 -0
  235. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/gemini_provider.py +0 -0
  236. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/retired.py +0 -0
  237. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/temperature.py +0 -0
  238. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/mapping.py +0 -0
  239. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/reload.py +0 -0
  240. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/README.md +0 -0
  241. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/Dockerfile +0 -0
  242. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/README.md +0 -0
  243. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/start-desktop.sh +0 -0
  244. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/debug_pipeline.py +0 -0
  245. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/README.md +0 -0
  246. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/RESEARCH.md +0 -0
  247. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/_config.yml +0 -0
  248. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/design/README.md +0 -0
  249. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/design/tiered-memory.md +0 -0
  250. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/codebase-code-map.png +0 -0
  251. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/exercise-todo-cli.png +0 -0
  252. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/otto-demo.gif +0 -0
  253. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/palette-doctor-route.png +0 -0
  254. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/run-trace-and-judge.png +0 -0
  255. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/session-resumed-thinking.png +0 -0
  256. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/sessions-picker.png +0 -0
  257. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/setup-screen.png +0 -0
  258. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/social-preview.png +0 -0
  259. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_agent_loop.py +0 -0
  260. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_art.py +0 -0
  261. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_ask_user_node.py +0 -0
  262. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_automap.py +0 -0
  263. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_browsing.py +0 -0
  264. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_budget.py +0 -0
  265. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_call_budget.py +0 -0
  266. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_chat_fast_path.py +0 -0
  267. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_claw_bench.py +0 -0
  268. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_clipboard.py +0 -0
  269. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_codemap.py +0 -0
  270. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_compaction_bench.py +0 -0
  271. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_custom_endpoints.py +0 -0
  272. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_diffusion_retry.py +0 -0
  273. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_envfile.py +0 -0
  274. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_eval_honesty.py +0 -0
  275. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_eval_runner.py +0 -0
  276. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evaluator_node.py +0 -0
  277. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evicted_context.py +0 -0
  278. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evidence.py +0 -0
  279. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_failures.py +0 -0
  280. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_hle_bench.py +0 -0
  281. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_inception_provider.py +0 -0
  282. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_inception_stream.py +0 -0
  283. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_langfuse_sync.py +0 -0
  284. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_mapping.py +0 -0
  285. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_bench.py +0 -0
  286. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_embeddings.py +0 -0
  287. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_hashing.py +0 -0
  288. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_queue.py +0 -0
  289. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_retrieval.py +0 -0
  290. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_session.py +0 -0
  291. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_store.py +0 -0
  292. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_tokens.py +0 -0
  293. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_wiring.py +0 -0
  294. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_model_policies.py +0 -0
  295. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_modes.py +0 -0
  296. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_native.py +0 -0
  297. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_no_import_shadowing.py +0 -0
  298. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_output.py +0 -0
  299. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_overrides.py +0 -0
  300. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_pipeline_nodes.py +0 -0
  301. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_pipeline_run.py +0 -0
  302. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_progress.py +0 -0
  303. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_prompt_tool_sync.py +0 -0
  304. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_reload.py +0 -0
  305. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_research_router.py +0 -0
  306. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_retrieval_method.py +0 -0
  307. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_screen.py +0 -0
  308. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_seat_outcomes.py +0 -0
  309. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_setup.py +0 -0
  310. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_swe_bench.py +0 -0
  311. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_temperature_learning.py +0 -0
  312. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_terminal_bench_adapter.py +0 -0
  313. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tool_loop.py +0 -0
  314. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tools_stubs.py +0 -0
  315. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tracing.py +0 -0
  316. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui.py +0 -0
  317. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui_progress.py +0 -0
  318. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui_setup.py +0 -0
  319. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_usage.py +0 -0
  320. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_vision_tool.py +0 -0
  321. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_walkthrough.py +0 -0
  322. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_workspace_session.py +0 -0
  323. {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_workspace_tools.py +0 -0
@@ -0,0 +1,3 @@
1
+ # The page corpus manifest records each fixture's sha256 over its bytes, so no checkout may
2
+ # rewrite their line ends (Windows, core.autocrlf).
3
+ tests/fixtures/phone_pages/*.json -text
@@ -7,7 +7,7 @@ built to: say what you measured, or what you would need to measure.
7
7
 
8
8
  ```bash
9
9
  uv sync
10
- uv run pytest -q # 1,552 tests, no API key, no network, about two minutes
10
+ uv run pytest -q # 1,593 tests, no API key, no network, about two minutes
11
11
  uv run otto doctor # once you have keys in .env
12
12
  ```
13
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: otto-cli-agent
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Otto: a terminal AI agent built around measured results -- one agent loop, a rubric-first evaluator, tiered memory, multi-vendor routing, and six benchmark harnesses.
5
5
  Project-URL: Repository, https://github.com/siddharth23P/otto_agent
6
6
  Project-URL: Issues, https://github.com/siddharth23P/otto_agent/issues
@@ -38,24 +38,28 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
38
38
  Classifier: Topic :: Software Development
39
39
  Requires-Python: >=3.12
40
40
  Requires-Dist: emoji>=2.15.0
41
- Requires-Dist: fastembed>=0.8.0
42
41
  Requires-Dist: inceptionai>=0.1.3
43
42
  Requires-Dist: langchain-anthropic>=1.7.1
44
43
  Requires-Dist: langchain-google-genai>=4.4.0
45
44
  Requires-Dist: langchain-openai>=1.6.0
46
45
  Requires-Dist: langchain>=1.4.0
47
46
  Requires-Dist: langfuse>=4.15.1
48
- Requires-Dist: langgraph-cli[inmem]>=0.4.31
49
47
  Requires-Dist: langgraph>=1.2.11
50
- Requires-Dist: numpy>=2.5.3
51
- Requires-Dist: openpyxl>=3.1.5
48
+ Requires-Dist: numpy>=1.26.2
49
+ Requires-Dist: openpyxl>=3.1.5; sys_platform != 'android'
52
50
  Requires-Dist: prompt-toolkit>=3.0.53
53
- Requires-Dist: python-docx>=1.1.2
51
+ Requires-Dist: python-docx>=1.1.2; sys_platform != 'android'
54
52
  Requires-Dist: python-dotenv>=1.2.3
55
- Requires-Dist: reportlab>=4.2
53
+ Requires-Dist: reportlab>=4.2; sys_platform != 'android'
56
54
  Requires-Dist: textual>=8.2.8
57
55
  Requires-Dist: tiktoken>=0.14.0
58
56
  Requires-Dist: typer>=0.27.2
57
+ Provides-Extra: dev-server
58
+ Requires-Dist: langgraph-cli[inmem]>=0.4.31; extra == 'dev-server'
59
+ Provides-Extra: local-embeddings
60
+ Requires-Dist: fastembed>=0.8.0; extra == 'local-embeddings'
61
+ Provides-Extra: serve
62
+ Requires-Dist: websockets>=13; extra == 'serve'
59
63
  Description-Content-Type: text/markdown
60
64
 
61
65
  # Otto
@@ -63,19 +67,24 @@ Description-Content-Type: text/markdown
63
67
  A terminal AI agent that works on a codebase, a container, a browser or a
64
68
  desktop, uses what it built, and judges its own work against criteria it
65
69
  wrote before it started. One agent loop with modes over four model vendors,
66
- a rubric-first evaluator, tiered memory, six benchmark harnesses, and 1,552
67
- tests that need no API key.
70
+ a rubric-first evaluator, tiered memory, six benchmark harnesses, and 1,685
71
+ tests that need no API key. It embeds: `agent.embed` is the surface an app
72
+ with Otto's Python inside it depends on.
68
73
 
69
74
  ![One minute of otto tui](https://raw.githubusercontent.com/siddharth23P/otto_agent/main/docs/media/otto-demo.gif)
70
75
 
71
76
  ## Install
72
77
 
73
78
  ```bash
74
- pip install otto-cli-agent
79
+ pip install "otto-cli-agent[local-embeddings]"
75
80
  otto doctor
76
81
  otto tui
77
82
  ```
78
83
 
84
+ The `local-embeddings` extra is the on-device embedding model; without it
85
+ recall uses a Gemini key when there is one, and recency otherwise. `[serve]`
86
+ adds `otto serve`, the agent behind a WebSocket for the phone app.
87
+
79
88
  Put keys in a `.env` in the directory you run it from. `INCEPTION_API_KEY`
80
89
  is required (it alone serves the fill-in-the-middle and edit endpoints);
81
90
  `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` and `GEMINI_API_KEY` are optional and
@@ -4,7 +4,7 @@
4
4
  ![python](https://img.shields.io/badge/python-3.12-blue)
5
5
  ![license](https://img.shields.io/badge/license-MIT-green)
6
6
  [![PyPI](https://img.shields.io/pypi/v/otto-cli-agent?label=pypi)](https://pypi.org/project/otto-cli-agent/)
7
- ![tests](https://img.shields.io/badge/tests-1552%20passed-brightgreen)
7
+ ![tests](https://img.shields.io/badge/tests-1685%20passed-brightgreen)
8
8
 
9
9
  Otto is a terminal AI agent that works on a codebase, a container, a browser
10
10
  or a desktop, uses what it built, and judges its own work against criteria it
@@ -88,10 +88,12 @@ transcript without spending a turn.
88
88
  ## Install
89
89
 
90
90
  ```bash
91
- pip install otto-cli-agent
91
+ pip install "otto-cli-agent[local-embeddings]"
92
92
  ```
93
93
 
94
- Python 3.12 or newer. The wheel and sdist are also attached to every [release](https://github.com/siddharth23P/otto_agent/releases).
94
+ Python 3.12 or newer. The `local-embeddings` extra is the on-device
95
+ embedding model (fastembed); without it, memory recall uses a Gemini key when
96
+ there is one and plain recency otherwise. `[serve]` adds `otto serve`. The wheel and sdist are also attached to every [release](https://github.com/siddharth23P/otto_agent/releases).
95
97
 
96
98
  ## Quick start
97
99
 
@@ -117,6 +119,7 @@ uv run otto chat # the same pipeline at a prompt
117
119
  | --- | --- |
118
120
  | `otto tui` / `otto chat` | interactive sessions; `--workspace PATH`, `--no-workspace`, `--resume <id\|prefix\|last>` |
119
121
  | `otto sessions` | list, `--delete`, `--rename`, `--export`, `--import`, `--prune` |
122
+ | `otto serve` | the agent behind a WebSocket for the phone app; `--host`, `--port`, `--token`, `--qr`, `--allow-origin`, `--no-exec` |
120
123
  | `otto doctor` | provider and route health, exit 2 on a missing required key |
121
124
  | `otto models` | every model each configured vendor lists, with detected capabilities |
122
125
  | `otto route <task>` | the fallback chain for a seat, pins starred, observed outcomes shown |
@@ -130,11 +133,17 @@ uv run otto chat # the same pipeline at a prompt
130
133
 
131
134
  Environment variables: `OTTO_MAX_MODEL_CALLS` (per-turn ceiling, default 120),
132
135
  `OTTO_COMMAND_TIMEOUT` (120 s with a workspace, 10 s without),
136
+ `OTTO_PYTHON_SESSION=0` (a fresh process per `execute_python` call instead of
137
+ one interpreter per run), `OTTO_PYTHON_SESSION_MEMORY_MB` (that interpreter's
138
+ address-space ceiling on Linux, default 8192, 0 for none),
133
139
  `OTTO_EMBEDDING_MODEL` (`provider:model`; local BGE is the floor),
134
140
  `OTTO_MODEL_PRICES` (a JSON file that overrides the price table),
135
141
  `OTTO_IGNORE_ROUTES=1` (use the shipped routing table untouched; evals do),
136
142
  `OTTO_BROWSER_PYTHON` (an interpreter with Playwright and `pyte`, which
137
- enables the browser and terminal tools), `OTTO_NO_ANIMATION=1`, `OTTO_THEME`.
143
+ enables the browser and terminal tools), `OTTO_HOME` (where state lives,
144
+ default `~/.otto`), `OTTO_ENV_FILE` (the `.env` to load and write),
145
+ `OTTO_OUTPUT_DIR`, `OTTO_SERVE_TOKEN` (the `otto serve` pairing secret),
146
+ `OTTO_NO_ANIMATION=1`, `OTTO_THEME`.
138
147
 
139
148
  Otto ships no browser. To let it load the pages and terminal programs it
140
149
  writes, install Playwright and `pyte` into any Python once and point
@@ -202,11 +211,41 @@ The pieces, each documented in its own folder:
202
211
  | [agent/router](agent/router/README.md) | task seats, the routing table, provider adapters, learned ordering, health, pins, temperature policy |
203
212
  | [agent/cli](agent/cli/README.md) | the TUI, the REPL, sessions, setup, and every `otto` command |
204
213
  | [agent/eval](agent/eval/README.md) | the six benchmark harnesses, the failure taxonomy, the single-agent control |
205
- | [agent/config](agent/config/README.md) | the one `.env` file Otto reads and writes |
214
+ | [agent/config](agent/config/README.md) | the one `.env` file Otto reads and writes, and `OTTO_HOME` |
215
+ | [agent/phone](agent/phone/README.md) | the phone tools, the screen digest and the money guard behind the Android app |
216
+ | [agent/server](agent/server/README.md) | `otto serve`, the agent over a WebSocket |
206
217
  | [containers](containers/README.md) | the throwaway desktop image the screen tools drive |
207
- | [tests](tests/README.md) | 1,552 tests that need no key and no network |
218
+ | [tests](tests/README.md) | 1,685 tests that need no key and no network |
208
219
  | [docs](docs/README.md) | the development log, the research sources, the memory design |
209
220
 
221
+ ## Embedding Otto
222
+
223
+ Otto is also a library. `agent/embed.py` is the surface a host depends on --
224
+ an Android app with Otto's Python inside it is the case it was built for --
225
+ and it is deliberately small:
226
+
227
+ ```python
228
+ from agent import embed
229
+
230
+ embed.configure("/data/data/dev.otto.phone/files/otto") # before anything else is imported
231
+ embed.set_key("INCEPTION_API_KEY", "...") # written masked; or pass environ={...}
232
+ runtime = embed.Runtime() # imports the pipeline, once
233
+ session = runtime.open_session()
234
+ session.run("make my font bigger", events=print,
235
+ tools=phone_tools(backend), guidance=PHONE_GUIDANCE,
236
+ disabled_tools=PHONE_DISABLED_STANDING_TOOLS) # blocks; ask/answer from another thread
237
+ # Keys given through configure(environ=...) switch execute_bash/execute_python off unless
238
+ # disabled_tools says otherwise: a subprocess inherits the environment, keys included.
239
+ ```
240
+
241
+ Events are plain dicts (`progress`, `board`, `ask`, `final`, `error`), a run
242
+ pauses inside `run()` until `answer()` arrives, and `cancel()` stops it within
243
+ one model call. Keys live in the process environment, so one process is one
244
+ person's Otto; a host serving several people runs several processes. `API_VERSION` says which contract you have. The phone tools,
245
+ the screen digest and the money guard are [agent/phone](agent/phone/README.md);
246
+ `otto serve` puts the same runtime behind a WebSocket for a client that has
247
+ the hands ([agent/server](agent/server/README.md)).
248
+
210
249
  ## Evaluation
211
250
 
212
251
  Six harnesses, each grading by something outside the model: a checker, a
@@ -219,7 +258,7 @@ enforce: [agent/eval/README.md](agent/eval/README.md).
219
258
 
220
259
  ## Testing
221
260
 
222
- 1,552 tests pass and 12 skip on macOS, Linux and Windows in under two minutes,
261
+ 1,685 tests pass and 12 skip on macOS, Linux and Windows in a few minutes,
223
262
  with no API keys and no network. Tests assert on the messages handed to the
224
263
  model, on the exact inputs that broke real runs, on call counts against the
225
264
  real compiled graph, and directly on the library behaviours the code relies
@@ -10,7 +10,13 @@ its own README:
10
10
  | [router/](router/README.md) | which model serves which kind of work, across four vendors |
11
11
  | [cli/](cli/README.md) | the TUI, the REPL and every `otto` sub-command |
12
12
  | [eval/](eval/README.md) | the benchmark harnesses |
13
- | [config/](config/README.md) | the `.env` file |
13
+ | [config/](config/README.md) | the `.env` file, and where Otto's state lives |
14
+ | [phone/](phone/README.md) | the phone as a place Otto works: the backend Protocol, the screen digest, the money guard, the phone tools |
15
+ | [server/](server/README.md) | `otto serve`: the agent behind a WebSocket for a client that has the hands |
16
+
17
+ Beside them, one module: `embed.py`, the surface an embedding host (the
18
+ Android app) depends on -- `configure()`, keys, `Runtime`, `SessionHandle` --
19
+ versioned by `API_VERSION` and deliberately narrow.
14
20
 
15
21
  Dependency direction, enforced by the modules' own docstrings and a few
16
22
  tests: `memory/` imports nothing from `pipeline/` or `router/`, so it never
@@ -14,6 +14,9 @@ rubbish" is how that gets noticed, and `--clear` is how it gets undone.
14
14
  through the same duplicate adjudication a run's own lessons face. The TUI's
15
15
  "Export lessons…" / "Import lessons…" palette entries call the same two
16
16
  functions (agent/memory/lessons.py).
17
+
18
+ `--phone` (2026-09-15) does any of the above to the lessons phone runs keep
19
+ apart from the workspace's (agent/memory/lessons.py PHONE_KIND).
17
20
  """
18
21
  from __future__ import annotations
19
22
 
@@ -28,7 +31,8 @@ from typing_extensions import Annotated
28
31
 
29
32
  from agent.cli.ui import err, out
30
33
  from agent.memory.lessons import (
31
- Lesson, all_lessons, bank_path, bind_bank, clear_bank, export_lessons, import_lessons,
34
+ KIND, PHONE_KIND, Lesson, all_lessons, bank_path, bind_bank, bind_kind, clear_bank, export_lessons,
35
+ import_lessons,
32
36
  )
33
37
  from agent.memory.store import MemoryStore
34
38
 
@@ -69,6 +73,10 @@ def lessons_cmd(
69
73
  bool,
70
74
  typer.Option("--replace", help="With --import: empty the bank first."),
71
75
  ] = False,
76
+ phone: Annotated[
77
+ bool,
78
+ typer.Option("--phone", help="The lessons phone runs learn and read, instead of the workspace's."),
79
+ ] = False,
72
80
  ) -> None:
73
81
  path = bank or bank_path()
74
82
  if import_from is None and not path.exists():
@@ -76,7 +84,7 @@ def lessons_cmd(
76
84
  return
77
85
 
78
86
  store = MemoryStore(path)
79
- with bind_bank(store):
87
+ with bind_bank(store), bind_kind(PHONE_KIND if phone else KIND):
80
88
  if clear:
81
89
  removed = clear_bank()
82
90
  err.print(f"cleared {removed} lesson(s) from {path}")
@@ -92,6 +100,6 @@ def lessons_cmd(
92
100
 
93
101
  learned = all_lessons()
94
102
  if not learned:
95
- out.print(f"{path} is empty")
103
+ out.print(f"{path} has no phone lessons yet" if phone else f"{path} is empty")
96
104
  return
97
105
  out.print(lessons_table(learned, path))
@@ -34,6 +34,7 @@ from agent.cli import lessons as lessons_cmd
34
34
  from agent.cli import models as model_cmd
35
35
  from agent.cli import route as route_cmd
36
36
  from agent.cli import sessions as sessions_cmd
37
+ from agent.cli import serve as serve_cmd
37
38
  from agent.cli import chat as chat_cmd
38
39
  from agent.cli import tui as tui_cmd
39
40
 
@@ -60,6 +61,7 @@ for _name, _fn in (
60
61
  ("eval-swe", eval_swe_cmd.eval_swe_cmd),
61
62
  ("lessons", lessons_cmd.lessons_cmd),
62
63
  ("sessions", sessions_cmd.sessions_cmd),
64
+ ("serve", serve_cmd.serve),
63
65
  ):
64
66
  app.command(_name)(friendly(_fn))
65
67
 
@@ -10,13 +10,15 @@ CodeTask, right after rendering the panel.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import os
13
14
  from pathlib import Path
14
15
 
15
16
  #: Relative to wherever `otto chat`/`otto tui` is launched from -- next to
16
17
  #: the project it's working on, not buried in ~/.otto with the ledger/profile
17
18
  #: (10.1/10.2), since this is meant to be opened by a person, not read back
18
- #: by Otto itself.
19
- OUTPUT_DIR = Path.cwd() / "otto_output"
19
+ #: by Otto itself. OTTO_OUTPUT_DIR overrides it for a host with no useful
20
+ #: working directory (an embedded run; agent/config/home.py).
21
+ OUTPUT_DIR = Path(os.environ.get("OTTO_OUTPUT_DIR", "").strip() or Path.cwd() / "otto_output")
20
22
 
21
23
  #: classify()'s LANGUAGE line is free text in plain English ("python",
22
24
  #: "javascript"), not an enum -- mapped here to the extension a person would
@@ -0,0 +1,127 @@
1
+ """`otto serve`: the agent behind a WebSocket for the phone app (agent/server/)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import ipaddress
6
+ import os
7
+ import secrets
8
+ from typing import Annotated, Optional
9
+
10
+ import typer
11
+
12
+ from agent.cli.ui import err, out
13
+
14
+ TOKEN_ENV = "OTTO_SERVE_TOKEN"
15
+
16
+
17
+ def pairing_url(host: str, port: int) -> str:
18
+ shown = "127.0.0.1" if host in ("", "0.0.0.0", "::") else host
19
+ return f"ws://{shown}:{port}/"
20
+
21
+
22
+ def is_loopback(host: str) -> bool:
23
+ """Whether `host` only accepts connections from this computer."""
24
+ host = (host or "").strip().strip("[]")
25
+ if host.lower() == "localhost":
26
+ return True
27
+ try:
28
+ return ipaddress.ip_address(host).is_loopback
29
+ except ValueError:
30
+ return False
31
+
32
+
33
+ def exposure_warning(host: str, *, no_exec: bool = False) -> str | None:
34
+ """What to print when the server listens beyond loopback, or None.
35
+
36
+ A turn the phone is not needed for runs like `otto tui` -- shell and
37
+ Python on this computer included (2026-09-15, decided with the person who
38
+ runs it) -- so the pairing token is no longer only "talk to the agent".
39
+ Whoever holds it on a network this reaches can run commands here. The
40
+ phone does not need the network for this: `adb reverse` carries a
41
+ loopback server over USB."""
42
+ if is_loopback(host):
43
+ return None
44
+ grants = ("read and write files in otto's workspaces on this computer" if no_exec
45
+ else "run commands and read and write files on this computer")
46
+ return (f"otto serve is listening on {host or 'every interface'}, beyond this computer: anyone "
47
+ f"with the pairing token can {grants}. Prefer --host 127.0.0.1 with "
48
+ "`adb reverse tcp:8765 tcp:8765`" + ("" if no_exec else ", or add --no-exec") + ".")
49
+
50
+
51
+ def ensure_token(explicit: str | None) -> str:
52
+ """The token from --token, else OTTO_SERVE_TOKEN, else a new one written
53
+ to the env file so the app stays paired across restarts."""
54
+ if explicit:
55
+ return explicit
56
+ existing = os.environ.get(TOKEN_ENV, "").strip()
57
+ if existing:
58
+ return existing
59
+ from agent.config import envfile
60
+
61
+ token = secrets.token_urlsafe(24)
62
+ envfile.set_value(TOKEN_ENV, token)
63
+ return token
64
+
65
+
66
+ def _log_phone_actions() -> None:
67
+ """Each phone tool call and what came of it, on stderr (agent/phone/tools.py logs them): what a
68
+ person reads when a run went back and forth. Typed text is never logged."""
69
+ import logging
70
+
71
+ phone = logging.getLogger("agent.phone")
72
+ if not any(getattr(h, "_otto_serve", False) for h in phone.handlers):
73
+ handler = logging.StreamHandler()
74
+ handler.setFormatter(logging.Formatter("%(asctime)s phone %(message)s", "%H:%M:%S"))
75
+ handler._otto_serve = True # type: ignore[attr-defined]
76
+ phone.addHandler(handler)
77
+ phone.setLevel(logging.INFO)
78
+
79
+
80
+ def serve(
81
+ host: Annotated[str, typer.Option(help="Interface to listen on; loopback unless you mean otherwise.")] = "127.0.0.1",
82
+ port: Annotated[int, typer.Option(help="Port to listen on.")] = 8765,
83
+ token: Annotated[Optional[str], typer.Option(help=f"Shared secret the app presents; default {TOKEN_ENV} or a generated one.")] = None,
84
+ qr: Annotated[bool, typer.Option("--qr", help="Print a pairing QR code (needs the qrcode package).")] = False,
85
+ allow_origin: Annotated[Optional[list[str]], typer.Option(
86
+ "--allow-origin", help="A browser origin allowed to connect (repeatable). Pages are refused otherwise.")] = None,
87
+ no_exec: Annotated[bool, typer.Option(
88
+ "--no-exec", help="No shell or Python for any turn: the app's answers stay in the workspace.")] = False,
89
+ allow_remote_setup: Annotated[bool, typer.Option(
90
+ "--allow-remote-setup", help="Take key and routing changes from a client not on this computer.")] = False,
91
+ ) -> None:
92
+ """Serve the agent over a WebSocket for the phone app."""
93
+ try:
94
+ import websockets # noqa: F401
95
+ except ImportError:
96
+ err.print("[bad]otto serve needs the websockets package: pip install 'otto-cli-agent[serve]'[/]")
97
+ raise typer.Exit(2)
98
+ from agent import embed
99
+ from agent.config.envfile import ENV_PATH
100
+ from agent.config.home import otto_home
101
+ from agent.server.app import OttoServer
102
+
103
+ embed.configure(otto_home(), env_file=ENV_PATH)
104
+ _log_phone_actions()
105
+ secret = ensure_token(token)
106
+ url = pairing_url(host, port)
107
+ payload = f"{url}#{secret}"
108
+ err.print(f"[muted]otto serve[/] listening on [bold]{url}[/]")
109
+ err.print(f"[muted]pair the app with[/] {payload}")
110
+ if (warning := exposure_warning(host, no_exec=no_exec)) is not None:
111
+ err.print(f"[warn]{warning}[/]", soft_wrap=True)
112
+ if qr:
113
+ try:
114
+ import qrcode
115
+
116
+ code = qrcode.QRCode(border=1)
117
+ code.add_data(payload)
118
+ code.print_ascii(invert=True)
119
+ except ImportError:
120
+ err.print("[warn]--qr needs `pip install qrcode`; the URL above pairs the same way[/]")
121
+ if not embed.ready():
122
+ err.print("[warn]INCEPTION_API_KEY is not set; turns will fail until it is (otto tui -> Setup)[/]")
123
+ try:
124
+ asyncio.run(OttoServer(secret, allowed_origins=tuple(allow_origin or ()),
125
+ no_exec=no_exec, allow_remote_setup=allow_remote_setup).run(host, port))
126
+ except KeyboardInterrupt:
127
+ out.print("[muted]bye[/]")
@@ -7,7 +7,6 @@ index from outside a session. Resuming is `otto chat --resume` / `otto tui
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- from datetime import date
11
10
  from pathlib import Path
12
11
  from typing import Optional
13
12
 
@@ -39,10 +38,9 @@ def sessions_table(rows: list[SessionInfo], current: str | None = None) -> Table
39
38
 
40
39
 
41
40
  def default_export_path(info: SessionInfo, directory: Path | None = None) -> Path:
42
- """`otto-session-<id>-<date>.json`, in `directory` (the current one by
43
- default; the TUI proposes the home directory) -- the shape agent/cli/
44
- lessons.py's export uses, with the id so two exports do not collide."""
45
- return (directory or Path.cwd()) / f"otto-session-{info.short_id}-{date.today():%Y-%m-%d}.json"
41
+ """agent/memory/sessions.py's `export_filename`, in `directory` (the
42
+ current one by default; the TUI proposes the home directory)."""
43
+ return (directory or Path.cwd()) / session_index.export_filename(info)
46
44
 
47
45
 
48
46
  def sessions_cmd(
@@ -48,11 +48,13 @@ from agent.cli import art
48
48
  from agent.cli.art import animations_enabled
49
49
  from agent.router.llm_provider.base import ModelInfo, ProviderStatus
50
50
  from agent.router.mapping import Candidate, Task
51
- from agent.router.overrides import PROVIDER_ONLY
52
51
 
53
52
  __all__ = ["SetupBackend", "SetupScreen", "MappingRow", "pin_options", "NO_PIN"]
54
53
 
55
- NO_PIN = ("(no pin — default route)", "")
54
+ # pin_options and NO_PIN live in agent/router/setup.py, beside the rest of the
55
+ # setup data layer, so `otto serve` offers the same choices; re-exported here
56
+ # for the screen and everything that imported them from it.
57
+ from agent.router.setup import NO_PIN, pin_options # noqa: E402
56
58
  CUSTOM = "__custom__"
57
59
 
58
60
  #: Status word -> primitive Rich style. agent/cli/doctor.py's map, without
@@ -86,21 +88,6 @@ class SetupBackend:
86
88
  reload: Callable[[], Any]
87
89
 
88
90
 
89
- def pin_options(task: Task, models: list[ModelInfo],
90
- routes: Mapping[Task, tuple[Candidate, ...]]) -> list[tuple[str, str]]:
91
- """The Select options for one task: no pin, then every model the seat
92
- could use -- the head candidate's `requires`, narrowed to the provider a
93
- task is bound to (agent/router/overrides.py PROVIDER_ONLY)."""
94
- head = routes[task][0]
95
- bound = PROVIDER_ONLY.get(task)
96
- eligible = [
97
- m for m in models
98
- if head.requires <= m.capabilities and (bound is None or m.provider == bound[0])
99
- ]
100
- eligible.sort(key=lambda m: (m.provider, m.id))
101
- return [NO_PIN] + [(m.spec, m.spec) for m in eligible]
102
-
103
-
104
91
  class MappingRow(Horizontal):
105
92
  """One task: its name, what resolves now, the proposal, and the pick."""
106
93
 
@@ -0,0 +1,15 @@
1
+ # agent/config/
2
+
3
+ `envfile.py` reads and writes the one `.env` file Otto loads, at the
4
+ repository root. It is what the setup screen writes keys and named endpoints
5
+ into, and what `agent/cli/main.py` loads at start. Keys are only ever shown
6
+ masked; a named custom endpoint `local` becomes `LOCAL_API_KEY` and
7
+ `LOCAL_BASE_URL`. Nothing else in the package reads the file directly.
8
+
9
+ `home.py` says where that file is and where every per-installation file
10
+ goes: `OTTO_HOME` (default `~/.otto`) for the memory stores, the session
11
+ index, the outcome log, routes and lessons; `OTTO_ENV_FILE` for the `.env`
12
+ (default: the checkout's, or `<OTTO_HOME>/.env` for an installed wheel).
13
+ Both are read when the state modules are imported, so an embedding host sets
14
+ them first -- `agent/embed.py`'s `configure()` does. `OTTO_OUTPUT_DIR` moves
15
+ `otto_output/` the same way.
@@ -20,10 +20,13 @@ from pathlib import Path
20
20
 
21
21
  from dotenv import set_key, unset_key
22
22
 
23
- #: The file agent/cli/main.py loads at import: the repository root's `.env`.
24
- #: One definition, imported by main.py, so the writer and the loader cannot
25
- #: drift apart.
26
- ENV_PATH: Path = Path(__file__).resolve().parents[2] / ".env"
23
+ from agent.config.home import env_file
24
+
25
+ #: The file agent/cli/main.py loads at import: the repository root's `.env`
26
+ #: from a checkout, `$OTTO_ENV_FILE` when set, `<OTTO_HOME>/.env` for an
27
+ #: installed wheel (agent/config/home.py). One definition, imported by
28
+ #: main.py, so the writer and the loader cannot drift apart.
29
+ ENV_PATH: Path = env_file()
27
30
 
28
31
  KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
29
32
 
@@ -42,7 +45,7 @@ def present(key: str) -> bool:
42
45
  return bool(os.environ.get(key))
43
46
 
44
47
 
45
- def set_value(key: str, value: str, *, path: Path = ENV_PATH) -> str:
48
+ def set_value(key: str, value: str, *, path: Path | None = None) -> str:
46
49
  """Write `KEY=value` to `path` and into `os.environ`. Returns the masked
47
50
  value, which is all a caller should ever display.
48
51
 
@@ -55,22 +58,38 @@ def set_value(key: str, value: str, *, path: Path = ENV_PATH) -> str:
55
58
  if not value:
56
59
  unset_value(key, path=path)
57
60
  return masked(None)
58
- path = Path(path)
61
+ # Resolved at call time, not bound as a default: a default would freeze
62
+ # the path at import, and both the tests and an embedding host that
63
+ # reassigns ENV_PATH would silently write somewhere else.
64
+ path = Path(ENV_PATH if path is None else path)
59
65
  if not path.exists():
60
66
  path.parent.mkdir(parents=True, exist_ok=True)
61
67
  path.touch(mode=0o600)
62
68
  set_key(str(path), key, value, quote_mode="auto")
69
+ _private(path)
63
70
  os.environ[key] = value
64
71
  return masked(value)
65
72
 
66
73
 
67
- def unset_value(key: str, *, path: Path = ENV_PATH) -> None:
74
+ def _private(path: Path) -> None:
75
+ """Owner-only, on every write and not only on creation: the file may
76
+ have been made by a host app or by hand with a wider mode, and since
77
+ 0.1.2 it also carries the `otto serve` pairing token. Windows has no
78
+ such mode; a failure to set it is not a failure to write."""
79
+ try:
80
+ os.chmod(path, 0o600)
81
+ except OSError:
82
+ pass
83
+
84
+
85
+ def unset_value(key: str, *, path: Path | None = None) -> None:
68
86
  """Remove `key` from `path` (if present) and from `os.environ`."""
69
87
  if not KEY_RE.match(key or ""):
70
88
  raise ValueError(f"{key!r} is not a valid environment variable name")
71
- path = Path(path)
89
+ path = Path(ENV_PATH if path is None else path)
72
90
  if path.exists():
73
91
  # unset_key logs a warning and returns (None, key) when the key is
74
92
  # absent; neither is an error for a caller that wants it gone.
75
93
  unset_key(str(path), key)
94
+ _private(path) # a removal is a write too
76
95
  os.environ.pop(key, None)
@@ -0,0 +1,74 @@
1
+ """Where Otto keeps its per-installation state, and where its `.env` is.
2
+
3
+ Two facts that used to be constants: `~/.otto` for the memory stores, the
4
+ session index, the outcome log, routes and lessons (agent/memory/store.py,
5
+ agent/memory/sessions.py, agent/router/outcomes.py), and the repository
6
+ root's `.env` for keys (agent/config/envfile.py). Both are wrong the moment
7
+ Otto runs anywhere but a developer's shell. Embedded in an Android app there
8
+ is no `HOME` at all -- `Path.home()` raises -- and no repository, so an
9
+ installed wheel has nowhere to write a key to.
10
+
11
+ So they are functions of two environment variables, read at import time by
12
+ the modules that own the constants:
13
+
14
+ OTTO_HOME the directory for every per-installation file
15
+ OTTO_ENV_FILE the .env Otto loads at start and writes keys into
16
+
17
+ Read at import rather than at every call, deliberately: the constants they
18
+ feed (`store.DB_DIR`, `sessions.DEFAULT_INDEX_PATH`, `outcomes.DB_DIR`,
19
+ `envfile.ENV_PATH`) are what tests monkeypatch and what `bind_index`,
20
+ `bind_log` and `bind_store` override per test, and a value that moved
21
+ underneath those seams would make every one of them lie. The contract is
22
+ therefore: set the variables BEFORE `import agent.<anything>`, which is what
23
+ agent/embed.py's `configure()` does for an embedding host.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ from pathlib import Path
29
+
30
+ HOME_ENV = "OTTO_HOME"
31
+ ENV_FILE_ENV = "OTTO_ENV_FILE"
32
+
33
+ #: The repository root when Otto runs from a checkout, else None. An
34
+ #: installed wheel's `agent/` sits in site-packages, which has no
35
+ #: pyproject.toml two levels up.
36
+ _CHECKOUT_ROOT = Path(__file__).resolve().parents[2]
37
+
38
+
39
+ def otto_home() -> Path:
40
+ """`$OTTO_HOME`, else `~/.otto`.
41
+
42
+ Raises RuntimeError naming the variable when neither can be resolved,
43
+ which is the honest answer on a host with no home directory rather than a
44
+ `Path.home()` traceback from inside an unrelated import.
45
+ """
46
+ raw = os.environ.get(HOME_ENV, "").strip()
47
+ if raw:
48
+ return Path(raw).expanduser()
49
+ try:
50
+ return Path.home() / ".otto"
51
+ except (RuntimeError, KeyError) as exc:
52
+ raise RuntimeError(
53
+ f"no home directory to put Otto's state in -- set {HOME_ENV} to a "
54
+ "writable directory before importing agent"
55
+ ) from exc
56
+
57
+
58
+ def running_from_checkout() -> bool:
59
+ return (_CHECKOUT_ROOT / "pyproject.toml").is_file()
60
+
61
+
62
+ def env_file() -> Path:
63
+ """`$OTTO_ENV_FILE`, else the checkout's `.env`, else `<otto_home>/.env`.
64
+
65
+ The checkout case keeps `otto tui`'s setup screen writing the file the
66
+ README tells a developer to create; an installed wheel has no such file
67
+ and keeps keys beside its other state instead.
68
+ """
69
+ raw = os.environ.get(ENV_FILE_ENV, "").strip()
70
+ if raw:
71
+ return Path(raw).expanduser()
72
+ if running_from_checkout():
73
+ return _CHECKOUT_ROOT / ".env"
74
+ return otto_home() / ".env"