Metadata-Version: 2.4
Name: juan-contreras
Version: 0.3.0
Summary: juan — self-hosted GitHub pull request reviewer powered by an LLM.
Project-URL: Homepage, https://github.com/karimpichara/juan-contreras
Project-URL: Repository, https://github.com/karimpichara/juan-contreras
Project-URL: Issues, https://github.com/karimpichara/juan-contreras/issues
Project-URL: Changelog, https://github.com/karimpichara/juan-contreras/blob/main/CHANGELOG.md
Author-email: Karim Pichara <55170215+karimpichara@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: anthropic,cli,code-review,deepseek,github,llm,openai,pull-request,self-hosted
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Version Control :: Git
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: anthropic>=0.105.2
Requires-Dist: click>=8.4.1
Requires-Dist: fastapi>=0.136.3
Requires-Dist: httpx>=0.27.0
Requires-Dist: openai>=2.41.0
Requires-Dist: prometheus-client>=0.20.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: rich>=13.7.0
Requires-Dist: slowapi>=0.1.9
Requires-Dist: tenacity>=8.2.0
Requires-Dist: uvicorn[standard]>=0.49.0
Provides-Extra: server
Requires-Dist: fastapi>=0.136.3; extra == 'server'
Requires-Dist: prometheus-client>=0.20.0; extra == 'server'
Requires-Dist: slowapi>=0.1.9; extra == 'server'
Requires-Dist: uvicorn[standard]>=0.49.0; extra == 'server'
Description-Content-Type: text/markdown

# juan-contreras

A self-hosted pull request reviewer that connects your local machine or Raspberry Pi to GitHub and uses an LLM to review code. It is designed to be a lightweight, private alternative to cloud-hosted code review bots.

There are two ways to use it:

- **Part 1 - Server mode**: run the reviewer as an HTTP server on an always-on host (Raspberry Pi, VPS, pythonanywhere). Friends can submit review requests to your server without ever seeing your LLM API key.
- **Part 2 - CLI mode**: run the reviewer directly on your own computer when you have your API key available.

Both modes use the same review engine and prompt.

---

## Requirements

- Python 3.13 or newer
- A GitHub personal access token with `repo` read access (classic) or `Contents` + `Pull requests` fine-grained permissions
- An API key for one of the supported LLM providers (OpenAI, Anthropic, DeepSeek, GLM) — or the `codex` CLI for personal use

---

## Installation

Pick the path that matches what you want to do.

### Option 1 — `pipx` (CLI users, recommended)

```bash
pipx install juan-contreras       # or: pipx install 'juan-contreras[server]'
juan --version
juan config init                  # writes ./.env scaffold
juan doctor                       # validate env without spending tokens
```

`pipx` puts `juan` on your PATH in an isolated venv. The plain install gives you everything needed to run `juan review` / `juan remote-review` / `juan history` / `juan doctor` / `juan config`. The `[server]` extra is only needed if you also want to run `juan serve` from the same install.

### Option 2 — `uv tool install`

```bash
uv tool install juan-contreras    # or: uv tool install 'juan-contreras[server]'
juan --version
```

Same shape as `pipx` but uses `uv`'s tool sandbox. Run `uv tool update juan-contreras` to upgrade.

### Option 3 — `pip` into a venv

```bash
python -m venv .venv && source .venv/bin/activate
pip install 'juan-contreras[server]'
juan --version
```

Use this when you want to run `juan` alongside other Python tooling in the same venv.

### Option 4 — clone for development

If you want to **modify** juan (or contribute), clone the repo:

```bash
git clone https://github.com/karimpichara/juan-contreras.git
cd juan-contreras
make setup-dev    # uv sync + pre-commit hooks + scaffold .env
uv run juan --help
```

`uv sync` reads `pyproject.toml`, creates `.venv/`, and installs dependencies. You don't need to activate the venv manually — prefix commands with `uv run` or use Makefile targets (`make help`). See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full dev loop.

### Configure your environment

After installing, scaffold a `.env` file and fill in your credentials:

