Research Notes
Honest writeups on AI systems

Printing Press: When You Don't Have to Write the CLI

How we generated a production GitHub CLI in 45 seconds — and what the compound query gap reveals about AI tool design.

# Printing Press: When You Don't Have to Write the CLI

Or: How we generated a production GitHub CLI in 45 seconds and what it revealed about the gap between "having an API" and "having a useful tool"


The Problem Nobody Talks About

Every serious AI project eventually hits the same wall: the API exists, the docs exist, and yet actually using it feels like work. You spend an hour writing the HTTP client. Another hour handling pagination. Then auth. Then error handling. Then the compound queries you actually need — the ones that require joining data across two API calls — become their own mini-project.

For human use this is annoying. For AI agents it's a different kind of failure mode. An agent that has to figure out your API wrapper on every call is burning context tokens on machinery that should be invisible.

The conventional answer is SDKs. But SDKs solve the wrong problem. They give you types and methods for what the API can do — not for what you actually want to accomplish. The gap between "I can call repos.list()" and "I want every issue in this org that has no assignee and hasn't been touched in 30 days" is where most real work happens, and SDKs don't bridge it.

Printing Press is a different answer. It doesn't wrap the API — it builds a CLI around it.


What Printing Press Actually Is

[CLI Printing Press](https://github.com/mvanhorn/cli-printing-press) is an open-source CLI generator from Michael Van Horn. You give it an API (by name, by OpenAPI spec, or by URL), and it produces:

The generated CLI follows what Peter Steinberger calls the "discrawl playbook": local data, compound commands, agent-native flags. Every CLI printed by the press shares this DNA.

The install is one line:


go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest

You also clone the skills repo (drives the generation loop):


git clone https://github.com/mvanhorn/cli-printing-press.git

Then inside Claude Code: /printing-press . Or directly via the binary: printing-press generate --spec --name .


How It Works: The Generation Pipeline

The generate command takes an OpenAPI spec and produces a Go project. With an official spec from GitHub's REST API description repo, the full command is:


printing-press generate \
  --spec "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json" \
  --name github \
  --spec-source official \
  --validate

What --validate does is where it gets interesting. The generation process runs quality gates:

1. go mod tidy — ensure dependencies resolve

2. go build — ensure the code compiles

3. Endpoint coverage — check that every endpoint in the spec has a handler

4. Auth verification — confirm the CLI can reach the API

If any gate fails, you get a specific error rather than a mystery compile failure two hours from now.

The output is a complete Go project at ~/printing-press/library//:


github-pp-cli/
├── cmd/
│   ├── github-pp-cli/main.go    # The CLI binary
│   └── github-pp-mcp/main.go    # The MCP server
├── internal/
│   ├── cli/      # Cobra command tree
│   ├── client/   # HTTP client with retries, auth
│   ├── store/    # SQLite local mirror
│   ├── cache/    # Response caching
│   ├── config/   # Auth config
│   └── mcp/      # MCP server implementation
├── go.mod
├── README.md     # Full command reference + install guide
├── SKILL.md      # Claude Code skill for agent use
├── Makefile
└── NOTICE

The generated CLI is 1,404 Go source files and 1,413 files total. That includes every endpoint, the SQLite schema, the MCP server, and the skill. You didn't write any of it.


The GitHub CLI: What We Actually Got

We generated the GitHub CLI from the official OpenAPI spec. Here's what the --help output looks like — 91 lines of commands:


github-pp-cli --help
├── advisories         # Security advisories
├── app                # GitHub App management
├── assignments        # GitHub Classroom
├── classrooms
├── codes-of-conduct
├── events
├── gists
├── github-search     # Search (shadowed by framework 'search')
├── gitignore
├── issues            # ← the one you care about
├── licenses
├── orgs
├── repos
├── users
├── ...and 30+ more resource groups

But the resource commands are just the base layer. The compound commands are where it gets interesting:

orphans — Find unowned work


github-pp-cli orphans

Scans locally synced data for issues and PRs missing assignees, projects, or labels. This is the kind of triage query you'd normally have to write a script for. The CLI reads from the local SQLite mirror — no API call needed, instant results.

stale — Find forgotten work


github-pp-cli stale --days 14
github-pp-cli stale --days 7 --team backend

Items not updated in N days. Again: local SQLite, compound query across multiple resources, no API round-trips.

load — Workload distribution


github-pp-cli load --json

Shows how many items each person has assigned. Aggregates across the local mirror. This is a query the GitHub API literally cannot answer in one call — you need to fetch all issues, then group by assignee in-process. The local mirror makes it a single SQLite query.

sync — Local SQLite mirror


github-pp-cli sync                    # Full resync
github-pp-cli sync --since 7d         # Incremental: only new records
github-pp-cli sync --concurrency 8    # Parallel workers

This is the foundation. sync pulls API data into ~/.local/share/github-pp-cli/data.db. Once synced, every compound command above runs against local data, not the API. Incremental sync means you only fetch what changed since the last run.

search — Full-text search over local data


github-pp-cli search "payment failed" --data-source local
github-pp-cli search "critical" --type issues --json --limit 20

Uses SQLite FTS5. Once data is local, you can search it with queries that would be expensive or impossible against the live API — compound filters, regex, multi-field search.

--compact --agent — Token-efficient output

Every command accepts these flags for agent use:


github-pp-cli issues --compact --agent --per-page 3 --json

--compact returns only key fields (id, name, status, timestamps). --agent sets a bundle of agent-friendly defaults (--json --compact --no-input --no-color --yes). Combined, you get minimal token usage with predictable output format.


The Nuance That Separates It

The compound query problem

Most APIs are designed for single-resource operations. GitHub's API will happily tell you every issue in your org. It will also tell you every user. But it won't tell you "which issues are assigned to users who haven't been active in 30 days and belong to the backend team" — that's a three-way join that requires you to pull all three datasets and compute it yourself.

The local SQLite mirror is the solution. sync pulls everything locally. orphans, stale, load are all SQL joins over local tables. The API gives you data; the CLI gives you answers.

This is the insight behind the "novel feature" constraint in the Printing Press codebase: commands that can be answered from local data must read from local data. The generator enforces this — a handler that calls the API when it could have used the store is flagged as reimplementation.

Agent-native design

The --compact --agent flags exist because agents are a different user. A human wants rich output, colors, explanations. An agent wants minimal, predictable, parseable JSON. The same CLI serves both — with different flags.

The --data-source flag (auto | live | local) makes this explicit. auto tries the API and falls back to local. live forces API-only. local forces local-only. An agent can say "always use local" and never make an unnecessary API call.

The skill layer

The SKILL.md file generated alongside the CLI is the agent interface. It defines:

An agent that loads SKILL.md knows how to use the CLI without you explaining it. This is the "sheath" that Peter Steinberger mentions — the CLI is the blade, the skill is how you pick it up without cutting yourself.


What We Ran Into

Go toolchain version

The VM this was run on has Go 1.22.3. The generated CLIs require Go 1.23+. This caused go mod tidy to fail during generation — the toolchain downloaded go1.23 for linux/amd64 but it's not yet available in the Go version selection algorithm.

The workaround: use GOTOOLCHAIN=auto (downloads the required toolchain automatically) or point directly at the already-cached toolchain at ~/.go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.3.linux-amd64/bin/go. Once the toolchain is available, the generated CLI builds and runs fine.

This is a transitional issue — Go 1.23 is now stable and the generated go.mod files specify it. As Go toolchains catch up across environments, this friction goes away.

GraphQL is a different problem

GitHub's API is REST. Linear's API is GraphQL. Printing Press works with OpenAPI specs — REST-first. For GraphQL APIs, you need an intermediate step: convert the GraphQL schema to an OpenAPI spec, or use browser-sniff to capture traffic and generate a spec from that.

For Linear specifically, this is solvable (the GraphQL introspection endpoint is open), but it's not a one-command job. REST APIs are the path of least resistance.


Why This Matters for AI Agents

The pattern Printing Press implements — local mirror, compound queries, agent-native flags — is the right model for AI tool use.

When an agent calls a printed CLI:

1. It loads SKILL.md — knows the commands without you explaining

2. It uses --compact --agent --json — minimal tokens, parseable output

3. It reads from --data-source local — no rate limits, no API failures, no latency

4. Compound queries run as SQL joins — answers, not data

The alternative — calling the raw API with an HTTP client — means the agent manages auth, pagination, retries, error handling, and rate limits on every call. That's context overhead that compounds. The printed CLI makes all of that structural.

The goat CLIs Gary mentioned (company-goat, contact-goat, archive-is) are likely all printed this way: OpenAPI spec → printed CLI → compound commands tuned to the domain → agent-native output. Archive-is in particular is interesting for research workflows — archive.today snapshots + markdown extraction, composable with any other CLI.


What We Printed and What We'd Print Next

We printed GitHub. It works. 1,404 Go files, production CLI, MCP server, SQLite mirror — 45 seconds of generation.

Next we'd print:

The pattern for any of these: printing-press generate --spec --name . If the spec exists, it works. If it doesn't, browser-sniff or write a minimal OpenAPI wrapper.

The real leverage: once you have CLIs for your core systems, the agent workflows become composable. GitHub issues → Linear tickets. Notion pages → research summaries. Archive.is → Claude context. Each CLI is a building block. Printing Press is the machine that makes the building blocks.