comodor 0.2.2__tar.gz → 0.2.3__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 (112) hide show
  1. {comodor-0.2.2 → comodor-0.2.3}/PKG-INFO +1 -1
  2. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/_version.py +2 -2
  3. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/uninstall.py +80 -18
  4. {comodor-0.2.2 → comodor-0.2.3}/tests/test_uninstall.py +39 -1
  5. {comodor-0.2.2 → comodor-0.2.3}/.gitignore +0 -0
  6. {comodor-0.2.2 → comodor-0.2.3}/LICENSE +0 -0
  7. {comodor-0.2.2 → comodor-0.2.3}/README.md +0 -0
  8. {comodor-0.2.2 → comodor-0.2.3}/pyproject.toml +0 -0
  9. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/__init__.py +0 -0
  10. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/__main__.py +0 -0
  11. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/agent/__init__.py +0 -0
  12. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/agent/context.py +0 -0
  13. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/agent/loop.py +0 -0
  14. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/agent/prompts.py +0 -0
  15. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/agent/tokens.py +0 -0
  16. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/catalogue.py +0 -0
  17. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/cli.py +0 -0
  18. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/config.py +0 -0
  19. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/doctor.py +0 -0
  20. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/events.py +0 -0
  21. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/__init__.py +0 -0
  22. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/bm25.py +0 -0
  23. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/hotindex.py +0 -0
  24. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/memory.py +0 -0
  25. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/progress.py +0 -0
  26. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/reflect.py +0 -0
  27. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/rules.py +0 -0
  28. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/signals.py +0 -0
  29. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/store.py +0 -0
  30. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/learning/writer.py +0 -0
  31. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/mcp/__init__.py +0 -0
  32. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/mcp/catalogue.py +0 -0
  33. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/mcp/commands.py +0 -0
  34. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/mcp/manager.py +0 -0
  35. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/mcp/protocol.py +0 -0
  36. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/net/__init__.py +0 -0
  37. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/net/http.py +0 -0
  38. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/net/sse.py +0 -0
  39. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/paths.py +0 -0
  40. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/__init__.py +0 -0
  41. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/anthropic.py +0 -0
  42. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/base.py +0 -0
  43. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/fake.py +0 -0
  44. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/gateway.py +0 -0
  45. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/openai_compat.py +0 -0
  46. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/providers/registry.py +0 -0
  47. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/safety/__init__.py +0 -0
  48. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/safety/checkpoints.py +0 -0
  49. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/safety/permissions.py +0 -0
  50. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/safety/redact.py +0 -0
  51. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/session/__init__.py +0 -0
  52. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/session/search.py +0 -0
  53. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/session/store.py +0 -0
  54. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/setup.py +0 -0
  55. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/skills/__init__.py +0 -0
  56. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/skills/examples.py +0 -0
  57. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/skills/loader.py +0 -0
  58. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/skills/propose.py +0 -0
  59. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/skills/registry.py +0 -0
  60. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/__init__.py +0 -0
  61. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/base.py +0 -0
  62. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/fs.py +0 -0
  63. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/history.py +0 -0
  64. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/mcp.py +0 -0
  65. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/registry.py +0 -0
  66. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/search.py +0 -0
  67. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/shell.py +0 -0
  68. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/skills.py +0 -0
  69. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/todo.py +0 -0
  70. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/tools/web.py +0 -0
  71. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/__init__.py +0 -0
  72. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/app.py +0 -0
  73. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/chooser.py +0 -0
  74. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/console.py +0 -0
  75. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/input/__init__.py +0 -0
  76. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/input/keys.py +0 -0
  77. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/input/reader.py +0 -0
  78. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/layout.py +0 -0
  79. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/markdown.py +0 -0
  80. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/screen.py +0 -0
  81. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/theme.py +0 -0
  82. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/__init__.py +0 -0
  83. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/buttons.py +0 -0
  84. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/chat.py +0 -0
  85. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/history.py +0 -0
  86. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/overlay.py +0 -0
  87. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/panel.py +0 -0
  88. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/progress.py +0 -0
  89. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/prompt.py +0 -0
  90. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/statusbar.py +0 -0
  91. {comodor-0.2.2 → comodor-0.2.3}/src/comodor/ui/widgets/toast.py +0 -0
  92. {comodor-0.2.2 → comodor-0.2.3}/tests/conftest.py +0 -0
  93. {comodor-0.2.2 → comodor-0.2.3}/tests/support/fake_mcp_server.py +0 -0
  94. {comodor-0.2.2 → comodor-0.2.3}/tests/test_agent_loop.py +0 -0
  95. {comodor-0.2.2 → comodor-0.2.3}/tests/test_app.py +0 -0
  96. {comodor-0.2.2 → comodor-0.2.3}/tests/test_chooser.py +0 -0
  97. {comodor-0.2.2 → comodor-0.2.3}/tests/test_doctor.py +0 -0
  98. {comodor-0.2.2 → comodor-0.2.3}/tests/test_history.py +0 -0
  99. {comodor-0.2.2 → comodor-0.2.3}/tests/test_input.py +0 -0
  100. {comodor-0.2.2 → comodor-0.2.3}/tests/test_layout.py +0 -0
  101. {comodor-0.2.2 → comodor-0.2.3}/tests/test_learning.py +0 -0
  102. {comodor-0.2.2 → comodor-0.2.3}/tests/test_markdown.py +0 -0
  103. {comodor-0.2.2 → comodor-0.2.3}/tests/test_mcp.py +0 -0
  104. {comodor-0.2.2 → comodor-0.2.3}/tests/test_performance.py +0 -0
  105. {comodor-0.2.2 → comodor-0.2.3}/tests/test_progress.py +0 -0
  106. {comodor-0.2.2 → comodor-0.2.3}/tests/test_propose.py +0 -0
  107. {comodor-0.2.2 → comodor-0.2.3}/tests/test_providers.py +0 -0
  108. {comodor-0.2.2 → comodor-0.2.3}/tests/test_reflex.py +0 -0
  109. {comodor-0.2.2 → comodor-0.2.3}/tests/test_run_loop.py +0 -0
  110. {comodor-0.2.2 → comodor-0.2.3}/tests/test_setup.py +0 -0
  111. {comodor-0.2.2 → comodor-0.2.3}/tests/test_skills.py +0 -0
  112. {comodor-0.2.2 → comodor-0.2.3}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comodor
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -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.2'
22
- __version_tuple__ = version_tuple = (0, 2, 2)
21
+ __version__ = version = '0.2.3'
22
+ __version_tuple__ = version_tuple = (0, 2, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -439,7 +439,7 @@ def survey(config: Config | None = None, cwd: Path | None = None) -> Survey:
439
439
  detail=install.detail,
440
440
  path=install.root,
441
441
  size=directory_size(install.root) if install.root else 0,
442
- remove=lambda tool=tool: _run_uninstaller(tool),
442
+ remove=lambda tool=tool, root=install.root: _run_uninstaller(tool, root),
443
443
  deferred=sys.platform == "win32",
444
444
  ))