```bash
juan config init       # writes ./.env from the bundled template (refuses to overwrite)
$EDITOR .env
juan doctor            # validates GITHUB_TOKEN, LLM_PROVIDER, API key, etc.
```

Minimum `.env`:

```
GITHUB_TOKEN=ghp_your_token_here
LLM_PROVIDER=openai
OPENAI_API_KEY=sk-your_key_here
```

`juan` searches for `.env` in `./` first (walking up from cwd), then `$XDG_CONFIG_HOME/juan/.env` (or `~/.config/juan/.env`). Run `juan config path` to see exactly what's loaded.

For the full CLI reference — every flag, env var, exit code, JSON output schema, provider notes — see [`docs/CLI.md`](docs/CLI.md).

---

## Part 1 - Server mode (Raspberry Pi or always-on host)

> **Setting up a fresh Raspberry Pi from scratch?** Follow [`docs/RASPBERRY_PI_SETUP.md`](docs/RASPBERRY_PI_SETUP.md) end-to-end. It covers the headless OS install, SSH hardening, `uv`, the systemd or Docker runtime, and a verification curl. The sections below are the reference docs — the Pi guide stitches them together.

### Configuration

In addition to the GitHub and LLM settings, set a strong server API key. This is the only secret your friends need:

```
SERVER_API_KEY=change_me_to_a_long_random_secret
SERVER_HOST=0.0.0.0
SERVER_PORT=8000

# Optional: slowapi rate limit (per X-API-Key, fallback per-IP).
# Format: "<count>/<period>" where period is second|minute|hour|day.
RATE_LIMIT=30/minute
```

### Starting the server

```bash
uv run juan serve
```

The `serve` command honours `SERVER_HOST` and `SERVER_PORT` from `.env` (and falls back to `0.0.0.0:8000`). You can also pass `--host` / `--port` explicitly, or run uvicorn directly: `uv run uvicorn server.app:app --host 0.0.0.0 --port 8000`.

For production use, run it as a background service. Example systemd unit at `/etc/systemd/system/juan.service`:

```ini
[Unit]
Description=PR Reviewer
After=network.target

[Service]
User=pi
WorkingDirectory=/home/pi/juan-contreras
EnvironmentFile=/home/pi/juan-contreras/.env
# `uv run` resolves the project venv automatically. If `uv` is not on the
# system PATH for the service user, use the absolute path (e.g. /home/pi/.local/bin/uv).
ExecStart=/usr/bin/env uv run juan serve
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now juan
sudo journalctl -u juan -f      # tail logs
```

### Exposing the server to the internet

Your Raspberry Pi is probably behind a home router. Choose one of the options below to make it reachable from anywhere.

#### Option A - ngrok (easiest, free tier available)

```bash
# Install: https://ngrok.com/download
ngrok http 8000
# ngrok prints a public URL like https://abc123.ngrok-free.app
```

Share that URL and your SERVER_API_KEY with friends.

#### Option B - Cloudflare Tunnel (free, stable URL)

```bash
# Install cloudflared: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/
cloudflared tunnel --url http://localhost:8000
# Cloudflare prints a *.trycloudflare.com URL
```

For a permanent subdomain, configure a named tunnel via the Cloudflare dashboard.

#### Option C - pythonanywhere

pythonanywhere supports always-on tasks and Flask/FastAPI apps. You can deploy this project directly:

1. Upload the repository to pythonanywhere via `git clone` in a Bash console.
2. Install [uv](https://docs.astral.sh/uv/) and run `uv sync` (or `make setup`).
3. Configure a web app pointing to `server/app.py` using the ASGI worker.
4. Set environment variables in the pythonanywhere web app configuration.

See: https://help.pythonanywhere.com/pages/DeployingFastAPI

#### Option D - router port forwarding

Forward port 8000 on your router to the Raspberry Pi's local IP. Your public IP is then the address. Use a dynamic DNS service (e.g. DuckDNS, No-IP) if your ISP assigns a dynamic IP.

#### Option E - Docker / docker-compose (recommended for Pi)

The repo ships a multi-stage `Dockerfile` and `docker-compose.yml` (multi-arch, builds on a Pi 4/5 directly):

```bash
cp .env.example .env       # fill in the secrets
docker compose up -d --build
docker compose logs -f
```

Compose binds the container's port to `127.0.0.1:8000` only — put a reverse proxy (Caddy / Traefik / nginx) in front for HTTPS and public exposure. The SQLite database lives in `./data/` on the host and persists across restarts.

`/metrics` is exposed inside the container with no auth; in this default config it's only reachable from the host (loopback bind).

### API reference

**Health checks and history**

```
GET /live    # liveness: 200 if the process is up
GET /ready   # readiness: 200 if GITHUB_TOKEN and the LLM key both work
GET /health  # alias of /live for backwards compatibility
GET /metrics # Prometheus scrape endpoint (bind to localhost in production)
GET /reviews # paginated review history (auth-protected)
```

**CLI niceties**

- `uv run juan review --repo owner/repo --pr 42 --post` runs a review locally and pushes it back to the PR via GitHub's Review API. Comments mapped to a diff position are posted inline; the rest are folded into the review summary.
- `uv run juan history --repo owner/repo --pr 42` lists the persisted reviews from the local SQLite store. Pass `--server`/`--server-key` to query a remote juan instead.
- Server-side, set `POST_REVIEW_TO_PR=true` in `.env` to make every `/review` call also push back to the PR (off by default).

`/ready` performs a lightweight authenticated probe against GitHub and the configured LLM provider. Results are cached for 30 seconds. A 503 response includes per-probe diagnostics:

```json
{
  "ok": false,
  "probes": [
    {"name": "github", "ok": false, "detail": "GITHUB_TOKEN rejected (401)."},
    {"name": "openai", "ok": true,  "detail": "OK"}
  ]
}
```

All endpoints accept and echo an optional `X-Request-ID` header so you can correlate client logs with server logs. Server logs are emitted as one JSON object per line on stderr (visible via `journalctl -u juan -f`).

**Request a review**

```
POST /review
Headers: X-API-Key: <your SERVER_API_KEY>
Content-Type: application/json

{
  "repo": "owner/repo",
  "pr": 42,
  "fresh": false
}
```

Set `"fresh": true` to ignore stored history and always run a full new review.

Response:

```json
{
  "pr_number": 42,
  "pr_title": "Add user authentication",
  "pr_author": "alice",
  "repo": "owner/repo",
  "overall_assessment": "needs_changes",
  "summary": "...",
  "categories_found": ["security", "missing_test"],
  "comments": [
    {
      "file": "auth/views.py",
      "line": 31,
      "category": "security",
      "severity": "high",
      "comment": "Password compared with == instead of a constant-time function."
    }
  ]
}
```

**Example with curl**

```bash
curl -s -X POST https://abc123.ngrok-free.app/review \
  -H "X-API-Key: change_me_to_a_long_random_secret" \
  -H "Content-Type: application/json" \
  -d '{"repo": "alice/myapp", "pr": 42}' | python -m json.tool
```

---

## Part 2 - CLI mode (your own machine, your own API key)

> **Convention:** the examples below show `uv run juan ...` because they assume a repo checkout (`git clone`). If you installed `juan` from PyPI via `pipx` / `uv tool install` / `pip`, drop the `uv run` prefix — the `juan` console script is on your PATH directly.

### Environment setup

Quickest path:

```bash
juan config init       # scaffold ./.env
$EDITOR .env           # fill in GITHUB_TOKEN, LLM_PROVIDER, API key
juan doctor            # validate before your first review
```

Minimum `.env`:

```
GITHUB_TOKEN=ghp_your_token_here
LLM_PROVIDER=openai
OPENAI_API_KEY=sk-your_key_here
```

### Commands

#### Review a PR locally

```bash
uv run juan review --repo owner/repo --pr 42
```

Override the model or provider:

```bash
uv run juan review --repo owner/repo --pr 42 --provider anthropic --model claude-3-haiku-20240307
```

Get JSON output (useful for scripting):

```bash
uv run juan review --repo owner/repo --pr 42 --output json
```

The JSON payload includes `llm_provider`, `llm_model`, and `llm_usage` (token counts from the provider). Text mode prints a one-line token summary after the review. Codex reports `llm_usage: null` (subprocess path has no usage API).

Set `LLM_MODEL` in `.env` to override the provider default without passing `--model` (ignored for `codex`).

Inspect prompts without calling the LLM (GitHub fetch only):

```bash
uv run juan review --repo owner/repo --pr 42 --dry-run
uv run juan review --repo owner/repo --pr 42 --dry-run -v   # prompt previews on stderr
```

Verbose mode (`juan -v review ...` or `juan review -v ...`): more logging, full traceback on errors, and truncated prompt previews on stderr after a review or dry run.

```bash
uv run juan --version
```

Pass credentials directly without a .env file:

```bash
uv run juan review \
  --repo owner/repo \
  --pr 42 \
  --github-token ghp_xxx \
  --api-key sk-xxx
```

#### Review a PR via the remote server

```bash
uv run juan remote-review \
  --server https://abc123.ngrok-free.app \
  --server-key change_me_to_a_long_random_secret \
  --repo owner/repo \
  --pr 42
```

Store server details in environment variables to avoid repeating them:

```bash
export REVIEWER_SERVER=https://abc123.ngrok-free.app
export REVIEWER_SERVER_KEY=change_me_to_a_long_random_secret

uv run juan remote-review --repo owner/repo --pr 42
```

### Help

```bash
uv run juan --help
uv run juan review --help
uv run juan remote-review --help
uv run juan serve --help
```

CLI reference: [docs/CLI.md](docs/CLI.md).

---

## Review categories

| Category | Description |
|---|---|
| bug | Logic errors, null handling, off-by-one, incorrect conditions |
| security | Injection, XSS, CSRF, hardcoded secrets, insecure auth |
| performance | N+1 queries, unnecessary loops, memory leaks |
| race_condition | Shared mutable state, missing locks, TOCTOU |
| missing_test | New logic with no test coverage |
| nitpick | Naming, formatting, minor style issues |
| design | SOLID violations, poor abstractions, tight coupling |

Severity levels: `high`, `medium`, `low`.

---

## Supported LLM providers

Set `LLM_PROVIDER` in `.env` (see `.env.example` for notes on cost and Pi use).

| Provider | Default model | API key env var |
|---|---|---|
| openai | gpt-4o | `OPENAI_API_KEY` |
| anthropic | claude-3-5-sonnet-20241022 | `ANTHROPIC_API_KEY` |
| deepseek | deepseek-v4-flash | `DEEPSEEK_API_KEY` |
| glm | glm-4.5-air | `GLM_API_KEY` |
| codex | (ChatGPT subscription) | none — local `codex login` |

`codex` shells out to the Codex CLI and is for **personal** use only (not multi-user
server exposure). See `docs/RASPBERRY_PI_SETUP.md`.

Set `LLM_MODEL` to override the default for your chosen provider.

---

## Development

A [Makefile](Makefile) wraps the usual `uv` commands — run `make help` for the list.

**First time (dev machine):**

```bash
make setup-dev    # uv sync + pre-commit hooks; creates .env if missing
```

**Before pushing (same gates as CI):**

```bash
make check        # ruff lint, format check, mypy, pytest
make fmt          # auto-format when check fails on format
```

Install hooks so you cannot forget (blocks `git push` on failure):

```bash
make install-hooks   # once per clone; also part of make setup-dev
```

Emergency bypass only: `git push --no-verify`.

Equivalent raw commands:

```bash
uv sync --all-groups
uv run pre-commit install
uv run pytest
uv run ruff check server reviewer tests
uv run ruff format --check server reviewer tests
uv run mypy server reviewer
```

CI runs the same gates on Python 3.13 (`.github/workflows/ci.yml`).

**On a Raspberry Pi** (runtime only): `make setup`, edit `.env`, then `make serve` or use Docker via `make docker-up`. See [`docs/RASPBERRY_PI_SETUP.md`](docs/RASPBERRY_PI_SETUP.md).

---

## Deployment scenarios

Three layered guides, in increasing order of trust:

1. [docs/RASPBERRY_PI_SETUP.md](docs/RASPBERRY_PI_SETUP.md) — fresh Pi from a flashed SD card to a working `POST /review`. Headless OS install, SSH hardening, `uv`, systemd vs Docker.
2. [docs/DEPLOYMENT_SELFHOST.md](docs/DEPLOYMENT_SELFHOST.md) — "anyone can self-host this safely". HTTPS via Caddy/Traefik, log retention, backups, locked-down GitHub token, blocked `/metrics`, secret rotation playbook.
3. [docs/DEPLOYMENT_TEAM.md](docs/DEPLOYMENT_TEAM.md) — "small team / company". Per-user key strategy, audit-log shipping, IP allowlist via proxy, Prometheus cost alerts, on-call hand-off.

Read them in order; each one assumes the previous is in place.

---

## Re-review (follow-up passes)

When SQLite history exists for a PR (local CLI or server `JUAN_DB`), a
second review **includes the last stored review** in the prompt. The model returns:

- `prior_findings` — each earlier comment with `status`: `fixed`, `partially_fixed`,
  `not_addressed`, or `obsolete`
- `comments` — new issues only (or still-open high/medium items)

If the PR **head commit is unchanged** since the last review, the server skips the
LLM call and returns `{"skipped": true, ...}` (daily budget is refunded). Force a
full new review with:

- CLI: `uv run juan review --repo owner/repo --pr 42 --fresh`
- Server: `"fresh": true` in the `POST /review` JSON body

Env knobs (server): `REVIEW_FOLLOW_UP`, `REVIEW_SKIP_UNCHANGED` (both default to
`true`). The bot still does **not** read comments from GitHub itself — only from
local history. Posting to GitHub still creates a **new** review each time.

---

## Limitations

- Prior context comes from **SQLite history**, not from GitHub review threads.
- No webhook automation yet — reviews are on-demand (CLI or HTTP). See
  [TODO.md](TODO.md) for planned work (webhooks, GitHub-thread sync, and more).

---

## Security notes

- The server **refuses to start** if `SERVER_API_KEY`, `GITHUB_TOKEN`, or the matching LLM key is missing or set to a known placeholder. All problems are reported in one go.
- The `SERVER_API_KEY` is the only credential exposed to the outside world. Use a long, random string (e.g. `openssl rand -hex 32`).
- API key comparison is constant-time (`secrets.compare_digest`) — bad keys can't be brute-forced via timing.
- Your GitHub token and LLM API key never leave the server host.
- The `/review` endpoint is rate-limited per `X-API-Key` (or per IP if no key is sent). Default `30/minute`; override with `RATE_LIMIT`. Bad-key requests count too, so credential-stuffing is bounded.
- A bounded semaphore caps simultaneous reviews (`MAX_CONCURRENT_REVIEWS`, default 2). Excess returns `503` with `Retry-After: 5`.
- POST bodies on `/review` are capped at `MAX_REQUEST_BODY_BYTES` (default 4 KiB).
- The GitHub `/files` pagination is capped at `MAX_PR_FILE_PAGES` (default 50, ~5000 files) to bound a worst-case PR's memory and time.
- LLM calls have a 120s client-side timeout so a stuck upstream cannot hold a worker indefinitely.
- Server errors are sanitized: upstream messages from GitHub / OpenAI / Anthropic are logged on the host but **not** returned to the client. Tail with `journalctl -u juan -f` to debug.
- Consider restricting access further via IP allowlisting on your router or using a VPN (see TODO.md).
- Keep your `.env` file out of version control (it is listed in `.gitignore`).