Raul CariniFull Stack Developer

Lazypr - AI CLI Tool

October 3, 2025 (8 hours ago)
lazypr

Tools are meant to remove friction. I built lazypr because writing pull request titles and descriptions is a repetitive, low-value task that nonetheless has outsized impact on code review quality. This article is less of a usage guide and more of a story: why I built it, the architectural choices I made, and how lessons from my first package shaped the project's design.

This is a narrative-focused piece: architecture, trade-offs, and lessons learned. If you want the quick installation and usage notes, see the README - this article explains the "why" and the "how".

The problem I wanted to solve

Pull requests are the primary place we communicate intent, risks, and testing expectations to reviewers. Yet, when you're shipping features or fixing bugs, writing a concise, clear PR body is often deferred. Teams either paste raw commits, write a short one-line summary, or spend time polishing content at the last minute.

I wanted a tool that would read the repository's source of truth - the commits - and that would respect repository conventions such as PR templates. The tool should produce a short, actionable PR title and a reviewer-friendly markdown body, and it should fit into a zero-friction developer workflow: run it from the terminal, copy the result to the clipboard, and you're done. That goal shaped every design decision.

High level architecture

I approached lazypr as a small, composable CLI built from clear layers. The Git layer gathers the minimal, canonical input - the commits on the feature branch that are not in the base branch. A Template layer detects and preserves any PR templates in the repo so the generated output fits your team's structure. The Prompt & AI layer transforms commits and templates into a structured prompt and calls the Groq AI models to synthesize a title and markdown body.

The UX layer provides a small interactive terminal UI for selection and clipboard copying. Finally, a Resilience & Config layer offers sensible defaults and small knobs (timeouts, retries, locale, model) so the tool behaves well in diverse environments. This separation makes the code easier to reason about and test: the git collector doesn't care about prompts; the prompt builder doesn't know about clipboard quirks.

Why this layering matters

If the tool were a single script, each new feature - template support, locale, different models - would increase the blast radius of changes. By keeping boundaries small and designing simple interfaces between parts, the CLI stays maintainable and easier to evolve.

Important design decisions

I made several key trade-offs intentionally. First, I kept the surface area minimal: the CLI does one thing well - generate polished PR content - and it doesn't try to open PRs or become a full release tool by default; those integrations can be added later as opt-in features. For model defaults, I selected GPT-OSS models optimized for structured outputs because they strike a good balance between cost, latency, and quality, while leaving the model configurable so teams can trade off cost versus fidelity per repo. I took a repository-first approach: commits are the single source of truth for intent, and parsing commit messages in chronological order (oldest to newest) produces the narrative the model needs to explain changes.

Template fidelity is important: PR templates are sacred in many orgs, so the tool respects structure, checkboxes, and headings so generated content fits the expected workflow. Finally, I cared about UX ergonomics: an interactive prompt that copies to the clipboard is a small but powerful adoption accelerator, and when clipboard access fails (for example in CI or remote shells), the tool gracefully falls back to printing content.

Implementation highlights

This section sketches the concrete building blocks I implemented and explains what exists and why. The Git collector is a small module that runs deterministic git commands to obtain base branch existence and commits, the commits unique to HEAD, and commit messages along with optional co-author metadata. Determinism is important - the same commit history should yield the same prompt.

The Prompt builder is a focused module that takes commit messages, an optional template, and a locale and produces a structured prompt while enforcing constraints like title length and requesting a markdown description; I designed it so the model can produce a JSON-like structured response where possible, which makes downstream rendering simple. The AI client adapter is intentionally thin - it speaks to the Vercel AI SDK with @ai-sdk/groq, handles retries and timeouts, and maps provider errors into clear messages for users; isolating the SDK details here keeps the rest of the codebase stable as provider APIs evolve.

The Template resolver scans conventional locations (such as .github and docs) and exposes either a single template or a list for interactive selection; when a template is used, the prompt includes template sections as context. The CLI and interaction layer is a small wrapper around a prompt library for terminal UIs: choose a template, view the generated title and body, copy to the clipboard, and exit. The alias lzp is just a tiny convenience. For testing and CI, I added unit tests for prompt construction and template parsing and end-to-end coverage that uses a small repo fixture to validate commit collection logic; tests run on GitHub Actions and verify the CLI output format.

Why Node, Bun, and modern runtimes

I chose Node.js (>= 20) because modern features like ESM-only packages and native clipboard support make it a pragmatic distribution target with broad availability. Bun is useful during development for faster iteration when running TypeScript directly and for small convenience scripts, while the distributed package remains fully usable with Node. Using ESM and TypeScript improves packaging, bundling, and type safety, which helps with code ownership across modules. These choices sped up the developer experience while keeping runtime requirements minimal for consumers.

Prompt engineering & structured outputs

One of the trickiest parts was making the model reliably produce concise titles and structured markdown. I iterated on prompts with several constraints in mind: supply commits in chronological order (oldest to newest) so the model can see intent and progression; explicitly ask for a one-line title limited to a fixed number of characters; request a markdown body with sections such as Summary, Why, and Notes and instruct the model to respect PR template placeholders; and favor structured responses where feasible (for example, JSON with title and body) to simplify parsing. The result is not perfect for every repository, but in practice it produces a strong first draft that developers can polish quickly.

Lessons from my first package

My first package taught me lessons that directly shaped lazypr. The first lesson is that a small surface area wins: when I shipped my first package I over-indexed on features and under-indexed on sensible defaults, and users wanted something predictable, so for lazypr I focused on the single core flow and made every addition opt-in.

The second lesson is that good defaults matter: most users will use defaults more than any edge setting, so sensible defaults for the model, locale, and retries reduce cognitive load and make the CLI useful out of the box. The third lesson is to design for failure: my first package exposed me to the many ways environments vary - networking, clipboard access, and CI systems can all fail - so lazypr surfaces clear, actionable errors and provides fallbacks (print output when clipboard is unavailable, meaningful exit codes on failure) so both automation and interactive use work reliably.

If you want, I can expand this section to name the first package and include specific anecdotes - for privacy and accuracy I kept the stories general here, but I can add concrete details on request.

What I would improve next

There are a few directions I would pursue next. First, add first-class provider integrations with opt-in GitHub/GitLab draft PR creation so teams can automate a full flow. Second, introduce per-repo presets that save preferred models, base branches, or default templates in repo-specific config. Third, improve structured outputs with richer schemas for changelogs and release notes to power automation. Finally, add an offline fallback: a lightweight local summarizer that can produce very short titles when the AI provider is unreachable.

Final thoughts

lazypr is an example of a tiny tool that reduces friction and improves communication. It's intentionally focused: it extracts the narrative already present in commits and transforms it into something readable and actionable for reviewers.

If you're using lazypr and it doesn't map well to your commit style or template, reach out - I iterate quickly on prompts and templates. If you'd like, I can add a follow-up section with real-world before/after examples from a repository you pick.