npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agentsox/pdf-builder

v0.3.0

Published

Agent-first CLI that turns a declarative document spec into a correct, branded PDF (Typst engine).

Downloads

227

Readme

@agentsox/pdf-builder

npm CI license: MIT

Turn a YAML (or JSON) spec into a PDF. You describe what's on the page — headings, tables, totals, equations, charts — and it renders with Typst. The same spec always produces the same bytes.

It's built to be driven by an LLM agent: the agent writes one structured file, gets a PDF plus page images back, and can look at what it made and fix it. It's also just a CLI, and works fine by hand.

spec.yaml  →  pdf  →  PDF + page PNGs + a manifest

Quick start

npm install -g @agentsox/pdf-builder
brew install typst                 # the render engine (other platforms below)
pdf new --template invoice > invoice.yaml
pdf build invoice.yaml --png

You get invoice.pdf, one PNG per page, and a manifest next to it.

What you write

A spec is either data for a template, or a list of blocks. Both become the same block tree.

With a template you give the data; the template handles layout and arithmetic:

template: invoice
data:
  client: { name: "Acme Co" }
  number: "INV-001"
  lineItems:
    - { description: "Setup", qty: 1, unitPrice: 1200 }

Totals and VAT are computed in code, never by you or the agent.

Freeform mode places blocks directly, for anything that isn't a template:

blocks:
  - { type: heading, level: 1, text: "Q2 Report" }
  - { type: text, text: "Revenue grew, with $\\Delta = 72\\%$ QoQ." }
  - type: table
    header: ["Client", "MRR"]
    rows: [["Acme", "$1,200"], ["Globex", "$900"]]

The full set of blocks: heading, text (with inline $…$ math, [label](url) links, **bold**, and _italic_), list, table, kv, math, chart, image, columns, sidebar, callout, spacer, pagebreak, header, footer. It's deliberately small, so it fits in your head (or an agent's context window).

What it handles

  • LaTeX math like \frac{d}{dx}, \int_a^b, \vec{F}. It's the default; set math: typst for native Typst math. A copy of mitex is bundled, so there's no first-run download and the output stays stable.
  • Right-to-left and mixed scripts. Set dir: rtl and lang on the document or any block. Hebrew, English, and numbers on one line resolve correctly, and a Hebrew font (David Libre) ships in the package.
  • Charts (bar, line, pie) via a bundled cetz, drawn in your brand color.
  • Side rails. A sidebar block draws a full-height colored column (left or right) for things like a CV's contact panel. The block carries only the content and side; the theme owns the rail's fill and text color.
  • Links. Write [label](url) in any text; http(s)/mailto become clickable, anything else stays literal.
  • Inline emphasis. **bold** and _italic_ in any text — semantic, so the theme still owns how bold/italic look. Intraword underscores (snake_case, file_name) are left alone.
  • Alignment. align: left | center | right on a heading, text, or list — e.g. a centered title. (Text direction is separate: dir: ltr | rtl.)
  • Letter-spacing. A theme can space out headings (heading.tracking, e.g. 0.2em) for tracked-caps titles — the theme owns it, not the spec.
  • Errors instead of bad PDFs. Unknown keys, ragged tables, missing images, unavailable fonts — each comes back as { path, expected, got, fix }. You won't get a wrong-but-plausible document.
  • A JSON Schema (pdf schema) for validation and editor autocomplete on spec files.

Invoices, reports, recipes, CVs, cheat sheets, study notes — they're all just blocks.

Theming

A theme owns the look: fonts, colors, the logo, callout styles. Specs never touch any of that, so an agent can't pick clashing colors or the wrong font. To brand it, extend a built-in theme and override what differs:

# themes/acme.yaml
extends: default
fonts: { heading: "Poppins", body: "Inter" }
color: { primary: "#E11D48", text: "#111" }
logo: assets/acme-logo.svg
pdf theme init acme --out themes/acme.yaml                       # scaffold one
pdf build report.yaml --theme acme                               # found in ./themes
pdf build report.yaml --theme acme --font-path ./brand-fonts     # bring your own fonts

Switch --theme and the same spec re-renders in a different brand.

Spacing is a small scale the theme owns, so gaps stay harmonious and content never sits against a colored edge. A theme defines primitive steps and points named roles at them — specs and agents never set lengths:

extends: default
space:
  scale: { xs: 4pt, sm: 8pt, md: 12pt, lg: 16pt, xl: 24pt }
  block: sm     # gap between blocks
  gutter: lg    # gap between columns / rail ↔ main
  inset: sm     # padding in callouts/tables
  edge: xl      # safe-area: content ↔ a fill (page/sidebar) edge

Profiles

A profile bundles a theme with document defaults and reusable identity under a name like business or academic. Set it once and your specs carry only what changes between documents.

# ~/.config/pdf-builder/profiles/business.yaml
name: business
theme: acme
defaults: { lang: he, dir: rtl }
template:
  invoice:
    seller: { name: "Acme Ltd", taxId: "514…" }
    currency: ILS
    vat: { mode: standard }
