Metadata-Version: 2.5
Name: openpurr
Version: 0.8.7
Summary: CLI tool to generate PR titles, descriptions, and post-review change summaries using a local or cloud LLM.
Project-URL: Homepage, https://github.com/ilypopv/openpurr
Project-URL: Repository, https://github.com/ilypopv/openpurr
Author-email: Ilya Popov <ilypopv@gmail.com>
License: MIT
Requires-Python: >=3.11
Requires-Dist: anthropic>=0.122.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: openai>=1.0.0
Requires-Dist: questionary>=2.0.0
Requires-Dist: rich>=13.7.0
Requires-Dist: typer>=0.12.0
Description-Content-Type: text/markdown

# OpenPullRequest

<img src="https://raw.githubusercontent.com/ilypopv/openpurr/main/imgs/openpurr.png" alt="logo" width="180" align="left">

CLI tool that generates PR titles, descriptions, and post-review change summaries using a local or cloud LLM. Supports Ollama, OpenAI, Anthropic, Google Gemini, OpenRouter, DeepSeek, llama.cpp, and MLX.

[![CI](https://img.shields.io/github/actions/workflow/status/ilypopv/openpurr/ci.yml?style=flat-square&label=CI)](https://github.com/ilypopv/openpurr/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE)

<br clear="left" />

Built primarily for local-first use — pair it with [Ollama](https://ollama.com) and a Gemma model (Google's Gemma family is currently the best fit for this kind of task at a size you can run locally) for a free, fully offline workflow. On Apple Silicon Macs, prefer the `-mlx` tagged variant of your chosen model (e.g. `gemma4:26b-mlx`) — it runs on Apple's MLX framework instead of plain GGUF and is noticeably faster on that hardware. Don't want to run anything locally? Google's Gemini API has a generous free tier and is a great low-friction cloud option.

## Installation

**Recommended**

```bash
uv tool install openpurr
```

Or

```bash
pipx install openpurr
```

On first run, the setup wizard creates `~/.openpurr` automatically. You can also run it any time:

```bash
opo setup
```

## Requirements

- Python 3.11+
- `git` on PATH
- One of: [Ollama](https://ollama.com) running locally, or an API key for a cloud provider

## Usage

```bash
# Generate a PR title + description from the diff against main
opo

# Diff against a different base branch
opo --base develop

# Summarise changes made since the last N commits (for reviewers)
opo review
opo review --commits 3
```

## Example output

Running `opo` against a diff produces a title and description like this — generated
with the default system prompt, no custom overrides. This is [PR #11](https://github.com/ilypopv/openpurr/pull/11) from this very repo — in fact, every PR in openpurr's history was generated by openpurr itself:

> **feat(config): add support for custom system prompt overrides**
>
> ## 📝 Summary
> Introduced a mechanism to override default system prompts via the configuration file. Users can now define custom `INIT PROMPT:` and `REVIEW PROMPT:` sections to tailor LLM behavior for PR generation and reviews.
>
> ## 🛠 Type of Change
> - New feature
>
> ## 🔍 Key Changes
> - Implemented a new configuration parsing logic that uses a `---` delimiter to separate key-value pairs from free-text prompt sections.
> - Added support for `INIT PROMPT:` and `REVIEW PROMPT:` headers to override default system prompts.
> - Updated `Config` class to load and expose custom prompt overrides.
> - Updated `run_init` and `run_review` to prioritize custom prompts over built-in defaults.
> - Enhanced `config_describe` to inform users of active prompt overrides.
> - Updated `README.md` with new installation instructions and documentation for custom prompts.
>
> ## 🧪 How Has This Been Tested?
> - Extensive unit tests in `tests/test_config.py` covering delimiter splitting, prompt parsing, and configuration preservation.
> - Functional tests in `tests/test_pr.py` verifying that custom prompts are correctly passed to the LLM provider.

Want a different tone or format? See [Custom system prompts](#custom-system-prompts) below.

## Configuration

Settings are stored in `~/.openpurr` as flat `OPO_KEY=value` lines — no sections, no quoting:

```text
OPO_PROVIDER=ollama
OPO_MODEL=gemma4:26b
OPO_API_KEY=
OPO_HOST=http://localhost:11434
OPO_TEMPERATURE=0.0
OPO_KEEP_ALIVE=5m
OPO_BASE=main
OPO_LANGUAGE=en
```

Use the `config` subcommand to inspect and update values without editing the file directly.

```bash
# Show all keys with descriptions and current values
opo config describe

# Read a single value
opo config get model

# Update a value
opo config set provider ollama
opo config set model gemma4:26b
opo config set api_key sk-...
opo config set keep_alive 0s
```

### Custom system prompts

The prompts `opo` sends to the LLM aren't fixed — everything after a lone `---` line in
`~/.openpurr` is free text, not `OPO_KEY=value` config, so it can hold Markdown, headers,
`=` signs, anything. Add an `INIT PROMPT:` section to override the prompt behind `opo`
(title + description) and/or a `REVIEW PROMPT:` section to override the one behind
`opo review`. Either section is optional; whichever is missing keeps the built-in default.

```text
OPO_PROVIDER=ollama
OPO_MODEL=gemma4:26b
OPO_API_KEY=
OPO_HOST=http://localhost:11434
OPO_TEMPERATURE=0.0
OPO_KEEP_ALIVE=5m
OPO_BASE=main
OPO_LANGUAGE=en

---
INIT PROMPT:
You are a Senior Principal Engineer. Write a terse PR title and description in
Conventional Commits style. No emoji, no checklists.

REVIEW PROMPT:
Summarize what changed since the last review in one short paragraph.
```

`opo config set`/`opo config describe` only ever touch the `OPO_KEY=value` part above the
`---` line — the prompt section is left exactly as you wrote it, and `opo config describe`
tells you which of `init`/`review` are currently overridden. Note that `OPO_LANGUAGE` (see
below) only affects the *built-in* prompts — once you write your own `INIT PROMPT:`/`REVIEW PROMPT:`,
you're in full control, so specify the language directly in the prompt text if you need one.

### Configuration keys

| Key | Env var | Default | Description |
| --- | ------- | ------- | ----------- |
| `provider` | `OPO_PROVIDER` | `ollama` | `ollama` · `openai` · `anthropic` · `gemini` · `openrouter` · `deepseek` · `llamacpp` · `mlx` |
| `model` | `OPO_MODEL` | _(empty)_ | Model name — set via `opo setup` or `opo config set model <name>` |
| `api_key` | `OPO_API_KEY` | _(empty)_ | API key for cloud providers |
| `host` | `OPO_HOST` | `http://localhost:11434` | Base URL — Ollama default; set to a custom endpoint when needed |
| `temperature` | `OPO_TEMPERATURE` | `0.0` | Sampling temperature (`0.0` = deterministic) |
| `keep_alive` | `OPO_KEEP_ALIVE` | `5m` | Ollama VRAM keep-alive (`0s` = unload immediately, `5m` = keep warm) |
| `base` | `OPO_BASE` | `main` | Default base branch to diff against |
| `language` | `OPO_LANGUAGE` | `en` | Output language for generated PR text (ISO 639-1 code, e.g. `es`, `fr`, `de`, `ja`, `zh`) |

There's no hardcoded default model: `opo setup` always has you pick one (from a live-fetched list, or typed manually), and `opo`/`opo review` will error out with a pointer back to `opo setup` if `model` is ever left blank (e.g. after hand-editing the file).

### Example: switch to Google Gemini (free tier)

```bash
opo config set provider gemini
opo config set model <your-chosen-model>
opo config set api_key <your-gemini-api-key>
```

Model names aren't hardcoded anywhere in this README on purpose — providers retire and rename models constantly. Run `opo setup` or `opo models --provider gemini` to pick from what's actually available right now.

### Example: unload Ollama from VRAM after each request

```bash
opo config set keep_alive 0s
```

### Example: generate PR text in another language

```bash
opo config set language es
```

`opo setup` also asks for this up front, with arrow-key selection from common languages
plus a custom-code fallback. This only affects the built-in prompts — see
[Custom system prompts](#custom-system-prompts) above.

## Models

List available models for the current or a specified provider:

```bash
opo models
opo models --provider anthropic
```

Model lists are always fetched live from the provider — Ollama's `/api/tags`, OpenAI/Anthropic/Gemini/OpenRouter/DeepSeek/llama.cpp/MLX's `models.list()` (or public models endpoint) — never a hardcoded/curated list baked into the tool. If the fetch fails (offline, bad key, local server not running) the command prints an empty result instead of erroring; check connectivity/credentials, or pull a model directly with `ollama pull <model>`.
