Skip to content

Repository files navigation

bun-pdf

A command-line tool for reading, filling, and commenting on PDF files — built for agents to drive.

  • Reads a PDF as annotated markdown. Tables become tables, form fields become HTML controls, figures become image links that resolve to real files.
  • Fills forms and leaves comments, saving incrementally so the original bytes stay a byte-identical prefix and signatures survive.
  • Every change reads back. A fill you cannot see in form export, or a comment you cannot see in comments list, is treated as a failure by the tool itself — not left for you to discover.

Status: pre-release (0.1.0). The command surface and output contract are stable enough to build on. The read view is still improving on dense tables; docs/metrics.md tracks exactly where it stands, and known gaps are marked test.failing in the suite rather than quietly omitted.

Install

macOS only for now (arm64 and x64). The platform gate is deliberate rather than incidental — the tool refuses to run elsewhere instead of half-working.

bun add -g bun-pdf     # or: bunx bun-pdf --help

Or grab a standalone binary — no runtime needed:

curl -fsSLO https://github.com/kklimuk/pdf-cli/releases/latest/download/install.sh
sh install.sh

The installer is a release asset, not a file served from a branch — so it is immutable per release, listed in that release's SHA256SUMS, and you can verify it before you run it. It downloads only release assets and executes no remote code. It verifies the binary against SHA256SUMS and refuses to install on a mismatch.

Once the standalone binary is installed, it updates itself:

pdf upgrade              # latest release
pdf upgrade --to v0.1.2  # a specific tag
pdf upgrade --dry-run    # report only

upgrade runs the same installer, embedded in the binary rather than fetched, so the replacement is pinned to a release tag and SHA-256 verified before it lands. An npm install is owned by the package manager; upgrade says so and exits nonzero instead of fighting it.

As an agent skill

skills/pdf-cli/ is a self-contained Agent Skill. Claude Code and Codex discover it from the plugin manifests in .claude-plugin/ and .codex-plugin/. An agent with no Bun can run sh skills/pdf-cli/scripts/bootstrap.sh, which resolves the latest release tag, downloads the binary pinned to it, and verifies its SHA-256 before installing.

SKILL.md is generated by the binary (pdf info skill) — see CONTRIBUTING.md.

Quick example: filling a W-4

$ pdf info fw4.pdf
5 pages · ~4210 tokens · form: xfa-static (19 fields)
next: pdf read fw4.pdf (fields render inline) · pdf form fields fw4.pdf

$ pdf form fields fw4.pdf
xfa-static · 5 page(s) · 19 fields
aliases are stable; fill: pdf form fill fw4.pdf --data values.json

p1  f1_01  text  (a) First name and middle initial
p1  f1_02  text  Last name
p1  f1_05  text  (b) Social security number
p1  c1_1   choose-one  set to ONE of: 1 (Single or Married filing separately) …

$ echo '{"f1_01":"Dana","f1_02":"Okafor","f1_05":"123-45-6789","c1_1":"1"}' \
    | pdf form fill fw4.pdf --data -
4 fields filled, verified on re-read

$ pdf form export fw4.pdf
{"f1_01":"Dana","f1_02":"Okafor","f1_05":"123-45-6789","c1_1":"1"}

The fill is all-or-nothing: if any value is invalid, nothing is written and every problem is listed at once. After saving, the file is re-opened and every touched field re-read — "filled but blank when printed" becomes a hard error instead of a silent success.

Leaving comments

$ pdf comments add lease.pdf --anchor "shall not be liable" --text "Check this" --highlight
p2.ann0

$ pdf comments reply lease.pdf --at p2.ann0 --text "Counsel signed off"
p2.ann1

$ pdf comments list lease.pdf
[{"locator":"p2.ann0","page":2,"kind":"highlight","author":"Reviewer","text":"Check this", …},
 {"locator":"p2.ann1","page":2,"kind":"note","text":"Counsel signed off","inReplyTo":"p2.ann0", …}]