pdf onboard                 # set one up interactively
pdf profile list            # ★ marks the default
pdf profile use academic    # change the default
pdf build invoice.yaml --profile business
pdf build paper.yaml --no-profile

Now an invoice is just the client and line items; the profile fills in the seller, tax ID, brand, and VAT. When a spec and a profile disagree, the spec wins, and the manifest records which profile was used. Your business details live in one file instead of every spec you hand out.

Examples

Each renders with pdf build examples/<name>.yaml --png:

| File | Shows | |---|---| | invoice.yaml | template path, computed totals | | hebrew-invoice.yaml | RTL invoice, localized labels, LTR amounts | | bilingual.yaml | mixed RTL/LTR on one line | | study-summary.yaml | LaTeX math, callouts, columns | | physics-cheatsheet.yaml | dense formula sheet | | recipe.yaml | columns and lists | | report.yaml | kv rows, a bar chart, tables, callouts | | cv.yaml | cv theme: a side rail, ruled accent headings, a link |

Install

npm install -g @agentsox/pdf-builder

You also need the Typst CLI on your PATH:

brew install typst          # macOS
cargo install typst-cli     # anywhere with Rust
winget install Typst.Typst  # Windows

It pins Typst 0.14.x, since the engine version changes layout and output bytes. A mismatch warns; override with PDF_BUILDER_ALLOW_TYPST_MISMATCH=1.

Commands

pdf build <file>     render a spec → PDF (+ PNGs, manifest)
pdf new              scaffold a starter spec
pdf templates        list templates
pdf themes           list built-in themes
pdf fonts            list font families Typst can see
pdf theme init       scaffold a brand theme
pdf schema           write the spec's JSON Schema
pdf guide            print the full playbook (see below)

Output location. By default the PDF lands in out/, named after the input file. Point it anywhere with -o/--output:

pdf build invoice.yaml                     # → out/invoice.pdf
pdf build invoice.yaml -o report.pdf       # → report.pdf (exact file)
pdf build invoice.yaml -o ~/Desktop/       # → ~/Desktop/invoice.pdf (folder, name kept)
pdf build invoice.yaml -o ~/Desktop/q2.pdf # → ~/Desktop/q2.pdf

A .pdf path sets the exact file; anything else is treated as a directory. (The granular --out <dir> + --basename <name> still work and take precedence.) Every build prints the final absolute path, and returns it as pdfPath under --json.

build flags: -o, --output <file|dir>, --theme <name|path>, --themes-dir <dir>, --font-path <dir> (repeatable), --out <dir>, --basename <name>, --png, --png-ppi <n>, --pdf-standard <a-2b|ua-1>, --strict, --json, --emit-typst, --emit-expanded-spec.

Determinism

The same spec produces a byte-identical PDF. Fonts are embedded, the creation date is pinned to zero, system fonts are ignored, and the Typst packages are vendored so nothing is fetched while rendering. Every build also writes a manifest:

{ "schemaVersion": 1, "pages": 1, "blocks": 6, "theme": "default",
  "typstVersion": "0.14.2", "hashes": { "spec": "…", "typst": "…", "output": "…" } }

For archival output, use --pdf-standard a-2b (PDF/A) or ua-1 (PDF/UA). If the result doesn't conform, the build fails instead of pretending it did.

Using it from an agent

Run pdf guide --json once. It returns everything in a single call: the workflow, the block list, the available themes, templates, and profiles, the paths to write config to, a worked example, and the JSON Schema. An agent can onboard from that alone, with nothing pasted into its prompt.

The loop it's designed for: the person describes their brand in plain words ("we're Acme, teal, VAT-registered, logo's attached"); the agent writes the theme and profile files to the paths from pdf guide, runs pdf profile use, and every build after that is branded. Same idea for "summarise these files into one PDF" — the agent reads them, writes a freeform spec, builds it, looks at the PNGs, and adjusts. (pdf onboard is just the by-hand version of that setup.)

Every command accepts --json and prints one envelope, a discriminated union on ok, with a non-zero exit on failure:

// success
{ "ok": true, "pdfPath": "…", "pageImages": ["…"], "manifest": { }, "warnings": [ { "path", "expected", "got", "fix" } ] }

// failure
{ "ok": false, "error": { "kind": "validation", "message": "…", "issues": [ { "path", "expected", "got", "fix" } ] } }

error.kind is one of validation, typst_missing, typst_compile, io, unknown, so an agent branches on it without matching strings. The process exit code matches the kind (validation=1, typst_missing=2, typst_compile=3, io=4, unknown=5), so a shell can branch on $? too. pdf guide --json returns this contract (envelope, kinds, exit codes, result keys) alongside the playbook.

Library

import { build } from "@agentsox/pdf-builder";

const result = await build(spec, { theme: "default", png: true });
// → { pdfPath, pageImages, manifest, warnings }

License

MIT. See LICENSE.