comodor 0.8.4__tar.gz → 0.8.8__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 (155) hide show
  1. {comodor-0.8.4 → comodor-0.8.8}/PKG-INFO +63 -3
  2. {comodor-0.8.4 → comodor-0.8.8}/README.md +61 -2
  3. {comodor-0.8.4 → comodor-0.8.8}/pyproject.toml +24 -1
  4. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/_version.py +2 -2
  5. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/context.py +1 -1
  6. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/loop.py +1 -1
  7. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/catalogue.py +6 -2
  8. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/cli.py +132 -3
  9. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/config.py +316 -16
  10. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/doctor.py +38 -2
  11. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/store.py +6 -2
  12. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/catalogue.py +1 -1
  13. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/commands.py +2 -2
  14. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/http.py +1 -2
  15. comodor-0.8.8/src/comodor/migrate.py +496 -0
  16. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/http.py +18 -7
  17. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/openai_compat.py +2 -1
  18. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/registry.py +16 -0
  19. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/setup.py +210 -16
  20. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/propose.py +1 -1
  21. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/fs.py +2 -1
  22. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/shell.py +4 -2
  23. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/todo.py +1 -1
  24. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/__init__.py +2 -1
  25. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/app.py +97 -8
  26. comodor-0.8.8/src/comodor/ui/banner.py +250 -0
  27. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/reader.py +1 -1
  28. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/markdown.py +1 -1
  29. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/theme.py +1 -1
  30. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/chat.py +1 -1
  31. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/overlay.py +1 -1
  32. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/statusbar.py +0 -2
  33. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/commands.py +80 -1
  34. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/server.py +4 -3
  35. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/session.py +19 -5
  36. {comodor-0.8.4 → comodor-0.8.8}/tests/conftest.py +33 -4
  37. {comodor-0.8.4 → comodor-0.8.8}/tests/test_associations.py +4 -1
  38. comodor-0.8.8/tests/test_banner.py +300 -0
  39. {comodor-0.8.4 → comodor-0.8.8}/tests/test_bidi.py +3 -2
  40. comodor-0.8.8/tests/test_config_save.py +236 -0
  41. {comodor-0.8.4 → comodor-0.8.8}/tests/test_delegate.py +3 -3
  42. {comodor-0.8.4 → comodor-0.8.8}/tests/test_doctor.py +63 -9
  43. comodor-0.8.8/tests/test_import_command.py +139 -0
  44. {comodor-0.8.4 → comodor-0.8.8}/tests/test_layout.py +2 -1
  45. {comodor-0.8.4 → comodor-0.8.8}/tests/test_learning.py +0 -1
  46. {comodor-0.8.4 → comodor-0.8.8}/tests/test_mcp.py +12 -1
  47. {comodor-0.8.4 → comodor-0.8.8}/tests/test_mcp_http.py +2 -0
  48. comodor-0.8.8/tests/test_migrate.py +481 -0
  49. {comodor-0.8.4 → comodor-0.8.8}/tests/test_providers.py +0 -2
  50. {comodor-0.8.4 → comodor-0.8.8}/tests/test_real_browser.py +0 -1
  51. {comodor-0.8.4 → comodor-0.8.8}/tests/test_setup.py +1 -1
  52. comodor-0.8.8/tests/test_setup_import.py +305 -0
  53. {comodor-0.8.4 → comodor-0.8.8}/tests/test_skills.py +2 -4
  54. comodor-0.8.8/tests/test_switching_model.py +158 -0
  55. {comodor-0.8.4 → comodor-0.8.8}/tests/test_tools.py +0 -1
  56. {comodor-0.8.4 → comodor-0.8.8}/tests/test_uninstall.py +2 -2
  57. {comodor-0.8.4 → comodor-0.8.8}/tests/test_update.py +6 -2
  58. {comodor-0.8.4 → comodor-0.8.8}/tests/test_web.py +96 -3
  59. {comodor-0.8.4 → comodor-0.8.8}/.gitignore +0 -0
  60. {comodor-0.8.4 → comodor-0.8.8}/LICENSE +0 -0
  61. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/__init__.py +0 -0
  62. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/__main__.py +0 -0
  63. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/__init__.py +0 -0
  64. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/prompts.py +0 -0
  65. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/spawn.py +0 -0
  66. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/tokens.py +0 -0
  67. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/__init__.py +0 -0
  68. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/cdp.py +0 -0
  69. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/launch.py +0 -0
  70. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/page.py +0 -0
  71. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/events.py +0 -0
  72. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/__init__.py +0 -0
  73. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/associations.py +0 -0
  74. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/bm25.py +0 -0
  75. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/hotindex.py +1 -1
  76. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/memory.py +0 -0
  77. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/progress.py +0 -0
  78. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/reflect.py +0 -0
  79. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/rules.py +0 -0
  80. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/signals.py +0 -0
  81. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/writer.py +0 -0
  82. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/__init__.py +0 -0
  83. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/manager.py +0 -0
  84. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/protocol.py +0 -0
  85. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/__init__.py +0 -0
  86. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/sse.py +0 -0
  87. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/paths.py +0 -0
  88. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/__init__.py +0 -0
  89. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/anthropic.py +0 -0
  90. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/base.py +0 -0
  91. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/caching.py +0 -0
  92. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/fake.py +0 -0
  93. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/gateway.py +0 -0
  94. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/__init__.py +0 -0
  95. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/checkpoints.py +0 -0
  96. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/permissions.py +0 -0
  97. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/redact.py +0 -0
  98. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/__init__.py +0 -0
  99. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/search.py +0 -0
  100. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/store.py +0 -0
  101. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/__init__.py +0 -0
  102. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/catalogue.py +0 -0
  103. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/commands.py +0 -0
  104. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/examples.py +0 -0
  105. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/loader.py +0 -0
  106. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/registry.py +0 -0
  107. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/__init__.py +0 -0
  108. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/base.py +0 -0
  109. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/browse.py +0 -0
  110. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/browser.py +0 -0
  111. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/delegate.py +0 -0
  112. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/history.py +0 -0
  113. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/mcp.py +0 -0
  114. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/overflow.py +0 -0
  115. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/registry.py +1 -1
  116. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/search.py +0 -0
  117. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/skills.py +0 -0
  118. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/web.py +0 -0
  119. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/bidi.py +0 -0
  120. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/chooser.py +0 -0
  121. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/console.py +0 -0
  122. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/__init__.py +0 -0
  123. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/keys.py +0 -0
  124. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/layout.py +0 -0
  125. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/screen.py +0 -0
  126. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/__init__.py +0 -0
  127. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/buttons.py +0 -0
  128. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/history.py +0 -0
  129. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/panel.py +0 -0
  130. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/progress.py +0 -0
  131. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/prompt.py +0 -0
  132. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/toast.py +0 -0
  133. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/uninstall.py +0 -0
  134. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/update.py +0 -0
  135. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/__init__.py +0 -0
  136. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/index.html +0 -0
  137. {comodor-0.8.4 → comodor-0.8.8}/src/comodor/workspace.py +0 -0
  138. {comodor-0.8.4 → comodor-0.8.8}/tests/support/fake_mcp_server.py +0 -0
  139. {comodor-0.8.4 → comodor-0.8.8}/tests/test_agent_loop.py +0 -0
  140. {comodor-0.8.4 → comodor-0.8.8}/tests/test_app.py +0 -0
  141. {comodor-0.8.4 → comodor-0.8.8}/tests/test_browser.py +0 -0
  142. {comodor-0.8.4 → comodor-0.8.8}/tests/test_caching.py +0 -0
  143. {comodor-0.8.4 → comodor-0.8.8}/tests/test_chooser.py +0 -0
  144. {comodor-0.8.4 → comodor-0.8.8}/tests/test_history.py +0 -0
  145. {comodor-0.8.4 → comodor-0.8.8}/tests/test_input.py +0 -0
  146. {comodor-0.8.4 → comodor-0.8.8}/tests/test_markdown.py +0 -0
  147. {comodor-0.8.4 → comodor-0.8.8}/tests/test_overflow.py +0 -0
  148. {comodor-0.8.4 → comodor-0.8.8}/tests/test_performance.py +0 -0
  149. {comodor-0.8.4 → comodor-0.8.8}/tests/test_prefix_stability.py +0 -0
  150. {comodor-0.8.4 → comodor-0.8.8}/tests/test_progress.py +0 -0
  151. {comodor-0.8.4 → comodor-0.8.8}/tests/test_propose.py +0 -0
  152. {comodor-0.8.4 → comodor-0.8.8}/tests/test_reflex.py +0 -0
  153. {comodor-0.8.4 → comodor-0.8.8}/tests/test_run_loop.py +0 -0
  154. {comodor-0.8.4 → comodor-0.8.8}/tests/test_skill_budget.py +0 -0
  155. {comodor-0.8.4 → comodor-0.8.8}/tests/test_skill_catalogue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comodor
