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.
- {comodor-0.8.4 → comodor-0.8.8}/PKG-INFO +63 -3
- {comodor-0.8.4 → comodor-0.8.8}/README.md +61 -2
- {comodor-0.8.4 → comodor-0.8.8}/pyproject.toml +24 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/_version.py +2 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/context.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/loop.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/catalogue.py +6 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/cli.py +132 -3
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/config.py +316 -16
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/doctor.py +38 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/store.py +6 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/catalogue.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/commands.py +2 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/http.py +1 -2
- comodor-0.8.8/src/comodor/migrate.py +496 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/http.py +18 -7
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/openai_compat.py +2 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/registry.py +16 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/setup.py +210 -16
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/propose.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/fs.py +2 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/shell.py +4 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/todo.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/__init__.py +2 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/app.py +97 -8
- comodor-0.8.8/src/comodor/ui/banner.py +250 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/reader.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/markdown.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/theme.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/chat.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/overlay.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/statusbar.py +0 -2
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/commands.py +80 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/server.py +4 -3
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/session.py +19 -5
- {comodor-0.8.4 → comodor-0.8.8}/tests/conftest.py +33 -4
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_associations.py +4 -1
- comodor-0.8.8/tests/test_banner.py +300 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_bidi.py +3 -2
- comodor-0.8.8/tests/test_config_save.py +236 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_delegate.py +3 -3
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_doctor.py +63 -9
- comodor-0.8.8/tests/test_import_command.py +139 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_layout.py +2 -1
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_learning.py +0 -1
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_mcp.py +12 -1
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_mcp_http.py +2 -0
- comodor-0.8.8/tests/test_migrate.py +481 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_providers.py +0 -2
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_real_browser.py +0 -1
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_setup.py +1 -1
- comodor-0.8.8/tests/test_setup_import.py +305 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_skills.py +2 -4
- comodor-0.8.8/tests/test_switching_model.py +158 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_tools.py +0 -1
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_uninstall.py +2 -2
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_update.py +6 -2
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_web.py +96 -3
- {comodor-0.8.4 → comodor-0.8.8}/.gitignore +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/LICENSE +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/__main__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/prompts.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/spawn.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/agent/tokens.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/cdp.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/launch.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/browser/page.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/events.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/associations.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/bm25.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/hotindex.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/memory.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/progress.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/reflect.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/rules.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/signals.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/learning/writer.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/manager.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/mcp/protocol.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/net/sse.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/paths.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/anthropic.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/base.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/caching.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/fake.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/providers/gateway.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/checkpoints.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/permissions.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/safety/redact.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/search.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/session/store.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/catalogue.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/commands.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/examples.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/loader.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/skills/registry.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/base.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/browse.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/browser.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/delegate.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/history.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/mcp.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/overflow.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/registry.py +1 -1
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/search.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/skills.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/tools/web.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/bidi.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/chooser.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/console.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/input/keys.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/layout.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/screen.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/buttons.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/history.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/panel.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/progress.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/prompt.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/ui/widgets/toast.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/uninstall.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/update.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/__init__.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/web/index.html +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/src/comodor/workspace.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/support/fake_mcp_server.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_agent_loop.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_app.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_browser.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_caching.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_chooser.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_history.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_input.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_markdown.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_overflow.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_performance.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_prefix_stability.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_progress.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_propose.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_reflex.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_run_loop.py +0 -0
- {comodor-0.8.4 → comodor-0.8.8}/tests/test_skill_budget.py +0 -0
- {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.
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 8,
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|