Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,23 @@ and the honest caveats — is in [`docs/eval.md`](docs/eval.md).

### ⚡ One line: install + wire into your agent

Download, install, and register CodeRAG as your coding agent's search tool — in a single command:
Download, install, and register CodeRAG as your coding agent's search tool — in a single command. [pipx](https://pipx.pypa.io) drops it into an isolated environment and puts `coderag` on your `PATH` — exactly what your agent needs to launch the server:

```bash
pip install "coderag[mcp] @ git+https://github.com/Neverdecel/CodeRAG" && coderag install
pipx install "coderag[mcp] @ git+https://github.com/Neverdecel/CodeRAG" && coderag install
```

This installs the engine **and** MCP server, then `coderag install` auto-detects **Claude Code**, **Hermes**, and **Codex** (or launches an interactive wizard) and wires CodeRAG in. Restart your agent and it searches a warm, pre-indexed workspace instead of grepping. Preview without writing any config: `coderag install --print`.

> No pipx yet? `sudo apt install pipx` (Debian/Ubuntu) or `brew install pipx` (macOS), then `pipx ensurepath` and open a new shell. Prefer plain `pip`? Install into a virtual environment instead (see **Quick start** below) — a global `pip install` is blocked on Debian/Ubuntu and other [PEP 668](https://peps.python.org/pep-0668/) systems.

## 🚀 Quick start

Work inside a virtual environment — a global `pip install` is blocked on Debian/Ubuntu and other [PEP 668](https://peps.python.org/pep-0668/) systems (`error: externally-managed-environment`):

```bash
python3 -m venv .venv && source .venv/bin/activate

pip install -e . # core engine (local embeddings included)
# optional extras:
pip install -e ".[server]" # HTTP/REST API
Expand All @@ -98,6 +104,11 @@ pip install -e ".[anthropic]" # Anthropic (Claude) LLM answers
pip install -e ".[all]" # everything above
```

> Wiring the MCP server into an agent from a venv? Run `coderag install` through the venv's
> binary (`.venv/bin/coderag install`) rather than after `activate`, so it records the venv's
> Python by absolute path — otherwise the bare `coderag` it writes won't resolve when your agent
> launches the server. Or use **pipx** (above), which keeps `coderag` on `PATH` for good.

Index a codebase and search it — no configuration, no API key:

```bash
Expand Down
65 changes: 59 additions & 6 deletions coderag/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,68 @@ class Plan:
# --- shared helpers -------------------------------------------------------------------


def _server_invocation(watched_dir: Optional[Path]) -> Tuple[str, List[str]]:
"""How an agent should launch the server: ``coderag mcp`` if on PATH, else ``python -m``.
def _within(path: str, prefix: str) -> bool:
"""Whether ``path`` *lives* inside ``prefix`` — its own location, symlink not followed.

The directory is resolved (to normalise ``..`` / symlinked tmp roots) but the final
entry is not, so a launcher symlink (e.g. a pipx shim) is judged by where it sits, not
by where it points.
"""
p = Path(path)
loc = p.parent.resolve() / p.name
try:
loc.relative_to(Path(prefix).resolve())
return True
except ValueError:
return False


def _env_console_script() -> Optional[str]:
"""Absolute path to the ``coderag`` console script in *this* interpreter's bin dir.

``None`` when there isn't one (e.g. running from a source checkout). Searching only
``sys.executable``'s directory ties the result to the Python we're running under and
handles the platform's script extension (``coderag.exe`` on Windows). The directory is
taken lexically — *not* via ``resolve()``, which would follow a venv's ``python``
symlink back to the base interpreter and miss the venv's own ``coderag`` script.
"""
bindir = Path(sys.executable).parent
return shutil.which("coderag", path=str(bindir))


Mirrors the README's launcher note (``README.md:210``). When ``watched_dir`` is given,
a ``--watched-dir`` arg is appended so a globally-configured agent indexes the right
tree regardless of where it was launched.
def _bare_coderag_is_durable() -> bool:
"""Whether a bare ``coderag`` will resolve for the *agent*, not just for us right now.

The agent launches the server from its own shell, so a bare command is only safe when
``coderag`` is on PATH via a location that survives without venv activation: a non-venv
(system / user) install, or a launcher outside the current venv prefix (e.g. a pipx
shim in ``~/.local/bin``). It is *not* safe when the only ``coderag`` on PATH is the
active venv's own ``bin/coderag``, which vanishes the moment the venv is deactivated —
the activation footgun this guards against.
"""
on_path = shutil.which("coderag")
if not on_path:
return False
in_venv = sys.prefix != sys.base_prefix
if not in_venv:
return True
return not _within(on_path, sys.prefix)


def _server_invocation(watched_dir: Optional[Path]) -> Tuple[str, List[str]]:
"""How an agent should launch the server.

The agent runs this from *its* environment, not ours, so a bare ``coderag`` is written
only when it will resolve on a PATH the agent also has (:func:`_bare_coderag_is_durable`).
Otherwise we pin an absolute target — this environment's ``coderag`` console script, or
``<python> -m coderag.surfaces.cli`` — so the server still launches when the install-time
venv isn't active. When ``watched_dir`` is given, ``--watched-dir`` is appended so a
globally-configured agent indexes the right tree regardless of where it was launched.
"""
if shutil.which("coderag"):
if _bare_coderag_is_durable():
command, args = "coderag", ["mcp"]
elif (script := _env_console_script()) is not None:
command, args = script, ["mcp"]
else:
command, args = sys.executable, ["-m", "coderag.surfaces.cli", "mcp"]
if watched_dir is not None:
Expand Down
70 changes: 70 additions & 0 deletions tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,73 @@ def test_wizard_collects_choices(home, monkeypatch):
assert len(plans) == 1
assert plans[0].target == "hermes"
assert plans[0].tools == inst.DEFAULT_TOOLS


# --- launcher resolution (the venv-activation footgun) --------------------------------
#
# An agent launches the server from its own shell, so the command written into its config
# must resolve there — not merely wherever `coderag install` happened to run.


def _patch_venv(monkeypatch, *, prefix: str, base_prefix: str) -> None:
monkeypatch.setattr(inst.sys, "prefix", prefix)
monkeypatch.setattr(inst.sys, "base_prefix", base_prefix)


def test_invocation_bare_for_system_install(monkeypatch):
# Not in a venv and `coderag` on PATH → a durable bare command.
_patch_venv(monkeypatch, prefix="/usr", base_prefix="/usr")
monkeypatch.setattr(inst.shutil, "which", lambda *_a, **_k: "/usr/bin/coderag")
assert inst._server_invocation(None) == ("coderag", ["mcp"])


def test_invocation_absolute_inside_activated_venv(monkeypatch, tmp_path):
# The only `coderag` on PATH is the active venv's own bin/coderag, which disappears
# once deactivated → pin that script by absolute path instead of a bare command.
bindir = tmp_path / ".venv" / "bin"
bindir.mkdir(parents=True)
script = bindir / "coderag"
script.write_text("#!stub\n")
_patch_venv(monkeypatch, prefix=str(tmp_path / ".venv"), base_prefix="/usr")
monkeypatch.setattr(inst.sys, "executable", str(bindir / "python"))
monkeypatch.setattr(inst.shutil, "which", lambda *_a, **_k: str(script))
assert inst._server_invocation(None) == (str(script), ["mcp"])


def test_invocation_absolute_when_venv_not_activated(monkeypatch, tmp_path):
# Ran via `.venv/bin/coderag install` without activating: `coderag` is not on PATH,
# but the env's console script is found in sys.executable's bin dir.
bindir = tmp_path / ".venv" / "bin"
bindir.mkdir(parents=True)
script = bindir / "coderag"
script.write_text("#!stub\n")
_patch_venv(monkeypatch, prefix=str(tmp_path / ".venv"), base_prefix="/usr")
monkeypatch.setattr(inst.sys, "executable", str(bindir / "python"))
monkeypatch.setattr(
inst.shutil, "which", lambda _n, path=None: str(script) if path else None
)
assert inst._server_invocation(None) == (str(script), ["mcp"])


def test_invocation_bare_for_pipx_shim_outside_venv(monkeypatch, tmp_path):
# pipx: in a venv, but the PATH launcher lives outside the venv prefix (~/.local/bin),
# so a bare `coderag` resolves without activation.
(tmp_path / "venvs" / "coderag" / "bin").mkdir(parents=True)
shim = tmp_path / ".local" / "bin" / "coderag"
shim.parent.mkdir(parents=True)
shim.write_text("#!stub\n")
_patch_venv(
monkeypatch, prefix=str(tmp_path / "venvs" / "coderag"), base_prefix="/usr"
)
monkeypatch.setattr(inst.shutil, "which", lambda *_a, **_k: str(shim))
assert inst._server_invocation(None) == ("coderag", ["mcp"])


def test_invocation_module_fallback_when_no_script(monkeypatch):
# Source checkout, no installed console script anywhere → run the module by interpreter.
_patch_venv(monkeypatch, prefix="/usr", base_prefix="/usr")
monkeypatch.setattr(inst.sys, "executable", "/usr/bin/python3")
monkeypatch.setattr(inst.shutil, "which", lambda *_a, **_k: None)
cmd, args = inst._server_invocation(None)
assert cmd == "/usr/bin/python3"
assert args == ["-m", "coderag.surfaces.cli", "mcp"]
Loading