3
- Version: 0.8.4
3
+ Version: 0.8.8
4
4
  Summary: Comodor — a self-improving terminal coding agent with a Rich TUI
5
5
  Project-URL: Homepage, https://comodor.ai
6
6
  Project-URL: Repository, https://github.com/ifekri/Comodor
@@ -22,6 +22,7 @@ Requires-Python: >=3.11
22
22
  Requires-Dist: rich>=13.7
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.14; extra == 'dev'
25
26
  Description-Content-Type: text/markdown
26
27
 
27
28
  # Comodor
@@ -114,14 +115,14 @@ Then type `comodor`.
114
115
 
115
116
  ## First run
116
117
 
117
- Four questions, once. Nothing to create beforehand — no config file, no
118
+ Five questions, once. Nothing to create beforehand — no config file, no
118
119
  environment variable, no documentation to read first.
119
120
 
120
121
  ```
121
122
  ✓ provider Ollama (local)
122
123
  ✓ api key not needed
123
124
 
124
- 3/4 Which model?
125
+ 3/5 Which model?
125
126
  ┌─ Models ──────────────────────────────────────────────┐
126
127
  │ › qwen2.5-coder:14b recommended │
127
128
  │ llama3.3 │
@@ -141,6 +142,19 @@ remembered.
141
142
 
142
143
  You are not asked again. Change your mind later with `comodor setup`.
143
144
 
145
+ **Already using another agent?** If OpenClaw or Hermes is installed, the first
146
+ screen offers to bring your API keys, your model and your skills across — the
147
+ tedious part, which you have already done once. Nothing is moved and nothing
148
+ already set here is replaced, so the other tool keeps working exactly as it
149
+ did. What deliberately does not come over is said rather than skipped in
150
+ silence: their memory is prose, and this agent's is lessons with confidence and
151
+ evidence, so importing one as the other would poison recall with entries
152
+ nothing earned.
153
+
154
+ Afterwards, or if you install one of them later, `comodor import` does the same
155
+ thing — with `--dry-run` to see what it would take, and `--keys-only` to leave
156
+ the rest.
157
+
144
158
  **No API key?** `comodor --demo` runs the whole interface offline — every
145
159
  panel, every command, no account required.
146
160
 
@@ -457,6 +471,35 @@ comodor --provider groq --model llama-3.3-70b-versatile
457
471
 
458
472
  ---
459
473
 
474
+ ## It tells you what it knows
475
+
476
+ Starting Comodor — the interface, a headless run, or the browser server — puts
477
+ this at the top:
478
+
479
+ ```
480
+ ______ __
481
+ / ____/___ ____ ___ ____ ____/ /___ _____
482
+ / / / __ \/ __ `__ \/ __ \/ __ / __ \/ ___/
483
+ / /___/ /_/ / / / / / / /_/ / /_/ / /_/ / /
484
+ \____/\____/_/ /_/ /_/\____/\__,_/\____/_/
485
+
486
+ it learns the way you correct it 0.8.6 · claude-sonnet-5
487
+ 412 lessons · 7 skills · 3 rules you set from 128 finished tasks
488
+ ~/work/your-project
489
+ ```
490
+
491
+ The second line is the one worth the space. Most banners are a logo and a
492
+ version number nobody reads twice; this one says what *this* installation has
493
+ accumulated, and that number goes up as you use it.
494
+
495
+ It stays out of the way where a banner would be noise: never in a pipe, so
496
+ `comodor run … | jq` sees the answer and nothing else; never in `--json`;
497
+ never in CI. Below 51 columns the art is dropped rather than reflowed, because
498
+ ASCII art wrapped by a terminal is not a smaller logo. `COMODOR_BANNER=0`
499
+ switches it off for one run and `"banner": false` for good.
500
+
501
+ ---
502
+
460
503
  ## A browser, and the reason it is not screenshots
461
504
 
462
505
  Comodor drives a real Chrome — one that runs JavaScript, keeps cookies and can
@@ -750,6 +793,23 @@ comodor preview 120x34 # render one frame at a fixed size
750
793
 
751
794
  ---
752
795
 
796
+ ## Checking it
797
+
798
+ ```bash
799
+ pip install -e ".[dev]"
800
+ ruff check src tests # the linter
801
+ pytest -q # the suite
802
+ ```
803
+
804
+ Those two commands are the gate, and they are the same two the release runs
805
+ before it builds anything. Nothing reaches PyPI that has not passed them: the
806
+ release workflow's first job is the gate, and building, publishing and the
807
+ GitHub release all depend on it.
808
+
809
+ That is not a precaution against a hypothetical. Release triggers on a tag and
810
+ CI on a push; before this they were unrelated, and 0.8.0 went to PyPI while CI
811
+ was red.
812
+
753
813
  ## Contributing
754
814
 
755
815
  Bug reports welcome — please include `comodor doctor`, which prints everything
@@ -88,14 +88,14 @@ Then type `comodor`.
88
88
 
89
89
  ## First run
90
90
 
91
- Four questions, once. Nothing to create beforehand — no config file, no
91
+ Five questions, once. Nothing to create beforehand — no config file, no
92
92
  environment variable, no documentation to read first.
93
93
 
94
94
  ```
95
95
  ✓ provider Ollama (local)
96
96
  ✓ api key not needed
97
97
 
98
- 3/4 Which model?
98
+ 3/5 Which model?
99
99
  ┌─ Models ──────────────────────────────────────────────┐
100
100
  │ › qwen2.5-coder:14b recommended │
101
101
  │ llama3.3 │
@@ -115,6 +115,19 @@ remembered.
115
115
 
116
116
  You are not asked again. Change your mind later with `comodor setup`.
117
117
 
118
+ **Already using another agent?** If OpenClaw or Hermes is installed, the first
119
+ screen offers to bring your API keys, your model and your skills across — the
120
+ tedious part, which you have already done once. Nothing is moved and nothing
121
+ already set here is replaced, so the other tool keeps working exactly as it
122
+ did. What deliberately does not come over is said rather than skipped in
123
+ silence: their memory is prose, and this agent's is lessons with confidence and
124
+ evidence, so importing one as the other would poison recall with entries
125
+ nothing earned.
126
+
127
+ Afterwards, or if you install one of them later, `comodor import` does the same
128
+ thing — with `--dry-run` to see what it would take, and `--keys-only` to leave
129
+ the rest.
130
+
118
131
  **No API key?** `comodor --demo` runs the whole interface offline — every
119
132
  panel, every command, no account required.
120
133
 
@@ -431,6 +444,35 @@ comodor --provider groq --model llama-3.3-70b-versatile
431
444
 
432
445
  ---
433
446
 
447
+ ## It tells you what it knows
448
+
449
+ Starting Comodor — the interface, a headless run, or the browser server — puts
450
+ this at the top:
451
+
452
+ ```
453
+ ______ __
454
+ / ____/___ ____ ___ ____ ____/ /___ _____
455
+ / / / __ \/ __ `__ \/ __ \/ __ / __ \/ ___/
456
+ / /___/ /_/ / / / / / / /_/ / /_/ / /_/ / /
457
+ \____/\____/_/ /_/ /_/\____/\__,_/\____/_/
458
+
459
+ it learns the way you correct it 0.8.6 · claude-sonnet-5
460
+ 412 lessons · 7 skills · 3 rules you set from 128 finished tasks
461
+ ~/work/your-project
462
+ ```
463
+
464
+ The second line is the one worth the space. Most banners are a logo and a
465
+ version number nobody reads twice; this one says what *this* installation has
466
+ accumulated, and that number goes up as you use it.
467
+
468
+ It stays out of the way where a banner would be noise: never in a pipe, so
469
+ `comodor run … | jq` sees the answer and nothing else; never in `--json`;
470
+ never in CI. Below 51 columns the art is dropped rather than reflowed, because
471
+ ASCII art wrapped by a terminal is not a smaller logo. `COMODOR_BANNER=0`
472
+ switches it off for one run and `"banner": false` for good.
473
+
474
+ ---
475
+
434
476
  ## A browser, and the reason it is not screenshots
435
477
 
436
478
  Comodor drives a real Chrome — one that runs JavaScript, keeps cookies and can
@@ -724,6 +766,23 @@ comodor preview 120x34 # render one frame at a fixed size
724
766
 
725
767
  ---
726
768
 
