comodor 0.2.3__tar.gz → 0.3.1__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 (117) hide show
  1. {comodor-0.2.3 → comodor-0.3.1}/PKG-INFO +83 -21
  2. {comodor-0.2.3 → comodor-0.3.1}/README.md +82 -20
  3. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/_version.py +2 -2
  4. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/cli.py +99 -0
  5. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/config.py +3 -0
  6. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/doctor.py +44 -6
  7. comodor-0.3.1/src/comodor/tools/browser.py +398 -0
  8. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/registry.py +19 -1
  9. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/app.py +1 -0
  10. comodor-0.3.1/src/comodor/update.py +324 -0
  11. comodor-0.3.1/src/comodor/workspace.py +127 -0
  12. {comodor-0.2.3 → comodor-0.3.1}/tests/conftest.py +12 -0
  13. comodor-0.3.1/tests/test_browser.py +320 -0
  14. {comodor-0.2.3 → comodor-0.3.1}/tests/test_doctor.py +59 -0
  15. comodor-0.3.1/tests/test_update.py +325 -0
  16. {comodor-0.2.3 → comodor-0.3.1}/.gitignore +0 -0
  17. {comodor-0.2.3 → comodor-0.3.1}/LICENSE +0 -0
  18. {comodor-0.2.3 → comodor-0.3.1}/pyproject.toml +0 -0
  19. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/__init__.py +0 -0
  20. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/__main__.py +0 -0
  21. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/agent/__init__.py +0 -0
  22. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/agent/context.py +0 -0
  23. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/agent/loop.py +0 -0
  24. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/agent/prompts.py +0 -0
  25. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/agent/tokens.py +0 -0
  26. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/catalogue.py +0 -0
  27. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/events.py +0 -0
  28. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/__init__.py +0 -0
  29. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/bm25.py +0 -0
  30. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/hotindex.py +0 -0
  31. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/memory.py +0 -0
  32. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/progress.py +0 -0
  33. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/reflect.py +0 -0
  34. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/rules.py +0 -0
  35. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/signals.py +0 -0
  36. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/store.py +0 -0
  37. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/learning/writer.py +0 -0
  38. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/mcp/__init__.py +0 -0
  39. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/mcp/catalogue.py +0 -0
  40. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/mcp/commands.py +0 -0
  41. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/mcp/manager.py +0 -0
  42. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/mcp/protocol.py +0 -0
  43. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/net/__init__.py +0 -0
  44. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/net/http.py +0 -0
  45. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/net/sse.py +0 -0
  46. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/paths.py +0 -0
  47. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/__init__.py +0 -0
  48. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/anthropic.py +0 -0
  49. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/base.py +0 -0
  50. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/fake.py +0 -0
  51. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/gateway.py +0 -0
  52. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/openai_compat.py +0 -0
  53. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/providers/registry.py +0 -0
  54. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/safety/__init__.py +0 -0
  55. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/safety/checkpoints.py +0 -0
  56. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/safety/permissions.py +0 -0
  57. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/safety/redact.py +0 -0
  58. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/session/__init__.py +0 -0
  59. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/session/search.py +0 -0
  60. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/session/store.py +0 -0
  61. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/setup.py +0 -0
  62. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/skills/__init__.py +0 -0
  63. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/skills/examples.py +0 -0
  64. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/skills/loader.py +0 -0
  65. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/skills/propose.py +0 -0
  66. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/skills/registry.py +0 -0
  67. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/__init__.py +0 -0
  68. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/base.py +0 -0
  69. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/fs.py +0 -0
  70. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/history.py +0 -0
  71. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/mcp.py +0 -0
  72. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/search.py +0 -0
  73. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/shell.py +0 -0
  74. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/skills.py +0 -0
  75. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/todo.py +0 -0
  76. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/tools/web.py +0 -0
  77. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/__init__.py +0 -0
  78. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/chooser.py +0 -0
  79. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/console.py +0 -0
  80. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/input/__init__.py +0 -0
  81. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/input/keys.py +0 -0
  82. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/input/reader.py +0 -0
  83. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/layout.py +0 -0
  84. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/markdown.py +0 -0
  85. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/screen.py +0 -0
  86. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/theme.py +0 -0
  87. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/__init__.py +0 -0
  88. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/buttons.py +0 -0
  89. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/chat.py +0 -0
  90. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/history.py +0 -0
  91. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/overlay.py +0 -0
  92. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/panel.py +0 -0
  93. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/progress.py +0 -0
  94. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/prompt.py +0 -0
  95. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/statusbar.py +0 -0
  96. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/ui/widgets/toast.py +0 -0
  97. {comodor-0.2.3 → comodor-0.3.1}/src/comodor/uninstall.py +0 -0
  98. {comodor-0.2.3 → comodor-0.3.1}/tests/support/fake_mcp_server.py +0 -0
  99. {comodor-0.2.3 → comodor-0.3.1}/tests/test_agent_loop.py +0 -0
  100. {comodor-0.2.3 → comodor-0.3.1}/tests/test_app.py +0 -0
  101. {comodor-0.2.3 → comodor-0.3.1}/tests/test_chooser.py +0 -0
  102. {comodor-0.2.3 → comodor-0.3.1}/tests/test_history.py +0 -0
  103. {comodor-0.2.3 → comodor-0.3.1}/tests/test_input.py +0 -0
  104. {comodor-0.2.3 → comodor-0.3.1}/tests/test_layout.py +0 -0
  105. {comodor-0.2.3 → comodor-0.3.1}/tests/test_learning.py +0 -0
  106. {comodor-0.2.3 → comodor-0.3.1}/tests/test_markdown.py +0 -0
  107. {comodor-0.2.3 → comodor-0.3.1}/tests/test_mcp.py +0 -0
  108. {comodor-0.2.3 → comodor-0.3.1}/tests/test_performance.py +0 -0
  109. {comodor-0.2.3 → comodor-0.3.1}/tests/test_progress.py +0 -0
  110. {comodor-0.2.3 → comodor-0.3.1}/tests/test_propose.py +0 -0
  111. {comodor-0.2.3 → comodor-0.3.1}/tests/test_providers.py +0 -0
  112. {comodor-0.2.3 → comodor-0.3.1}/tests/test_reflex.py +0 -0
  113. {comodor-0.2.3 → comodor-0.3.1}/tests/test_run_loop.py +0 -0
  114. {comodor-0.2.3 → comodor-0.3.1}/tests/test_setup.py +0 -0
  115. {comodor-0.2.3 → comodor-0.3.1}/tests/test_skills.py +0 -0
  116. {comodor-0.2.3 → comodor-0.3.1}/tests/test_tools.py +0 -0
  117. {comodor-0.2.3 → comodor-0.3.1}/tests/test_uninstall.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comodor
3
- Version: 0.2.3
3
+ Version: 0.3.1
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
@@ -28,6 +28,11 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  **A coding agent that lives in your terminal — and learns the way you correct it.**
30
30
 
31
+ [![PyPI](https://img.shields.io/pypi/v/comodor?label=pypi&color=c4441e)](https://pypi.org/project/comodor/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/comodor)](https://pypi.org/project/comodor/)
33
+ [![CI](https://github.com/ifekri/Comodor/actions/workflows/ci.yml/badge.svg)](https://github.com/ifekri/Comodor/actions/workflows/ci.yml)
34
+ [![Licence](https://img.shields.io/pypi/l/comodor?color=555)](LICENSE)
35
+
31
36
  [comodor.ai](https://comodor.ai) · [Install](#install) · [What it can do](#what-it-can-do)
32
37
 
33
38
  ---
@@ -113,12 +118,27 @@ Four questions, once. Nothing to create beforehand — no config file, no
113
118
  environment variable, no documentation to read first.
114
119
 
115
120
  ```
116
- 1/4 Which model provider? 18 to choose from, numbered
117
- 2/4 API key masked, with a link to the page that issues one
118
- 3/4 Which model? read live from the provider you just chose
119
- 4/4 How much should it ask? ask first · writes allowed · full autonomy
121
+ ✓ provider Ollama (local)
122
+ ✓ api key not needed
123
+
124
+ 3/4 Which model?
125
+ ┌─ Models ──────────────────────────────────────────────┐
126
+ │ › qwen2.5-coder:14b recommended │
127
+ │ llama3.3 │
128
+ │ deepseek-r1:14b │
129
+ └─────────────────────────────────────────────────────────┘
130
+ ↑↓ move enter choose type filter esc cancel
120
131
  ```
121
132
 
133
+ One question per screen, answered with the arrow keys. Where a provider offers
134
+ sixty models, typing filters them. Piped or scripted, the same questions arrive
135
+ as a numbered list, so it can still be automated.
136
+
137
+ Then it shows you the directory it is about to work in and asks once — the
138
+ project root is found by walking upwards, and the answer is occasionally a
139
+ surprise worth seeing before anything reads it. Approved folders are
140
+ remembered.
141
+
122
142
  You are not asked again. Change your mind later with `comodor setup`.
123
143
 
124
144
  **No API key?** `comodor --demo` runs the whole interface offline — every
@@ -191,6 +211,35 @@ Everything you have ever asked is searchable.
191
211
  The agent searches it too, on its own, when you refer to earlier work — *"like
192
212
  we did last time"*, *"that bug from last week"*.
193
213
 
214
+ ### It can browse, not just fetch
215
+
216
+ Most agents get one page at a time: download a URL, strip the markup, and the
217
+ links go with it — so the only way onward is guessing another URL. Comodor
218
+ browses.
219
+
220
+ ```
221
+ › find out how the GitHub MCP server handles rate limits
222
+
223
+ ⚙ search: github mcp server rate limit 1.2s
224
+ ⚙ browse https://github.com/modelcontextprotocol/servers 0.8s
225
+ Links on this page:
226
+ 1. src/github → …/tree/main/src/github
227
+ ⚙ follow link 1 0.6s
228
+ ⚙ find "rate limit" on the page 0.0s
229
+ ```
230
+
231
+ The links come back numbered and resolved, so the next move is `follow 4`
232
+ rather than a guess — and links inside the content rank above the navigation
233
+ bar that every page of a documentation site repeats. It is one session, so
234
+ cookies, redirects and consent pages survive the hop. Long pages are handed
235
+ over a screenful at a time with `find` to jump, instead of being cut off at
236
+ 40,000 characters. Moving around a page it has already fetched touches no
237
+ network and asks no permission; a new host does.
238
+
239
+ There is no JavaScript engine, and there is not going to be one — that means a
240
+ real browser, which means a real dependency. A page that draws itself in the
241
+ client says so and points at the Puppeteer server below rather than pretending.
242
+
194
243
  ### It connects to other tools
195
244
 
196
245
  Comodor speaks the [Model Context Protocol](https://modelcontextprotocol.io),
@@ -233,6 +282,34 @@ cache gets deleted. A corrupt config is reported and left exactly as it was,
233
282
  because it holds your API key — the one thing on your machine that cannot be
234
283
  regenerated.
235
284
 
285
+ ### It updates itself
286
+
287
+ ```
288
+ $ comodor update
289
+
290
+ Comodor 0.2.3
291
+ 0.3.0 is available. https://pypi.org/project/comodor/0.3.0/
292
+ installed as a uv tool
293
+ uv tool upgrade comodor
294
+
295
+ updating…
296
+ now on 0.3.0
297
+ ```
298
+
299
+ It uses whatever put it there — uv, pipx, pip, or the environment the installer
300
+ built — because guessing wrong is worse than not offering the command: a `pip
301
+ install --upgrade` inside a uv environment appears to work and leaves uv's
302
+ record pointing at a version that is gone.
303
+
304
+ Afterwards it runs the new one and asks what it is, and *that* is what gets
305
+ printed. An upgrade that reports success by echoing the number it was aiming at
306
+ is how a silently failed install goes unnoticed for a week. `--check` says
307
+ what is available and changes nothing, and `comodor doctor` mentions a new
308
+ release without ever installing one.
309
+
310
+ A source checkout is left alone: `git pull` is the upgrade, and overwriting a
311
+ working tree with a release throws away work that was never committed.
312
+
236
313
  ### It leaves when you ask it to
237
314
 
238
315
  ```
@@ -271,22 +348,6 @@ not by searching your disk. Three things it will not do: touch a source
271
348
  checkout, take a directory off your PATH that other programs are still using,
272
349
  or claim to have deleted a file the operating system would not let go of.
273
350
 
274
- ### It can prove it is improving
275
-
276
- Every tool claims to get better over time. Comodor shows the numbers.
277
-
278
- ```
279
- ◈ Steps per task down 40% since the first tasks in this project.
280
-
281
- metric trend now vs first
282
- Steps per task ▇██▇▇▆▇▅▅▅▅▆▄▅▄▅▄▄▂▄▂▁▂▂▂▁▁▁▁▁ 5.3 ↓40%
283
- Corrections per task ████▆▇▆█▆▆▆▇▆▆▅▃▃▅▆▆▅▃▆▃▆▃▂▁▁▃ 0.9 ↓65%
284
- Approvals asked █▇▇▇▇▇▇▇▇▇▅▅▅▅▅▅▅▅▅▅▃▃▃▃▃▃▃▃▃▁ 0.8 ↓73%
285
- ```
286
-
287
- The panel is built to under-claim: with too little history it says so, and a
288
- fall from 0.4 to 0 is never allowed to headline as "down 100%".
289
-
290
351
  ---
291
352
 
292
353
  ## You stay in control
@@ -339,6 +400,7 @@ comodor --demo # offline walkthrough, no key needed
339
400
  comodor run "fix the failing test" --yes # one task, headless, for scripts
340
401
  comodor run "audit this module" --json # machine-readable, for pipelines
341
402
  comodor doctor # check everything; --fix repairs it
403
+ comodor update # move to the newest release; --check first
342
404
  comodor uninstall # remove it completely; --dry-run first
343
405
  ```
344
406
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  **A coding agent that lives in your terminal — and learns the way you correct it.**
4
4
 
5
+ [![PyPI](https://img.shields.io/pypi/v/comodor?label=pypi&color=c4441e)](https://pypi.org/project/comodor/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/comodor)](https://pypi.org/project/comodor/)
7
+ [![CI](https://github.com/ifekri/Comodor/actions/workflows/ci.yml/badge.svg)](https://github.com/ifekri/Comodor/actions/workflows/ci.yml)
8
+ [![Licence](https://img.shields.io/pypi/l/comodor?color=555)](LICENSE)
9
+
5
10
  [comodor.ai](https://comodor.ai) · [Install](#install) · [What it can do](#what-it-can-do)
6
11
 
7
12
  ---
@@ -87,12 +92,27 @@ Four questions, once. Nothing to create beforehand — no config file, no
87
92
  environment variable, no documentation to read first.
88
93
 
89
94
  ```
90
- 1/4 Which model provider? 18 to choose from, numbered
91
- 2/4 API key masked, with a link to the page that issues one
92
- 3/4 Which model? read live from the provider you just chose
93
- 4/4 How much should it ask? ask first · writes allowed · full autonomy
95
+ ✓ provider Ollama (local)
96
+ ✓ api key not needed
97
+
98
+ 3/4 Which model?
99
+ ┌─ Models ──────────────────────────────────────────────┐
100
+ │ › qwen2.5-coder:14b recommended │
101
+ │ llama3.3 │
102
+ │ deepseek-r1:14b │
103
+ └─────────────────────────────────────────────────────────┘
104
+ ↑↓ move enter choose type filter esc cancel
94
105
  ```
95
106
 
107
+ One question per screen, answered with the arrow keys. Where a provider offers
108
+ sixty models, typing filters them. Piped or scripted, the same questions arrive
109
+ as a numbered list, so it can still be automated.
110
+
111
+ Then it shows you the directory it is about to work in and asks once — the
112
+ project root is found by walking upwards, and the answer is occasionally a
113
+ surprise worth seeing before anything reads it. Approved folders are
114
+ remembered.
115
+
96
116
  You are not asked again. Change your mind later with `comodor setup`.
97
117
 
98
118
  **No API key?** `comodor --demo` runs the whole interface offline — every
@@ -165,6 +185,35 @@ Everything you have ever asked is searchable.
165
185
  The agent searches it too, on its own, when you refer to earlier work — *"like
166
186
  we did last time"*, *"that bug from last week"*.
167
187
 
188
+ ### It can browse, not just fetch
189
+
190
+ Most agents get one page at a time: download a URL, strip the markup, and the
191
+ links go with it — so the only way onward is guessing another URL. Comodor
192
+ browses.
193
+
194
+ ```
195
+ › find out how the GitHub MCP server handles rate limits
196
+
197
+ ⚙ search: github mcp server rate limit 1.2s
198
+ ⚙ browse https://github.com/modelcontextprotocol/servers 0.8s
199
+ Links on this page:
200
+ 1. src/github → …/tree/main/src/github
201
+ ⚙ follow link 1 0.6s
202
+ ⚙ find "rate limit" on the page 0.0s
203
+ ```
204
+
205
+ The links come back numbered and resolved, so the next move is `follow 4`
206
+ rather than a guess — and links inside the content rank above the navigation
207
+ bar that every page of a documentation site repeats. It is one session, so
208
+ cookies, redirects and consent pages survive the hop. Long pages are handed
209
+ over a screenful at a time with `find` to jump, instead of being cut off at
210
+ 40,000 characters. Moving around a page it has already fetched touches no
211
+ network and asks no permission; a new host does.
212
+
213
+ There is no JavaScript engine, and there is not going to be one — that means a
214
+ real browser, which means a real dependency. A page that draws itself in the
215
+ client says so and points at the Puppeteer server below rather than pretending.
216
+
168
217
  ### It connects to other tools
169
218
 
170
219
  Comodor speaks the [Model Context Protocol](https://modelcontextprotocol.io),
@@ -207,6 +256,34 @@ cache gets deleted. A corrupt config is reported and left exactly as it was,
207
256
  because it holds your API key — the one thing on your machine that cannot be
208
257
  regenerated.
209
258
 
259
+ ### It updates itself
260
+
261
+ ```
262
+ $ comodor update
263
+
264
+ Comodor 0.2.3
265
+ 0.3.0 is available. https://pypi.org/project/comodor/0.3.0/
266
+ installed as a uv tool
267
+ uv tool upgrade comodor
268
+
269
+ updating…
270
+ now on 0.3.0
271
+ ```
272
+
273
+ It uses whatever put it there — uv, pipx, pip, or the environment the installer
274
+ built — because guessing wrong is worse than not offering the command: a `pip
275
+ install --upgrade` inside a uv environment appears to work and leaves uv's
276
+ record pointing at a version that is gone.
277
+
278
+ Afterwards it runs the new one and asks what it is, and *that* is what gets
279
+ printed. An upgrade that reports success by echoing the number it was aiming at
280
+ is how a silently failed install goes unnoticed for a week. `--check` says
281
+ what is available and changes nothing, and `comodor doctor` mentions a new
282
+ release without ever installing one.
283
+
284
+ A source checkout is left alone: `git pull` is the upgrade, and overwriting a
285
+ working tree with a release throws away work that was never committed.
286
+
210
287
  ### It leaves when you ask it to
211
288
 
212
289
  ```
@@ -245,22 +322,6 @@ not by searching your disk. Three things it will not do: touch a source
245
322
  checkout, take a directory off your PATH that other programs are still using,
246
323
  or claim to have deleted a file the operating system would not let go of.
247
324
 
248
- ### It can prove it is improving
249
-
250
- Every tool claims to get better over time. Comodor shows the numbers.
251
-
252
- ```
253
- ◈ Steps per task down 40% since the first tasks in this project.
254
-
255
- metric trend now vs first
256
- Steps per task ▇██▇▇▆▇▅▅▅▅▆▄▅▄▅▄▄▂▄▂▁▂▂▂▁▁▁▁▁ 5.3 ↓40%
257
- Corrections per task ████▆▇▆█▆▆▆▇▆▆▅▃▃▅▆▆▅▃▆▃▆▃▂▁▁▃ 0.9 ↓65%
258
- Approvals asked █▇▇▇▇▇▇▇▇▇▅▅▅▅▅▅▅▅▅▅▃▃▃▃▃▃▃▃▃▁ 0.8 ↓73%
259
- ```
260
-
261
- The panel is built to under-claim: with too little history it says so, and a
262
- fall from 0.4 to 0 is never allowed to headline as "down 100%".
263
-
264
325
  ---
265
326
 
266
327
  ## You stay in control
@@ -313,6 +374,7 @@ comodor --demo # offline walkthrough, no key needed
313
374
  comodor run "fix the failing test" --yes # one task, headless, for scripts
314
375
  comodor run "audit this module" --json # machine-readable, for pipelines
315
376
  comodor doctor # check everything; --fix repairs it
377
+ comodor update # move to the newest release; --check first
316
378
  comodor uninstall # remove it completely; --dry-run first
317
379
  ```
318
380
 
@@ -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.2.3'
22
- __version_tuple__ = version_tuple = (0, 2, 3)
21
+ __version__ = version = '0.3.1'
22
+ __version_tuple__ = version_tuple = (0, 3, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,6 +9,8 @@ Three ways in:
9
9
  * ``comodor setup`` — the first-run questions again, on demand.
10
10
  * ``comodor doctor`` — what is configured, what is reachable, what the terminal
11
11
  can do. The first thing to run when something is not working.
12
+ * ``comodor update`` — move to the newest published version, using whichever
13
+ of uv, pipx or pip put this copy here.
12
14
  * ``comodor uninstall`` — takes every file, folder and PATH line back off the
13
15
  machine, after showing you the list.
14
16
 
@@ -64,6 +66,11 @@ def build_parser() -> argparse.ArgumentParser:
64
66
  doctor.add_argument("--fix", action="store_true",
65
67
  help="apply every repair the check found")
66
68
 
69
+ upgrade = sub.add_parser(
70
+ "update", help="upgrade to the newest published version")
71
+ upgrade.add_argument("--check", action="store_true",
72
+ help="say what is available and change nothing")
73
+
67
74
  remove = sub.add_parser(
68
75
  "uninstall",
69
76
  help="remove Comodor and everything it has written, completely")
@@ -303,6 +310,80 @@ def run_doctor(config: Config, fix: bool = False) -> int:
303
310
  return 1 if report.worst is Status.FAIL else 0
304
311
 
305
312
 
313
+ def run_update(config: Config, check_only: bool = False) -> int:
314
+ """Move to the newest published version.
315
+
316
+ Four steps, and each one can stop the run with something the user can act
317
+ on: what is out there, whether it is newer than this, how this copy would
318
+ be upgraded, and what version answers afterwards.
319
+ """
320
+ from .ui import console as console_module
321
+ from . import update as updater
322
+
323
+ theme = console_module.prepare_theme(config.ui.theme, config.ui.ascii_borders,
324
+ no_color=False)
325
+ console = console_module.build(theme)
326
+ here = updater.current()
327
+
328
+ console.print(f"\n[title]Comodor {here}[/title]")
329
+
330
+ release = updater.latest()
331
+ if release is None:
332
+ console.print(" [bad]Could not reach the package index.[/bad] "
333
+ "[dim]Check the connection and try again.[/dim]\n")
334
+ return 1
335
+
336
+ step = updater.plan()
337
+
338
+ if not updater.is_newer(release.version, here):
339
+ # Ahead of the index rather than behind it: a development build, which
340
+ # is a different sentence from "up to date".
341
+ ahead = updater.is_newer(here, release.version)
342
+ console.print(
343
+ f" [good]{release.version} is the newest published version"
344
+ f"[/good][dim], and this is "
345
+ f"{'ahead of it' if ahead else 'it'}.[/dim]")
346
+ if step.detail:
347
+ console.print(f" [dim]{step.detail}[/dim]")
348
+ console.print("")
349
+ return 0
350
+
351
+ console.print(f" [accent]{release.version}[/accent] is available."
352
+ f" [dim]{release.url}[/dim]")
353
+
354
+ if step.blocked:
355
+ console.print(f"\n [warn]{step.blocked}[/warn]\n")
356
+ return 1
357
+
358
+ if step.detail:
359
+ console.print(f" [dim]{step.detail}[/dim]")
360
+ console.print(f" [dim]{' '.join(step.command)}[/dim]")
361
+
362
+ if check_only:
363
+ console.print("\n[dim]Nothing was changed: this was --check.[/dim]\n")
364
+ return 0
365
+
366
+ console.print("\n updating…")
367
+ # Run it, then ask what version answers. Asked, not assumed.
368
+ outcome = updater.upgrade(step, release.version)
369
+
370
+ if not outcome.ok:
371
+ console.print(f" [bad]failed[/bad] [dim]{outcome.message}[/dim]\n")
372
+ return 1
373
+ if outcome.deferred:
374
+ console.print(f" [good]{outcome.message}[/good]\n")
375
+ return 0
376
+ if outcome.forced:
377
+ # Worth saying out loud: something was pinned, and now it is not.
378
+ console.print(" [dim]the recorded requirement pinned the version, so it "
379
+ "was reinstalled from the index[/dim]")
380
+ if outcome.version:
381
+ console.print(f" [good]now on {outcome.version}[/good]\n")
382
+ else:
383
+ console.print(f" [good]done[/good] [dim]— {outcome.message}.[/dim]\n")
384
+ return 0
385
+
386
+
306
387
  def run_uninstall(config: Config, dry_run: bool = False,
307
388
  assume_yes: bool = False) -> int:
308
389
  """Show everything that would go, then go.
@@ -448,6 +529,8 @@ def main(argv: list[str] | None = None) -> int:
448
529
 
449
530
  if args.command == "doctor":
450
531
  return run_doctor(config, fix=getattr(args, "fix", False))
532
+ if args.command == "update":
533
+ return run_update(config, check_only=args.check)
451
534
  if args.command == "uninstall":
452
535
  return run_uninstall(config, dry_run=args.dry_run, assume_yes=args.yes)
453
536
  if args.command == "mcp":
@@ -484,6 +567,22 @@ def main(argv: list[str] | None = None) -> int:
484
567
  if config.needs_setup:
485
568
  return 1
486
569
 
570
+ # Which directory is this about? Asked once per folder, before the agent
571
+ # exists — the project root is worked out by walking upwards, and the
572
+ # answer is occasionally a surprise worth seeing before anything reads it.
573
+ # `--cwd` is the user naming it themselves, so it does not ask again.
574
+ if not args.cwd:
575
+ from .ui import console as console_module
576
+ from .workspace import confirm
577
+
578
+ theme = console_module.prepare_theme(config.ui.theme,
579
+ config.ui.ascii_borders, no_color=False)
580
+ chosen = confirm(config, console_module.build(theme), theme)
581
+ if chosen is None:
582
+ return 0
583
+ if chosen != config.paths.project:
584
+ config = apply_overrides(load_config(str(chosen)), args)
585
+
487
586
  from .ui.app import App
488
587
 
489
588
  resume = None
@@ -227,6 +227,9 @@ class SafetyConfig:
227
227
  deny_commands: list[str] = field(default_factory=lambda: list(DEFAULT_DENY))
228
228
  workspace_only: bool = True
229
229
  max_file_read_bytes: int = 512_000
230
+ #: Directories the user has confirmed as a workspace. Exact paths, never
231
+ #: prefixes: approving ~/work/api must not quietly approve ~/work.
232
+ trusted_folders: list[str] = field(default_factory=list)
230
233
 
231
234
 
232
235
  # Patterns that are never worth running from an agent, however it is prompted.
@@ -92,13 +92,22 @@ class Report:
92
92
  # --------------------------------------------------------------------------- #
93
93
 
94
94
 
95
- def run_checks(config: Config) -> Report:
96
- """Every check, in the order a person would want to read them."""
95
+ def run_checks(config: Config, online: bool = True) -> Report:
96
+ """Every check, in the order a person would want to read them.
97
+
98
+ `online` exists because one check asks the package index whether there is a
99
+ newer release, and everything else here reads the local machine. A caller
100
+ that must not touch the network — a test, or a diagnostic being gathered
101
+ from somewhere with no route out — turns it off and gets the rest.
102
+ """
103
+ checks = [_check_config, _check_config_permissions, _check_provider,
104
+ _check_saved_provider, _check_model, _check_brain,
105
+ _check_search_index, _check_skills, _check_leftovers, _check_mcp]
106
+ if online:
107
+ checks.append(_check_version)
108
+
97
109
  report = Report()
98
- for check in (_check_config, _check_config_permissions, _check_provider,
99
- _check_saved_provider, _check_model, _check_brain,
100
- _check_search_index,
101
- _check_skills, _check_leftovers, _check_mcp):
110
+ for check in checks:
102
111
  try:
103
112
  finding = check(config)
104
113
  except Exception as error: # a check must never be the bug
@@ -450,3 +459,32 @@ def wait_for(condition: Callable[[], bool], timeout: float = 2.0,
450
459
  return True
451
460
  time.sleep(interval)
452
461
  return condition()
462
+
463
+
464
+ def _check_version(config: Config) -> Finding | None:
465
+ """Is there a newer release than the one running?
466
+
467
+ Doctor is where people look when something is not working, and "you are
468
+ four versions behind" is often the whole answer. It is not a repair, though
469
+ — `--fix` must never reach out and change what is installed under somebody
470
+ without being asked, so this states the command and stops.
471
+
472
+ On a short timeout, and silent when the network is not there. A diagnostic
473
+ that hangs for fifteen seconds on an aeroplane is a worse diagnostic than
474
+ one with a line missing.
475
+ """
476
+ from . import __version__
477
+ from .update import is_newer, latest
478
+
479
+ release = latest(timeout=(2.0, 3.0))
480
+ if release is None:
481
+ return None # offline; not a fault to report
482
+
483
+ if not is_newer(release.version, __version__):
484
+ return Finding("version", Status.OK, f"{__version__} is current")
485
+
486
+ return Finding(
487
+ "version", Status.WARN,
488
+ f"{__version__} is installed; {release.version} is out",
489
+ remedy="comodor update",
490
+ )