445
445
  elif install.method == "venv" and install.owned:
@@ -490,9 +490,17 @@ def survey(config: Config | None = None, cwd: Path | None = None) -> Survey:
490
490
 
491
491
  others = _other_launchers(BIN_DIR)
492
492
  if others:
493
+ # Named, not counted. "2 other programs live there" leaves the reader
494
+ # with a decision they have no information for; `uv, uvx` tells them
495
+ # whether the line is worth keeping, and those two in particular were
496
+ # very likely fetched by this installer in the first place.
497
+ listed = ", ".join(others[:4])
498
+ if len(others) > 4:
499
+ listed += f" and {len(others) - 4} more"
493
500
  found.notes.append(
494
- f"{BIN_DIR} stays on your PATH: {others} other program"
495
- f"{'s' if others != 1 else ''} live there")
501
+ f"{BIN_DIR} stays on your PATH, and so does the line that put it "
502
+ f"there: {listed} still live in it. Remove the line by hand if "
503
+ f"none of those matter to you.")
496
504
  else:
497
505
  for profile, _ in profile_edits(BIN_DIR):
498
506
  found.add(Item(
@@ -525,30 +533,84 @@ def _within(path: Path, root: Path) -> bool:
525
533
  return False
526
534
 
527
535
 
528
- def _other_launchers(directory: Path) -> int:
529
- """How many programs besides ours live in the launcher directory."""
536
+ def _other_launchers(directory: Path) -> list[str]:
537
+ """What else lives in the launcher directory, by name."""
530
538
  if not directory.is_dir():
531
- return 0
539
+ return []
532
540
  ours = {"comodor", "comodor.exe", "comodor.cmd", "comodor-script.py"}
533
541
  try:
534
- return sum(1 for entry in directory.iterdir() if entry.name not in ours)
542
+ return sorted(entry.name for entry in directory.iterdir()
543
+ if entry.name not in ours)
535
544
  except OSError:
536
- return 0
537
-
545
+ return []
546
+
547
+
548
+ #: Where a tool manager lives when it is not on PATH. The same list the
549
+ #: installer searches, and for the same reason: `curl | sh` runs without the
550
+ #: user's profile, so `uv` gets bootstrapped into ~/.local/bin and nothing in
551
+ #: that session ever puts it on PATH.
552
+ TOOL_DIRS = (
553
+ Path.home() / ".local" / "bin",
554
+ Path.home() / ".cargo" / "bin",
555
+ Path.home() / "bin",
556
+ Path("/opt/homebrew/bin"),
557
+ Path("/usr/local/bin"),
558
+ Path("/home/linuxbrew/.linuxbrew/bin"),
559
+ )
560
+
561
+
562
+ def find_tool(name: str) -> str | None:
563
+ """`name` on PATH, or in the places an installer would have put it."""
564
+ found = shutil.which(name)
565
+ if found:
566
+ return found
567
+ suffixes = (".exe", ".cmd", "") if sys.platform == "win32" else ("",)
568
+ for directory in TOOL_DIRS:
569
+ for suffix in suffixes:
570
+ candidate = directory / f"{name}{suffix}"
571
+ if candidate.is_file():
572
+ return str(candidate)
573
+ return None
574
+
575
+
576
+ def _run_uninstaller(tool: str, root: Path | None = None) -> str:
577
+ """Hand the package back to whatever installed it.
578
+
579
+ And if that is not possible, take it off anyway.
580
+
581
+ uv and pipx keep a record of the tools they manage, so asking them is the
582
+ tidy way: it removes the environment *and* the entry. But the installer
583
+ fetches `uv` into `~/.local/bin` when the machine has none, and a shell
584
+ that ran `curl | sh` never read a profile, so `uv` is frequently installed
585
+ and simultaneously not on PATH — which is how a real uninstall reported
586
+ "No such file or directory: 'uv'" and left seventy megabytes behind.
587
+
588
+ So the tool is looked for where an installer would have put it, and if it
589
+ genuinely is not there the environment is deleted directly. That leaves uv
590
+ with an entry for something that is gone, which is worth saying out loud —
591
+ and is a great deal better than leaving the thing itself.
592
+ """
593
+ if tool == "pip":
594
+ command = [sys.executable, "-m", "pip", "uninstall", "-y", "comodor"]
595
+ else:
596
+ executable = find_tool(tool)
597
+ if executable is None:
598
+ if root is None:
599
+ raise RuntimeError(f"{tool} is not installed any more")
600
+ _rmtree(root)
601
+ return f"{root} (removed directly: {tool} is not on this machine)"
602
+ command = ([executable, "tool", "uninstall", "comodor"] if tool == "uv"
603
+ else [executable, "uninstall", "comodor"])
538
604
 
539
- def _run_uninstaller(tool: str) -> str:
540
- """Hand the package back to whatever installed it."""
541
- commands = {
542
- "uv": ["uv", "tool", "uninstall", "comodor"],
543
- "pipx": ["pipx", "uninstall", "comodor"],
544
- "pip": [sys.executable, "-m", "pip", "uninstall", "-y", "comodor"],
545
- }
546
- command = commands[tool]
547
605
  result = subprocess.run(command, capture_output=True, text=True, timeout=180)
548
606
  if result.returncode != 0:
607
+ if root is not None and root.exists():
608
+ _rmtree(root)
609
+ reason = (result.stderr or result.stdout).strip().splitlines()
610
+ return f"{root} (removed directly: {reason[-1][:80] if reason else tool})"
549
611
  raise RuntimeError((result.stderr or result.stdout).strip()[:200]
550
612
  or f"{tool} exited {result.returncode}")
551
- return " ".join(command[-3:] if tool != "pip" else command[1:])
613
+ return " ".join(command[1:])
552
614
 
553
615
 
554
616
  # --------------------------------------------------------------------------- #
@@ -22,6 +22,7 @@ Comodor and then checks it starts failed with "command not found".
22
22
  from __future__ import annotations
23
23
 
24
24
  import json
25
+ import os
25
26
  from pathlib import Path
26
27
 
27
28
  import pytest
@@ -211,7 +212,8 @@ def test_a_shared_bin_directory_keeps_its_place_on_path(tmp_path, monkeypatch):
211
212
  found = survey(config)
212
213
 
213
214
  assert not [item for item in found.items if item.kind == "path"]
214
- assert any("stays on your PATH" in note for note in found.notes)
215
+ # Named, so the reader can judge whether the line is worth keeping.
216
+ assert any("ruff" in note for note in found.notes)
215
217
 
216
218
 
217
219
  def test_a_directory_that_was_only_ours_gives_the_path_line_back(tmp_path,
@@ -305,6 +307,42 @@ def test_the_command_itself_goes_too(tmp_path, monkeypatch):
305
307
  assert not launcher.exists()
306
308
 
307
309
 
310
+ def test_a_tool_manager_off_the_path_is_still_found(tmp_path, monkeypatch):
311
+ """`curl | sh` bootstraps uv into ~/.local/bin and never reads a profile.
312
+
313
+ So the shell that installed Comodor frequently has uv installed and not on
314
+ PATH at the same time, and `shutil.which` says it is not there at all.
315
+ """
316
+ from comodor.uninstall import find_tool
317
+
318
+ tool = Path.home() / ".local" / "bin" / ("uv.exe" if os.name == "nt" else "uv")
319
+ tool.write_text("#!/bin/sh", encoding="utf-8")
320
+ monkeypatch.setattr("comodor.uninstall.TOOL_DIRS", (tool.parent,))
321
+ monkeypatch.setattr("shutil.which", lambda name: None)
322
+
323
+ assert find_tool("uv") == str(tool)
324
+ assert find_tool("pipx") is None
325
+
326
+
327
+ def test_a_missing_tool_manager_does_not_leave_the_environment_behind(tmp_path,
328
+ monkeypatch):
329
+ """Reported honestly and left on disk is still left on disk.
330
+
331
+ Deleting it directly leaves uv with an entry for something that is gone.
332
+ Seventy megabytes of environment is the worse of the two.
333
+ """
334
+ from comodor.uninstall import _run_uninstaller
335
+
336
+ environment = tmp_path / "uv" / "tools" / "comodor"
337
+ (environment / "bin").mkdir(parents=True)
338
+ monkeypatch.setattr("comodor.uninstall.find_tool", lambda name: None)
339
+
340
+ outcome = _run_uninstaller("uv", environment)
341
+
342
+ assert not environment.exists()
343
+ assert "removed directly" in outcome
344
+
345
+
308
346
  def test_a_removal_that_fails_is_reported_rather_than_raised(tmp_path):
309
347
  """One thing that will not go must not stop the rest from going."""
310
348
  def explode() -> str:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes