Reference
The PR Lens CLI: diagram a change before you open the pull request
Run PR Lens locally with @coldtea/pr-lens-cli: analyze a diff, render the SVGs, compose the comment, validate a document, push a canvas. Your key, your machine.
Yes, PR Lens runs locally. One command reads a diff with your own model key and writes the graph document; a second draws it as light and dark SVGs.
npx @coldtea/pr-lens-cli analyze --base origin/main
npx @coldtea/pr-lens-cli render .pr-lens/graph.jsonNo pull request has to exist. No PR Lens service is in that path. The package is @coldtea/pr-lens-cli on npm, currently 0.4.0, MIT, and it needs Node 20.11 or newer. Installed, it puts a binary called pr-lens on your path, which is the name I will use for the rest of this post.
Bring your own key
Only analyze talks to a model. render, comment, validate, export and canvas never do, which means you can rework a document, redraw it and rebuild a comment as often as you like without another provider call.
The key is read from the environment, never from a flag, because a flag lands in shell history and in a CI log. The diff goes to the provider you name and to nobody else.
export GEMINI_API_KEY=...
pr-lens analyze --base origin/main --head HEADThree request shapes are implemented rather than a list of vendors.
--provider | Endpoint | Key |
|---|---|---|
gemini (default) | Google's own API | GEMINI_API_KEY |
openai | OpenAI's own API | OPENAI_API_KEY |
openai-compatible | anything speaking /chat/completions, named by --base-url | OPENAI_API_KEY |
DeepSeek, OpenRouter, Ollama and llama.cpp are all reached by pointing --base-url at them, and a new vendor needs no release. OpenAI has its own row rather than sharing the compatible one because the two have drifted: OpenAI renamed the output limit to max_completion_tokens and its newer models reject max_tokens, which is the only spelling the servers that copied it know. No field works for both, so you say which endpoint you are talking to.
pr-lens analyze --base origin/main \
--provider openai-compatible \
--base-url http://localhost:11434/v1 \
--model qwen3-coder \
--api-key-env OLLAMA_KEYThe commands
There are seven: analyze, render, comment, validate, export, canvas and skill. Any of them takes --help.
analyze
Diff in, graph document out.
pr-lens analyze --base origin/main --pr 42The document lands in .pr-lens/graph.json unless -o says otherwise. The base is the merge base of the two refs, not the tip of the base branch, because a diff against the tip would blame your change for every commit made on main since you forked.
The commit shas, the repository, the pull request number and the line counts are filled in from the repository itself, and anything the model writes in those fields is discarded. A fact the model is free to restate is a fact that eventually disagrees with itself.
The answer is parsed against the contract. If it fails, the validation errors go back to the model once, with their paths, and once only. --dry-run reports what would be sent and sends nothing, which is the flag to use when you want to know how big the request is before you pay for it. Other flags worth knowing: --lens to render one lens instead of both, --repo to read a repository other than the current directory, --max-diff-bytes to truncate what is sent, and --no-config to ignore the repository's own config.
render
pr-lens render .pr-lens/graph.jsonDraws the document as self-contained light and dark SVGs, one pair per drill-down section per lens, or one pair per lens when the document has no sections. --theme light or --theme dark draws half the pair.
It writes two more files beside them. manifest.json says what was drawn and under which file name, and drawn.graph.json is the document those pictures actually show. The second one exists because corrections are applied at draw time, and excluding a node can empty a whole drill-down section so the renderer draws nothing for it. A comment composed from the document that went in would announce a section that came out of nothing.
The SVGs carry no script and no external reference, and the same document renders to the same bytes every time.
comment
pr-lens comment --graph .pr-lens/drawn.graph.json --manifest .pr-lens/manifest.json \
--asset-base-url https://raw.githubusercontent.com/owner/repo/pr-lens/42Composes the markdown and prints it. That is the <picture> pairs which read in both GitHub themes, the headline chips, and the nested <details> tree. It posts nothing; posting is your business, and --print-marker gives whatever does the posting the hidden marker that identifies an existing comment to update.
Two details save you a confusing hour. The two files have to belong to each other: the manifest records the hash of the document it came from, and a mismatched pair is refused rather than composed into a comment describing diagrams nobody drew. And --asset-base-url is where you published the SVGs. Leave it out and the markdown points at local paths no reader can fetch.
Each diagram in that markdown links to itself, because a comment column is about 830 pixels wide and a system with several lanes is several times that. The picture arrives scaled to fit, and one click gives a reader the size the labels were drawn at.
validate
pr-lens validate .pr-lens/graph.json .github/pr-lens.ymlParses graph documents, patch documents, render manifests and configs, JSON or YAML, and reports every problem in each rather than stopping at the first. A file with no kind field is read as a config, because a config is the only one of the four a person writes by hand.
This is the command an agent loops on. Six rules cannot be expressed in JSON Schema and are checked only by the parser: referential integrity, a line range that ends before it starts, a self message whose endpoints disagree, a patch whose two commits are the same, more views than a render manifest could describe, and a walkthrough step focusing flow steps the diagram on its stage does not draw. Structured output alone does not make a document valid.
export
pr-lens export .pr-lens/graph.json -o .github/pr-lens.map.jsonTurns a pull-request document into a map of the system, once that pull request has merged. Elements the change deletes are dropped, along with the edges and flow steps hanging off them; the rest stops being annotated; the walkthrough goes with the change it narrated; and the result is stamped with the single commit it reflects.
The map is a snapshot, and nothing reads it back into the pipeline. A committed map that overrode inference would be hand-maintained rot with merge conflicts attached. Commit it so a repository has something to read, to diff, and to hand an agent.
canvas
pr-lens canvas pushPuts the document on prlens.dev as a page anyone with the link can read, plus an SVG a README can embed. push sends the JSON, never an SVG, and the app draws it. The default source is .pr-lens/drawn.graph.json, so the page shows what your diagrams show. The first push of a file mints a canvas and every push after that updates the same one, matched by the path it came from.
It prints three links: the view link at prlens.dev/c/<id>, an edit link with the write token in its fragment, and an embed link ending in .svg. Share the first. Anyone holding the second can push over your canvas, so it stays with you, and pr-lens canvas rotate mints a new token if it leaks. pull fetches a document back by link or bare id, which is how a fresh checkout recovers a canvas it can still edit.
The write token lives in .pr-lens/canvas.json, and the CLI refuses to write that registry anywhere git could commit it. A checkout that tracks the file, or un-ignores .pr-lens/, gets CANVAS_REGISTRY_EXPOSED and nothing is written.
skill
pr-lens skill prints the agent-facing instructions to stdout, and pr-lens skill references prints the configuration format, the graph document specification and a complete example. It is how you hand the whole thing to an agent that has no skill installed. The packaged version is in PR Lens as an agent skill.
Where the files go
analyze and render write to .pr-lens/ in the current directory, and the first write adds that directory to your .gitignore alongside a README saying what it holds.
None of it is meant to be committed. The SVGs, the document and the manifest are rebuilt from the diff whenever anyone wants them, and a diagram of a pull request that merged months ago is worse than no diagram at all. What lasts is the comment on the pull request and the map that export writes. --out moves the output somewhere else on every command that writes, and when you do that PR Lens leaves your .gitignore alone.
Failures a script can branch on
Every failure carries a code: USAGE, UNREADABLE_FILE, UNKNOWN_DOCUMENT, INVALID_DOCUMENT, GIT_FAILED, EMPTY_DIFF, REPOSITORY_UNKNOWN, MISSING_API_KEY, PROVIDER_FAILED, MODEL_OUTPUT_INVALID, RENDER_FAILED, and for canvas a further set beginning CANVAS_. Misuse exits 2, everything else exits 1.
The reason to have it on your machine
Reviewing is the obvious use, and it is not the best one. The better one is drawing your own change before anybody else has to read it.
pr-lens analyze --base origin/main
pr-lens render .pr-lens/graph.json
open .pr-lens/*-dark-*.svgLook at the picture. If a component sits in a lane that surprises you, or an edge runs somewhere you did not intend, you found that in your own diff instead of in a reviewer's comment three days later. Then open the pull request.
The Action shells out to these same commands, and the hosted App draws with the same renderer and the same schema, so the picture on your laptop is the picture your reviewers get. See the PR Lens GitHub Action for the CI version, or what is PR Lens for the four ways to run it.
Questions people ask
Can I run PR Lens locally?
- Yes. npx @coldtea/pr-lens-cli analyze --base origin/main reads the diff with your own model key and writes a graph document, and npx @coldtea/pr-lens-cli render draws it as light and dark SVGs in .pr-lens/. Nothing is sent to any PR Lens service on that path, and no pull request has to exist.
Which model providers does the PR Lens CLI support?
- Three request shapes: --provider gemini (the default, using GEMINI_API_KEY), --provider openai (OPENAI_API_KEY), and --provider openai-compatible with a --base-url for anything else answering /chat/completions. That last one covers DeepSeek, OpenRouter, Ollama and llama.cpp without a release.
Do I need an API key for every PR Lens CLI command?
- No. Only analyze talks to a model. render, comment, validate, export and canvas never do, so you can rework a document, redraw it and recompose a comment as many times as you like without another provider call.
Should I commit the .pr-lens directory?
- No, and the CLI adds it to your .gitignore the first time it writes there. The SVGs, the document and the manifest are rebuilt from the diff whenever anyone wants them. The one file worth committing is the map that pr-lens export writes.
What Node version does the PR Lens CLI need?
- Node 20.11 or newer. The package is @coldtea/pr-lens-cli, it installs a binary called pr-lens, and it is MIT licensed like the rest of the repository.
Sources
Keep reading
Turn it on
Install the App and every pull request in the repositories you pick gets drawn. Or hand the prompt to the agent you already have open.