769
+ ## Checking it
770
+
771
+ ```bash
772
+ pip install -e ".[dev]"
773
+ ruff check src tests # the linter
774
+ pytest -q # the suite
775
+ ```
776
+
777
+ Those two commands are the gate, and they are the same two the release runs
778
+ before it builds anything. Nothing reaches PyPI that has not passed them: the
779
+ release workflow's first job is the gate, and building, publishing and the
780
+ GitHub release all depend on it.
781
+
782
+ That is not a precaution against a hypothetical. Release triggers on a tag and
783
+ CI on a push; before this they were unrelated, and 0.8.0 went to PyPI while CI
784
+ was red.
785
+
727
786
  ## Contributing
728
787
 
729
788
  Bug reports welcome — please include `comodor doctor`, which prints everything
@@ -32,7 +32,7 @@ Issues = "https://github.com/ifekri/Comodor/issues"
32
32
  Changelog = "https://github.com/ifekri/Comodor/blob/main/CHANGELOG.md"
33
33
 
34
34
  [project.optional-dependencies]
35
- dev = ["pytest>=8.0"]
35
+ dev = ["pytest>=8.0", "ruff>=0.14"]
36
36
 
37
37
  [project.scripts]
38
38
  comodor = "comodor.cli:main"
@@ -41,6 +41,29 @@ comodor = "comodor.cli:main"
41
41
  # with it, so nothing can fall out of step with it: `git tag v0.3.0` is the
42
42
  # whole of a release. `_version.py` is generated at build time - including by an
43
43
  # editable install - and is not committed.
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py311"
47
+ extend-exclude = ["src/comodor/_version.py"]
48
+
49
+ [tool.ruff.lint]
50
+ # Chosen by counting what each family actually found in this tree, not by
51
+ # taking a preset. The reasoning is in the commit that added this.
52
+ select = ["F", "E4", "E7", "E9", "E501", "W", "I", "B", "C4", "PIE"]
53
+
54
+ [tool.ruff.lint.per-file-ignores]
55
+ # A table of models, one per line, whose columns line up across rows.
56
+ # That alignment is why it can be read at a glance, and wrapping every
57
+ # row onto two lines to satisfy a column count would cost more than the
58
+ # rule is worth here.
59
+ "src/comodor/providers/registry.py" = ["E501"]
60
+ # A test may import a name only to prove it is importable, and its fixtures
61
+ # shadow each other by design.
62
+ "tests/*" = ["B008"]
63
+
64
+ [tool.ruff.lint.isort]
65
+ known-first-party = ["comodor"]
66
+
44
67
  [tool.hatch.version]
45
68
  source = "vcs"
46
69
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.8.4'
22
- __version_tuple__ = version_tuple = (0, 8, 4)
21
+ __version__ = version = '0.8.8'
22
+ __version_tuple__ = version_tuple = (0, 8, 8)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -139,7 +139,7 @@ class Conversation:
139
139
  # -- introspection ---------------------------------------------------- #
140
140
 
141
141
  def summary_line(self) -> str:
142
- roles = {role: 0 for role in ("user", "assistant", "tool")}
142
+ roles = dict.fromkeys(("user", "assistant", "tool"), 0)
143
143
  for message in self.messages:
144
144
  key = message.role.value
145
145
  if key in roles:
@@ -256,7 +256,7 @@ class AgentLoop:
256
256
  else:
257
257
  results = [self._run_one(call, context) for call in calls]
258
258
 
259
- for call, result in zip(calls, results):
259
+ for call, result in zip(calls, results, strict=True):
260
260
  message = Message.tool(
261
261
  call_id=call.id, name=call.name,
262
262
  content=result.content, is_error=not result.ok,
@@ -65,8 +65,12 @@ CATALOGUE: tuple[ProviderSpec, ...] = (
65
65
  base_url="https://api.anthropic.com/v1",
66
66
  keys_url="https://console.anthropic.com/settings/keys",
67
67
  blurb="Claude, direct from the source. Strongest at long agentic work.",
68
- default_model="claude-sonnet-4-5",
69
- models=("claude-opus-4-1", "claude-sonnet-4-5", "claude-haiku-4-5"),
68
+ # These have to be models the pricing registry knows. It did not know
69
+ # any of the three that were here, so the wizard's own default landed
70
+ # on an unpriced model: the context gauge read a fallback 128k instead
71
+ # of a million, and the spend limit could not be enforced at all.
72
+ default_model="claude-sonnet-5",
73
+ models=("claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5"),
70
74
  env_key="ANTHROPIC_API_KEY",
71
75
  rank=2,
72
76
  ),
@@ -27,7 +27,8 @@ import sys
27
27
  from typing import Any
28
28
 
29
29
  from . import __version__
30
- from .config import Config, load as load_config
30
+ from .config import Config
31
+ from .config import load as load_config
31
32
 
32
33
 
33
34
  def build_parser() -> argparse.ArgumentParser:
@@ -62,6 +63,12 @@ def build_parser() -> argparse.ArgumentParser:
62
63
  run.add_argument("--max-steps", type=int, help="override the step limit")
63
64
 
64
65
  sub.add_parser("setup", help="choose a provider and model, and save the answers")
66
+ brought = sub.add_parser(
67
+ "import", help="bring keys, model and skills over from another agent")
68
+ brought.add_argument("--dry-run", action="store_true",
69
+ help="say what would come over, and change nothing")
70
+ brought.add_argument("--keys-only", action="store_true",
71
+ help="leave the skills and the model where they are")
65
72
  doctor = sub.add_parser(
66
73
  "doctor", help="check everything, and repair what can be repaired")
67
74
  doctor.add_argument("--fix", action="store_true",
@@ -122,15 +129,42 @@ def apply_overrides(config: Config, args: argparse.Namespace) -> Config:
122
129
  # --------------------------------------------------------------------------- #
123
130
 
124
131
 
132
+ def _greet_on_stderr(config: Config, memory: Any, skills: Any) -> None:
133
+ """The wordmark, on the error stream with the progress.
134
+
135
+ Same reason the progress is there: `comodor run … | jq` has to see the
136
+ answer and nothing else, and a logo in the middle of a JSON document is
137
+ somebody else's afternoon.
138
+ """
139
+ from rich.console import Console
140
+
141
+ from .ui import banner
142
+ from .ui import console as console_module
143
+
144
+ theme = console_module.prepare_theme(config.ui.theme, config.ui.ascii_borders,
145
+ no_color=False)
146
+ errors = Console(stderr=True, theme=theme.rich_theme())
147
+ banner.show(errors, theme, config,
148
+ version=_release(), model=config.active_model(),
149
+ project=str(config.paths.project),
150
+ standing=banner.gather(memory, skills))
151
+
152
+
153
+ def _release() -> str:
154
+ from . import __version__
155
+
156
+ return __version__
157
+
158
+
125
159
  def run_headless(config: Config, args: argparse.Namespace) -> int:
126
160
  """One task, no TUI. Used by scripts, hooks and CI."""
127
161
  from .agent import AgentLoop, Conversation
128
162
  from .agent.spawn import spawner
129
163
  from .events import EventBus, Kind
130
164
  from .learning import LearningEngine
165
+ from .mcp import MCPManager
131
166
  from .providers.gateway import Gateway
132
167
  from .safety import CheckpointStore, PermissionEngine, Redactor
133
- from .mcp import MCPManager
134
168
  from .skills import load_for as load_skills
135
169
  from .tools import ToolRegistry
136
170
 
@@ -161,6 +195,14 @@ def run_headless(config: Config, args: argparse.Namespace) -> int:
161
195
  permissions, Conversation(), memory, skills=skills)
162
196
 
163
197
  if not args.json:
198
+ _greet_on_stderr(config, memory, skills)
199
+ for note in config.complaints:
200
+ print(f"config: {note}", file=sys.stderr)
201
+ if config.project_refused:
202
+ print(f"config: this project cannot set "
203
+ f"{', '.join(sorted(config.project_refused))} — only yours can",
204
+ file=sys.stderr)
205
+
164
206
  # Progress on stderr keeps stdout clean for the answer itself.
165
207
  def echo(event: Any) -> None:
166
208
  if event.kind is Kind.TOOL_START:
@@ -330,8 +372,8 @@ def run_update(config: Config, check_only: bool = False) -> int:
330
372
  on: what is out there, whether it is newer than this, how this copy would
331
373
  be upgraded, and what version answers afterwards.
332
374
  """
333
- from .ui import console as console_module
334
375
  from . import update as updater
376
+ from .ui import console as console_module
335
377
 
336
378
  theme = console_module.prepare_theme(config.ui.theme, config.ui.ascii_borders,
337
379
  no_color=False)
@@ -563,6 +605,91 @@ def run_preview(config: Config, args: argparse.Namespace) -> int:
563
605
  # --------------------------------------------------------------------------- #
564
606
 
565
607
 
608
+ def _say_what_the_copy_refused(console, found: list, already: dict) -> None:
609
+ """Reasons the copy added after the listing was printed."""
610
+ for entry in found:
611
+ for note in entry.passed_over[already[id(entry)]:]:
612
+ console.print(f"[dim] not imported: {note}[/dim]")
613
+
614
+
615
+ def run_import(config: Config, args: argparse.Namespace) -> int:
616
+ """Bring another agent's settings over, outside the first run.
617
+
618
+ Same rules as the wizard's step: nothing already set here is replaced,
619
+ nothing is moved, and what was deliberately left behind is said rather than
620
+ skipped silently.
621
+ """
622
+ from . import migrate
623
+ from .ui import console as console_module
624
+
625
+ theme = console_module.prepare_theme(config.ui.theme, config.ui.ascii_borders,
626
+ no_color=False)
627
+ console = console_module.build(theme)
628
+
629
+ try:
630
+ found = migrate.discover()
631
+ except OSError as error:
632
+ console.print(f"[bad]could not look: {error}[/bad]")
633
+ return 1
634
+
635
+ if not found:
636
+ console.print("Nothing to import — no OpenClaw or Hermes installation "
637
+ "was found in your home directory.")
638
+ return 0
639
+
640
+ for entry in found:
641
+ console.print(f"[accent]{entry.tool}[/accent] {entry.summary()}")
642
+ console.print(f"[dim]{entry.root}[/dim]")
643
+ for note in entry.passed_over:
644
+ console.print(f"[dim] not imported: {note}[/dim]")
645
+
646
+ # Anything the copy itself decides against - a skill folder holding a link
647
+ # out of itself, one far larger than a skill - is added to this list while
648
+ # applying, which is after the lines above. Remembering where each list
649
+ # ended is what lets those reasons reach the screen, rather than leaving
650
+ # the reader to wonder why a skill they can see did not arrive.
651
+ already = {id(entry): len(entry.passed_over) for entry in found}
652
+
653
+ if args.dry_run:
654
+ console.print("\n[dim]--dry-run: nothing was changed.[/dim]")
655
+ return 0
656
+
657
+ took = migrate.Outcome()
658
+ for entry in found:
659
+ try:
660
+ got = migrate.apply(entry, config, take_keys=True,
661
+ take_skills=not args.keys_only,
662
+ take_model=not args.keys_only)
663
+ except OSError as error:
664
+ console.print(f"[bad]could not read {entry.tool}: {error}[/bad]")
665
+ continue
666
+ took.keys += got.keys
667
+ took.skills += got.skills
668
+ took.skipped += got.skipped
669
+ took.model = took.model or got.model
670
+
671
+ if not took.anything:
672
+ console.print("\nNothing new — everything it found is already set here.")
673
+ for note in took.skipped:
674
+ console.print(f"[dim] {note}[/dim]")
675
+ _say_what_the_copy_refused(console, found, already)
676
+ return 0
677
+
678
+ config.save()
679
+ console.print()
680
+ _say_what_the_copy_refused(console, found, already)
681
+ if took.keys:
682
+ console.print(f"[good]keys[/good] {', '.join(took.keys)}")
683
+ if took.model:
684
+ console.print(f"[good]model[/good] {took.model}")
685
+ if took.skills:
686
+ console.print(f"[good]skills[/good] {', '.join(took.skills)}")
687
+ for note in took.skipped:
688
+ console.print(f"[dim] kept as it was — {note}[/dim]")
689
+ console.print(f"\n[dim]saved to {config.paths.config_file}[/dim]")
690
+ return 0
691
+
692
+
566
693
  def main(argv: list[str] | None = None) -> int:
567
694
  args = build_parser().parse_args(argv)
568
695
  config = apply_overrides(load_config(args.cwd), args)
@@ -589,6 +716,8 @@ def main(argv: list[str] | None = None) -> int:
589
716
  return run_preview(config, args)
590
717
  if args.command == "setup":
591
718
  return run_setup_command(config)
719
+ if args.command == "import":
720
+ return run_import(config, args)
592
721
  if args.command == "run":
593
722
  return run_headless(config, args)
594
723