You say where in the document's own words. The phrase is found the way pdf find finds it, and the highlight lands on the glyphs that drew it — a phrase that wraps is marked as two runs, the way a person would draw it. A phrase matching several places is refused with a count and the flag that resolves it, because a comment on the wrong clause is worse than no comment.

pdf <command> --help is the authoritative contract

Help text renders the locator and anchor grammars from the same registry the parser uses, so it cannot drift from what the tool accepts. Where this README and --help disagree, --help is right.

info      Inspect a FILE, or print reference material (pdf info locators)
read      Print pages as annotated markdown (fields as HTML controls)
outline   Document structure as a pN locator tree
find      Locate text, mint span locators for --at
wc        Pages, words, and estimated tokens before you read
render    Rasterize pages or a region to PNG (--read: the markdown view)
images    List and extract embedded images (list, extract)
form      Inspect and fill AcroForm fields (fields, fill, export)
comments  Read and write annotations (list, add, reply)

Output contract

  • Exit codes are the signal: 0 ok, 1 error, 2 usage or bad locator, 3 not found.
  • Errors are one JSON line: {"code": "...", "error": "...", "hint": "..."}.
  • A mutation that changes nothing is an error, never a cheerful zero-count — NO_CHANGE, MATCH_NOT_FOUND, ANCHOR_NOT_FOUND. Weak agents react to exit codes, not to prose.
  • Mutators print what they minted (a locator per line) or a one-line ack; --verbose prints the full JSON.
  • --dry-run validates everything — anchors resolved, save ladder checked — and writes nothing. A passing dry run means the real run will succeed.

Locators

Everything addressable has a stable handle. Run pdf info locators (no file needed) for the full grammar.

p3            page 3 (1-based, agrees with --pages)
p3.b7         block 7 of page 3
p3.b7:10-24   characters 10–24 of that block (end-exclusive)
p3.t0         table 0 of page 3
p3.img1       image 1 of page 3
p3.ann0       comment 0 of page 3

Page numbers are 1-based because they agree with what a person sees; everything minted below the page is 0-based. Ids are deterministic for a given file and pipeline version, so an id from one run still means the same thing in the next.

How it works

Two engines, one document. PDFium reads (text, page objects, rasterizing, images, the structure tree); pdf-lib writes (fills, annotations, incremental save). Nothing outside src/core/engine/ imports either — everything else compiles against neutral types, so the engines stay swappable.

Coordinates are top-left, y-down, in points everywhere above the engine adapters. PDF's bottom-up space is flipped in exactly two files and nowhere else, so no downstream code has to remember which way is up.

Saves are incremental by default. The original bytes must remain a byte-identical prefix of the output — asserted on every save, not assumed. A signed document stays verifiable as "signed, then modified" rather than being quietly invalidated. A full rewrite happens only with --rewrite, and is refused on a signed document without --force.

Reading is a layout problem, not a text-extraction problem. A PDF stores glyphs at coordinates; paragraphs, columns, tables and reading order are all inferred. Figures are settled before tables so chart labels never become candidate cells; columns are settled before either. The table detector is a face lattice — edges propose boundaries, merging disposes of them.

Content renders as what it is. Images are ![alt](url) with the file actually written; form fields are <input name="alias"> fillable by that name; a scanned page says so and names the command that rasterizes it, because an agent that can see is better served by a picture than by an empty string.

Stack

@embedpdf/pdfium MIT (PDFium itself BSD-3) — read engine
@cantoo/pdf-lib MIT — write engine
@pdf-lib/fontkit MIT — font embedding
ESDB / SCOWL word list permissive, notice required — dehyphenation

No copyleft: no GPL, LGPL or AGPL code, and no MPL-licensed source. NOTICES carries the full attribution and ships with every release.

Contributing

bun install, then bun run check (biome + knip + tsc) and bun test before anything is considered done. CLAUDE.md carries the conventions and the invariants the code holds itself to.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages