Set up a new integration, connector, or CLI binding for any API. Wrap or generate a ship-ready Go CLI from an OpenAPI, HAR, or Postman spec via the lean research -> generate -> build -> shipcheck loop. Use when the user says build a CLI, wrap this API, set up a new integration, add a connector, integrate with a service, or names an API by domain.
Installation
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section.Voir sur GitHub
name
printing-press
description
Set up a new integration, connector, or CLI binding for any API. Wrap or generate a ship-ready Go CLI from an OpenAPI, HAR, or Postman spec via the lean research -> generate -> build -> shipcheck loop. Use when the user says build a CLI, wrap this API, set up a new integration, add a connector, integrate with a service, or names an API by domain.
too many mandatory research documents before code existed
too many separate late-stage validation phases after code existed
too many chances to discover obvious failures late
This version uses one lean loop:
Resolve the spec and write one research brief
Generate
Build the highest-value gaps
Run one shipcheck block
Optionally run live API smoke tests
Artifacts are still written, but only the ones that materially help the next step.
Modes
Default
Normal mode. Claude does research, generation orchestration, implementation, and verification.
Codex Mode
If the arguments include codex or --codex, offload pure code-writing tasks to Codex CLI.
Use Codex for:
writing store/data-layer code
writing workflow commands
fixing dead flags / dead code / path issues
README cookbook edits
Keep on Claude:
research and product positioning
choosing which gaps matter
verification results and ship decisions
If Codex fails 3 times in a row, stop delegating and finish locally.
Polish Mode (Standalone Skill)
For second-pass improvements to an existing CLI, use the standalone polish skill:
/printing-press-polish redfin
See the printing-press-polish skill for details. It runs diagnostics, fixes verify failures, removes dead code, cleans up descriptions and README, and offers to publish.
Rules
Do not ship a CLI that hasn't been behaviorally tested against real targets.go build and verify pass-rate are structural signals, not correctness signals. Phase 5's mechanical test matrix runs every subcommand + --json + error paths; if that matrix was not executed, the CLI is not shippable. Quick Check is the floor; Full Dogfood is required when the user asks for thoroughness.
Bugs found during dogfood are fix-before-ship, not "file for v0.2". If a 1-3 file edit resolves it, do it now. ship-with-gaps is deprecated as a default verdict (see Phase 4). Context is freshest in-session; a v0.2 backlog that may never be revisited ships known-broken CLIs.
Features approved in Phase 1.5 are shipping scope. Do not downgrade a shipping-scope feature to a stub mid-build. If implementation becomes infeasible, return to Phase 1.5 with a revised manifest and get explicit re-approval.
Do not quote human-time estimates for sub-tasks ("~15-30 min", "~1 hour", "quick fix") in AskUserQuestion options, phase descriptions, or reference docs. The agent does the work, not the user; agent-fabricated estimates are notoriously bad and train users to distrust the prompt. Describe scope instead (lines of code, files touched, relative size). The carve-outs are wall-clock estimates for genuinely time-bound things: the whole-CLI run (set the user's expectation up front — most CLIs take 30+ minutes), tool installs (go install takes ~10 seconds), and printing-press subcommands that do network-bound work (crowd-sniff scans npm + GitHub, ~5-10 minutes). Anything bounded by agent reasoning time is not time-bound — describe scope.
Use raw captures for contract research. When reading official docs, auth/error/rate-limit pages, endpoint references, OpenAPI/Postman links, or source pages whose exact identifiers affect the generated CLI, read references/fetch-docs.md and use its fetch-docs.sh helper. Reserve WebFetch for quick TL;DR reads where losing field-level details is acceptable.
Optimize for time-to-ship, not time-to-document.
Reuse prior research whenever it is already good enough.
Do not split one idea across multiple mandatory artifacts.
Durable files produced by this skill go under $PRESS_RUNSTATE/ (working state) or $PRESS_MANUSCRIPTS/ (archived). Short-lived command captures may use /tmp/printing-press/ and must be removed after use.
Do not create a separate narrative phase for dogfood, dead-code audit, runtime verification, and final score. Treat them as one shipcheck block.
Run cheap, high-signal checks early.
Fix blockers and high-leverage failures first.
Reuse the same spec path across generate, dogfood, verify, and scorecard.
YAML, JSON, local paths, and URLs are all valid spec inputs for the verification tools.
Maximum 2 verification fix loops unless the user explicitly asks for more.
Secret & PII Protection (Cardinal Rules)
These rules are non-negotiable. They apply at ALL times during a run.
API key values, token values, passwords, and session cookies must NEVER
appear in any artifact: source code, manuscripts, proofs, READMEs, HARs, or
anything committed to git. Env var names (e.g., STEAM_API_KEY) and
placeholders (e.g., "your-key-here") are safe.
Exact-value scanning and auto-redaction of artifacts
HAR auth stripping (headers, query strings, cookies)
API key handling rules during the run
Session state cleanup ordering
Preflight
This section MUST run before any user-facing prompt — including the Orientation and Briefing flow below. A missing binary or available upgrade is information the user needs before they commit to an API. Do not invoke AskUserQuestion, print the orientation prose, or otherwise engage the user until preflight has completed and any signals from references/setup-checks.md have been handled.
# min-binary-version: 4.0.0# Derive scope first — needed for local build detection
_scope_dir="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
_scope_dir="$(cd "$_scope_dir" && pwd -P)"
_press_repo=falseif [ -d "$_scope_dir/cmd/cli-printing-press" ] && [ -f "$_scope_dir/go.mod" ]; then
_press_repo=truefi_resolve_press_bin() {
ifcommand -v cli-printing-press >/dev/null 2>&1; thencommand -v cli-printing-press
return 0
fiifcommand -v printing-press >/dev/null 2>&1 && printing-press version --json >/dev/null 2>&1; thencommand -v printing-press
return 0
fireturn 1
}
# Strict-older semver compare on the first three components. Pre-release# suffixes collapse to their GA counterpart (acceptable: we ship no pre-release# tags)._semver_lt() {
awk -v a="$1" -v b="$2"'BEGIN {
split(a, x, ".")
split(b, y, ".")
for (i = 1; i <= 3; i++) {
if ((x[i] + 0) < (y[i] + 0)) exit 0
if ((x[i] + 0) > (y[i] + 0)) exit 1
}
exit 1
}'
}
_source_press_version() {
sed -nE 's/^var[[:space:]]+Version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' \
"$_scope_dir/internal/version/version.go" 2>/dev/null | head -n 1
}
_rebuild_local_press_bin_if_stale() {
if [ "$_press_repo" != "true" ] || [ ! -x "$_scope_dir/cli-printing-press" ]; thenreturn 0
fi
_local_v="$("$_scope_dir/cli-printing-press" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"
_source_v="$(_source_press_version)"
if [ -z "$_local_v" ] || [ -z "$_source_v" ] || ! _semver_lt "$_local_v" "$_source_v"; then
return 0
fi
echo ""
echo "[local-binary-stale] local build v$_local_v is older than source v$_source_v"
if ! command -v go >/dev/null 2>&1; then
echo "[setup-error] local cli-printing-press binary is stale and Go is not on PATH, so it cannot be rebuilt."
return 1 2>/dev/null || exit 1
fi
if (cd "$_scope_dir" && go build -o ./cli-printing-press ./cmd/cli-printing-press); then
echo "[local-binary-rebuilt] rebuilt $_scope_dir/cli-printing-press"
echo ""
else
echo "[setup-error] local cli-printing-press binary is stale and rebuild failed."
return 1 2>/dev/null || exit 1
fi
}
# Prefer local build when running from inside the printing-press repo.
# Lefthook may keep ./cli-printing-press current, but hooks can be absent or
# disabled. Compare against the checked-out source version before trusting it.
_rebuild_local_press_bin_if_stale || { return 1 2>/dev/null || exit 1; }
if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
export PATH="$_scope_dir:$PATH"
echo "Using local build: $_scope_dir/cli-printing-press"
elif ! _resolve_press_bin >/dev/null; then
# Augment PATH if the binary is in ~/go/bin but not on the user's interactive PATH.
if [ -x "$HOME/go/bin/cli-printing-press" ]; thenexport PATH="$HOME/go/bin:$PATH"elif [ -x "$HOME/go/bin/printing-press" ] && "$HOME/go/bin/printing-press" version --json >/dev/null 2>&1; thenexport PATH="$HOME/go/bin:$PATH"else# Refuse: the cli-printing-press binary is required and we will not auto-install# it. The README's install flow is the source of truth;# silent auto-install hides failure modes (network, wrong GOPATH) inside an# opaque skill invocation.echo""echo"[setup-error] cli-printing-press binary not found."echo""ifcommand -v go >/dev/null 2>&1; thenecho"Install it in your terminal:"echo" go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest"elseecho"Go 1.26.5 or newer is also not installed. Install Go from https://go.dev/dl/, then:"echo" go install github.com/mvanhorn/cli-printing-press/v4/cmd/cli-printing-press@latest"fiecho""echo"Verify with: cli-printing-press --version"echo"Then re-run /printing-press."return 1 2>/dev/null || exit 1
fifi# Verify the Go toolchain is on PATH. Generation runs Go-based quality gates# (go mod tidy, go vet, etc.) after writing thousands of lines of scaffolding,# so a missing `go` only surfaces 5+ minutes in. Fail-fast costs one command -v# call when Go is present and converts a late, opaque failure into a 30-second# actionable abort.if ! command -v go >/dev/null 2>&1; thenecho""echo"[setup-error] Go toolchain not found."echo""echo"The Printing Press generator runs Go-based quality gates after generation."echo"Install Go 1.26.5 or newer from https://go.dev/dl/, then verify with:"echo" go version"echo"Then re-run /printing-press."echo""return 1 2>/dev/null || exit 1
fi# Verify the installed Go tree can compile and run common standard library# imports. A truncated Go extraction can leave the binary working enough for# `go version` while missing packages under $GOROOT/src, which otherwise fails# deep into generation during later Go quality gates.
_go_smoke_root="${PRINTING_PRESS_GO_SMOKE_DIR:-$HOME/.printing-press-smoke}"if ! mkdir -p "$_go_smoke_root"; thenecho""echo"[setup-error] Unable to create Go smoke-test workspace at $_go_smoke_root."echo"Set PRINTING_PRESS_GO_SMOKE_DIR to a writable non-temp directory and retry."echo""return 1 2>/dev/null || exit 1
fi
_go_smoke_dir="$(mktemp -d "$_go_smoke_root/stdlib.XXXXXX" 2>/dev/null || true)"if [ -z "$_go_smoke_dir" ]; thenecho""echo"[setup-error] Unable to create Go smoke-test workspace under $_go_smoke_root."echo"Set PRINTING_PRESS_GO_SMOKE_DIR to a writable non-temp directory and retry."echo""return 1 2>/dev/null || exit 1
ficat > "$_go_smoke_dir/go.mod" <<'__PP_GO_SMOKE_MOD__'
module pp-go-stdlib-smoke
go 1.20
__PP_GO_SMOKE_MOD__
cat > "$_go_smoke_dir/main.go" <<'__PP_GO_SMOKE_MAIN__'
package main
import (
"context""encoding/json""fmt""io""net/http""regexp"
)
func main() {
ctx := context.Background()
payload, err := json.Marshal(map[string]string{"status": "ok"})
if err != nil {
panic(err)
}
if !regexp.MustCompile(`ok`).Match(payload) {
panic("regexp mismatch")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
if err != nil {
panic(err)
}
_, _ = fmt.Fprint(io.Discard, req.Method)
}
__PP_GO_SMOKE_MAIN__
if ! (cd"$_go_smoke_dir" && GOFLAGS= GOWORK=off go run . >/dev/null 2>"$_go_smoke_dir/error.log"); then
_go_smoke_output="$(sed -n '1,12p' "$_go_smoke_dir/error.log" 2>/dev/null || true)"rm -rf "$_go_smoke_dir"echo""echo"[setup-error] Go std library is incomplete (truncated or corrupted install)."echo"Reinstall Go from https://go.dev/dl/ and verify with the smoke test before retrying."if [ -n "$_go_smoke_output" ]; thenecho""echo"Go smoke test output:"printf'%s\n'"$_go_smoke_output"fiecho""return 1 2>/dev/null || exit 1
firm -rf "$_go_smoke_dir"# Resolve and emit the absolute path the agent must use for every later# `cli-printing-press` invocation. `export PATH` above only affects this one# Bash tool call; subsequent calls open a fresh shell and resolve bare# `cli-printing-press` against the user's default PATH. When a global is# installed at a stale version, that silently shadows the local build the# preflight just chose. Handing the agent an absolute path eliminates the# shadow.if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
PRINTING_PRESS_BIN="$_scope_dir/cli-printing-press"else
PRINTING_PRESS_BIN="$(_resolve_press_bin 2>/dev/null || true)"fiecho"PRINTING_PRESS_BIN=$PRINTING_PRESS_BIN"echo"PRESS_REPO_MODE=$_press_repo"_pp_go_version_norm() {
printf'%s\n'"$1" | sed -nE 's/.*go([0-9]+)\.([0-9]+)(\.([0-9]+))?.*/\1.\2.\4/p' | awk -F. 'NF >= 2 { printf "%d.%d.%d\n", $1, $2, ($3 == "" ? 0 : $3) }'
}
_pp_check_go_currency() {
if [ -z "${PRINTING_PRESS_BIN:-}" ] || ! command -v go >/dev/null 2>&1; thenreturn 0
fi
_pp_go_installed="$(_pp_go_version_norm "$(go env GOVERSION 2>/dev/null)")"
_pp_go_required="$(_pp_go_version_norm "$(go version "$PRINTING_PRESS_BIN" 2>/dev/null)")"if [ -z "$_pp_go_installed" ] || [ -z "$_pp_go_required" ] || ! _semver_lt "$_pp_go_installed""$_pp_go_required"; thenreturn 0
fiecho""if [ "${GOTOOLCHAIN:-auto}" = "local" ]; thenecho"[setup-error] Go $_pp_go_required or newer is required by this cli-printing-press binary (installed: $_pp_go_installed)."echo"GOTOOLCHAIN=local disables automatic toolchain downloads, so later Go quality gates would fail."echo"Install Go $_pp_go_required or newer from https://go.dev/dl/, or unset GOTOOLCHAIN."echo""return 1
fiecho"[go-toolchain-old] Go $_pp_go_required or newer is required by this cli-printing-press binary (installed: $_pp_go_installed)."echo"PRESS_GO_INSTALLED=$_pp_go_installed"echo"PRESS_GO_REQUIRED=$_pp_go_required"echo"Default GOTOOLCHAIN behavior may download the required toolchain during Go commands."echo""return 0
}
_pp_check_go_currency || { return 1 2>/dev/null || exit 1; }
# Shadow detector (advisory). When a local build is in use, surface any# differing global so the user can see at a glance that the two binaries# disagree. Detect-only: the absolute path emitted above is the one the# agent will actually invoke; this warning does not change selection.if [ "$_press_repo" = "true" ] && [ -x "$_scope_dir/cli-printing-press" ]; then
_global_bin=""for _candidate in"$HOME/go/bin/cli-printing-press""/usr/local/bin/cli-printing-press""/opt/homebrew/bin/cli-printing-press""$HOME/go/bin/printing-press""/usr/local/bin/printing-press""/opt/homebrew/bin/printing-press"; doif [ -x "$_candidate" ] && [ "$_candidate" != "$_scope_dir/cli-printing-press" ] && "$_candidate" version --json >/dev/null 2>&1; then
_global_bin="$_candidate"breakfidoneif [ -n "$_global_bin" ]; then
_local_v="$("$_scope_dir/cli-printing-press" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"
_global_v="$("$_global_bin" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')"if [ -n "$_local_v" ] && [ -n "$_global_v" ] && [ "$_local_v" != "$_global_v" ]; thenecho""echo"[binary-shadow] local build v$_local_v differs from global v$_global_v at $_global_bin"echo"PRESS_BIN_LOCAL_VERSION=$_local_v"echo"PRESS_BIN_GLOBAL_VERSION=$_global_v"echo"PRESS_BIN_GLOBAL_PATH=$_global_bin"echo""fififi
PRESS_BASE="$(basename "$_scope_dir" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g; s/^-+//; s/-+$//')"if [ -z "$PRESS_BASE" ]; then
PRESS_BASE="workspace"fi
PRESS_SCOPE="$PRESS_BASE-$(printf '%s' "$_scope_dir" | shasum -a 256 | cut -c1-8)"
PRESS_HOME="${PRINTING_PRESS_HOME:-$HOME/printing-press}"
PRESS_RUNSTATE="$PRESS_HOME/.runstate/$PRESS_SCOPE"
PRESS_LIBRARY="$PRESS_HOME/library"
PRESS_MANUSCRIPTS="$PRESS_HOME/manuscripts"
PRESS_CURRENT="$PRESS_RUNSTATE/current"_pp_check_disk_space() {
_pp_disk_warn_kb="${PRINTING_PRESS_DISK_WARN_KB:-3145728}"
_pp_disk_fail_kb="${PRINTING_PRESS_DISK_FAIL_KB:-524288}"case"$_pp_disk_warn_kb$_pp_disk_fail_kb"in""|*[!0-9]*) return 0 ;;
esac
_pp_disk_path="$PRESS_HOME"while [ ! -e "$_pp_disk_path" ] && [ "$_pp_disk_path" != "/" ]; do
_pp_disk_path="$(dirname "$_pp_disk_path")"done
_pp_disk_avail_kb="$(df -Pk "$_pp_disk_path" 2>/dev/null | awk 'NR == 2 { print $4; exit }')"case"$_pp_disk_avail_kb"in""|*[!0-9]*) return 0 ;;
esacif [ "$_pp_disk_avail_kb" -lt "$_pp_disk_fail_kb" ]; thenecho""echo"[setup-error] Critically low disk space on the Printing Press workspace volume."echo"PRESS_DISK_PATH=$_pp_disk_path"echo"PRESS_DISK_AVAIL_KB=$_pp_disk_avail_kb"echo"PRESS_DISK_FAIL_KB=$_pp_disk_fail_kb"echo"Free disk space or set PRINTING_PRESS_HOME to a volume with more room, then re-run /printing-press."echo""return 1
fiif [ "$_pp_disk_avail_kb" -lt "$_pp_disk_warn_kb" ]; thenecho""echo"[low-disk] Printing Press workspace volume is low on free space."echo"PRESS_DISK_PATH=$_pp_disk_path"echo"PRESS_DISK_AVAIL_KB=$_pp_disk_avail_kb"echo"PRESS_DISK_WARN_KB=$_pp_disk_warn_kb"echo"Generation may need several GiB for generated files, Go build cache, and module downloads."echo""fi
}
_pp_check_disk_space || { return 1 2>/dev/null || exit 1; }
mkdir -p "$PRESS_RUNSTATE""$PRESS_LIBRARY""$PRESS_MANUSCRIPTS""$PRESS_CURRENT"# --- Latest-version advisory (fail-open) ---# Repo checkouts track origin/main because their skills and local binary come# from the checkout. Standalone installs track the latest released Go module.
PRESS_VERCHECK_FILE="$PRESS_HOME/.version-check"
PRESS_VERCHECK_TTL=86400
_now_ts=$(date +%s)
_should_check=trueif [ -f "$PRESS_VERCHECK_FILE" ] && [ -z "$PRESS_VERCHECK_FORCE" ]; then
_last_ts=$(awk -F= '/^last_check=/{print $2}'"$PRESS_VERCHECK_FILE" 2>/dev/null)
if [ -n "$_last_ts" ] && [ "$((_now_ts - _last_ts))" -lt "$PRESS_VERCHECK_TTL" ]; then
_should_check=falsefifiif [ "$_press_repo" = "true" ]; then# Repo mode checks origin/main every run because the checkout and local build# move quickly; skipped_repo_main suppresses repeated prompts for one SHA.if git -C "$_scope_dir" remote get-url origin >/dev/null 2>&1 &&
git -C "$_scope_dir" fetch --quiet origin main >/dev/null 2>&1; then
_head_rev=$(git -C "$_scope_dir" rev-parse HEAD 2>/dev/null || true)
_main_rev=$(git -C "$_scope_dir" rev-parse origin/main 2>/dev/null || true)
_skipped_repo_main=""if [ -f "$PRESS_VERCHECK_FILE" ] && [ -z "$PRESS_VERCHECK_FORCE" ]; then
_skipped_repo_main=$(awk -F= '/^skipped_repo_main=/{value=$2} END{print value}'"$PRESS_VERCHECK_FILE" 2>/dev/null)
fiif [ -n "$_head_rev" ] && [ -n "$_main_rev" ] &&
[ "$_head_rev" != "$_main_rev" ] &&
[ "$_skipped_repo_main" != "$_main_rev" ] &&
git -C "$_scope_dir" merge-base --is-ancestor "$_head_rev""$_main_rev" 2>/dev/null; thenecho""echo"[repo-upgrade-available] origin/main has newer Printing Press changes"echo"PRESS_REPO_DIR=$_scope_dir"echo"PRESS_REPO_HEAD=$_head_rev"echo"PRESS_REPO_MAIN=$_main_rev"echo""fiprintf"last_check=%s\nlatest=%s\nmode=repo\nskipped_repo_main=%s\n""$_now_ts""${_main_rev:-unknown}""$_skipped_repo_main" > "$PRESS_VERCHECK_FILE" 2>/dev/null || truefielif [ "$_should_check" = "true" ] && command -v go >/dev/null 2>&1; then
_installed=$("$PRINTING_PRESS_BIN" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
_latest=""if [ -n "$_installed" ]; then
_latest=$(go list -m -json github.com/mvanhorn/cli-printing-press/v4@latest 2>/dev/null | awk '
/"Version":/ {
version=$2
gsub(/[",]/, "", version)
sub(/^v/, "", version)
print version
exit
}
')
fi# Currency floor: the lowest release still considered safe to generate with,# published out-of-band so maintainers can raise it without a binary or skill# release. Fetched here (throttled by the TTL above) and cached for the# always-run enforcement gate below.
_min_supported=""
_min_reason=""ifcommand -v curl >/dev/null 2>&1; then
_floor_doc=$(curl -fsSL --max-time 5 \
https://raw.githubusercontent.com/mvanhorn/cli-printing-press/main/supported-versions.txt 2>/dev/null || true)
if [ -n "$_floor_doc" ]; then
_min_supported=$(printf'%s\n'"$_floor_doc" | awk -F= '/^min_supported=/{print $2; exit}')
_min_reason=$(printf'%s\n'"$_floor_doc" | sed -nE 's/^reason=//p' | head -n 1)
fifiif [ -n "$_installed" ] && [ -n "$_latest" ] && _semver_lt "$_installed""$_latest"; then# Marker for the skill prose below to detect and offer an interactive upgrade.# The skill reads PRESS_UPGRADE_AVAILABLE / PRESS_UPGRADE_INSTALLED from this output.echo""echo"[upgrade-available] printing-press v$_latest is available (you have v$_installed)"echo"PRESS_UPGRADE_AVAILABLE=$_latest"echo"PRESS_UPGRADE_INSTALLED=$_installed"echo""fiprintf"last_check=%s\nlatest=%s\nmode=standalone\nmin_supported=%s\nreason=%s\n" \
"$_now_ts""${_latest:-$_installed}""$_min_supported""$_min_reason" > "$PRESS_VERCHECK_FILE" 2>/dev/null || truefi# --- Currency-floor enforcement (standalone, every run, fail-open) ---# The floor *fetch* above is throttled to once per TTL, but enforcement must run# every invocation: a fresh cache must never let a stale binary keep generating# CLIs with since-fixed bugs. Compare the always-fresh installed version against# the cached floor; the network is never touched here. Only enforce a floor that# is itself <= latest, so a typo'd or tampered floor above the newest release# cannot brick every install.if [ "$_press_repo" != "true" ] && [ -f "$PRESS_VERCHECK_FILE" ]; then
_floor_min=$(awk -F= '/^min_supported=/{print $2; exit}'"$PRESS_VERCHECK_FILE" 2>/dev/null)
_floor_latest=$(awk -F= '/^latest=/{print $2; exit}'"$PRESS_VERCHECK_FILE" 2>/dev/null)
_floor_reason=$(sed -nE 's/^reason=//p'"$PRESS_VERCHECK_FILE" 2>/dev/null | head -n 1)
_floor_installed=$("$PRINTING_PRESS_BIN" version --json 2>/dev/null | sed -nE 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
if [ -n "$_floor_min" ] && [ -n "$_floor_installed" ] && [ -n "$_floor_latest" ] &&
_semver_lt "$_floor_installed""$_floor_min" &&
! _semver_lt "$_floor_latest""$_floor_min"; thenecho""echo"[upgrade-required] printing-press v$_floor_min is the minimum supported version (you have v$_floor_installed)"echo"PRESS_REQUIRED_MIN=$_floor_min"echo"PRESS_REQUIRED_INSTALLED=$_floor_installed"echo"PRESS_REQUIRED_REASON=$_floor_reason"echo""fifi# --- Browser-sniff backend advisory (fail-open, every-run) ---# browser-use and agent-browser are the preferred Phase 1.7 browser-sniff# backends. They are not hard requirements — vendor-spec, --spec, and --har# runs never invoke them — but when discovery does need them, mid-flight# install prompts are disruptive. Emit a marker every run so setup-checks.md# can strongly offer install. No decline caching: a run that didn't need them# yesterday may need them today, and the prompt cost is small.
_browser_use_missing=true
_agent_browser_missing=true# Use `command -v` only. Do NOT use `uvx browser-use --help` as a fallback# probe: when uvx exists but browser-use doesn't, that command silently# downloads and caches the package, which would be an unconsented install.# Downstream capture commands also invoke `browser-use` directly (not via# uvx), so a uvx-cache-only state would lie to the detection.ifcommand -v browser-use >/dev/null 2>&1; then
_browser_use_missing=falsefiifcommand -v agent-browser >/dev/null 2>&1; then
_agent_browser_missing=falsefiif [ "$_browser_use_missing" = "true" ] || [ "$_agent_browser_missing" = "true" ]; thenecho""echo"[browser-tools-missing] one or more browser-sniff backends not installed"echo"PRESS_BROWSER_USE_MISSING=$_browser_use_missing"echo"PRESS_AGENT_BROWSER_MISSING=$_agent_browser_missing"echo""fi# --- Codex mode detection (must run as part of setup, not a separate step) ---# Codex mode: opt-in only. User must pass "codex" or "--codex" to enable.ifecho"$ARGUMENTS" | grep -qiE '(^| )(--?codex|codex)( |$)'; then
CODEX_MODE=trueelse
CODEX_MODE=falsefi# Environment guard: don't delegate if already inside a Codex sandboxif [ "$CODEX_MODE" = "true" ]; thenif [ -n "$CODEX_SANDBOX" ] || [ -n "$CODEX_SESSION_ID" ]; then
CODEX_MODE=falsefifi# Health check: verify codex binary existsif [ "$CODEX_MODE" = "true" ]; thenifcommand -v codex >/dev/null 2>&1; then# Model and reasoning effort inherit from ~/.codex/config.toml. Do not pin -m / -c here.
CODEX_MODEL=$(grep -E '^model[[:space:]]*=' ~/.codex/config.toml 2>/dev/null | head -1 | sed -E 's/^model[[:space:]]*=[[:space:]]*"?([^"]+)"?.*$/\1/')
[ -z "$CODEX_MODEL" ] && CODEX_MODEL="codex default"echo"Codex mode enabled (model: $CODEX_MODEL). Code-writing tasks will be delegated to Codex."elseecho"Codex CLI not found - running in standard mode."
CODEX_MODE=falsefifi# Circuit breaker state
CODEX_CONSECUTIVE_FAILURES=0
MANDATORY: Read and apply references/setup-checks.md immediately after the setup contract bash block runs, before any other action. It handles the contract output signals: [setup-error] (refuse to run, surface the install instructions), optional [local-binary-stale] / [local-binary-rebuilt] repo-mode rebuild markers, [go-toolchain-old] / [low-disk] advisories, [repo-upgrade-available] (interactive AskUserQuestion prompt + optional repo pull), PRESS_REPO_MODE=<true|false> plus the targeted global open-agent-skills freshness check, the min-binary-version compatibility check (hard stop if binary is too old), [upgrade-required] (hard gate below the published currency floor — interactive upgrade-or-abort, no skip), [upgrade-available] (interactive AskUserQuestion prompt + optional standalone binary upgrade), [browser-tools-missing] (interactive AskUserQuestion prompt + optional install of browser-use and/or agent-browser), and the PRINTING_PRESS_BIN=<abs-path> marker plus optional [binary-shadow] warning (capture the path; use it for every subsequent generator invocation). Skipping the reference will cause the skill to proceed with a missing or out-of-date binary, run with stale global skill text when the session is managed by open-agent-skills, hit a mid-flight install prompt if browser-sniff is later needed, or invoke the wrong binary because a stale global or the public-library installer on PATH shadowed the local build. Do not skip.
Absolute-path rule. The preflight contract always emits PRINTING_PRESS_BIN=<absolute path> to stdout. Capture this value and substitute it (the resolved absolute path, not the literal $PRINTING_PRESS_BIN token) for every subsequent cli-printing-press ... invocation in this skill, references, and any sub-skill you delegate to. The export PATH=... line inside the contract only affects the single Bash tool call it runs in; later Bash tool calls open fresh shells and resolve bare cli-printing-press against the user's default PATH, where a stale globally-installed binary ($HOME/go/bin/cli-printing-press, Homebrew copy, etc.) will silently shadow the local build the preflight just chose. Bash code examples below are written cli-printing-press generate ... for readability — replace cli-printing-press with the captured absolute path each time you actually run one.
Only after preflight completes successfully (no [setup-error]; no [upgrade-required] left unresolved — the user either upgraded or the run was aborted; no global skill update that requires restart; any [repo-upgrade-available], [upgrade-available], or [browser-tools-missing] was offered to the user; PRINTING_PRESS_BIN is captured) should you proceed to the Orientation & Briefing section below.
Orientation & Briefing
After preflight has completed, check whether the user provided arguments. Handle two cases:
No Arguments: Orientation
If the user typed /printing-press with no arguments (no API name, no --spec, no --har, no URL), print an orientation and ask what they'd like to build:
The Printing Press generates a fully functional CLI for any API. You give it an API name, a spec file, or a URL. It researches the landscape, catalogs every feature that exists in any competing tool, invents novel features of its own, then generates a Go CLI that matches and beats everything out there — with offline search, agent-native output, and a local SQLite data layer.
By the end, you'll have a working CLI in $PRESS_LIBRARY/ that you can use for yourself, ship on your own, or apply to add to the printing-press library.
The process takes 30-60 minutes depending on API complexity. Simple APIs with official specs (Stripe, GitHub) are faster. Undocumented APIs that need discovery (ESPN, Domino's) take longer.
Print these example invocations as plain text BEFORE the AskUserQuestion call (so they appear as context above the question, not as competing menu options):
question:"What API would you like to build a CLI for?"
header:"API target"
multiSelect:false
options:
label:"Type it (recommended)" — description:"Provide an API name, URL, spec path, or HAR file via the 'Other' option below."
label:"Browse existing CLIs first" — description:"Visit the public library to see what's already been printed before deciding what to build."
Do not add additional options — no "Show me popular options", no pre-populated buttons for Notion / Stripe / GitHub / Linear / Discord. The example invocations above already cover the common shapes, and most popular APIs are already in the public library (offering to re-print them is noise). The two options above plus the automatic "Other" field is the entire interface.
If the user picks "Type it (recommended)", they will provide their answer via the auto "Other" field. Set their input as the argument and proceed to the briefing below.
If the user picks "Browse existing CLIs first", print the public library URL prominently and try to open it in the browser, then end the skill so the user can browse before deciding:
echo""echo"Public library: https://github.com/mvanhorn/printing-press-library"echo"(If you have the Printing Press Library plugin, you can also run /ppl in Claude Code.)"echo""command -v open >/dev/null 2>&1 && open https://github.com/mvanhorn/printing-press-library
After printing, end the skill cleanly. Do not proceed to briefing or research — the user is exploring, not building yet. They can re-invoke /printing-press <api> once they've decided.
With Arguments: Briefing
When the user provided an argument (API name, --spec, --har, or URL), print a brief process overview. This sets expectations and collects any upfront context. (Preflight has already run at this point.)
Print as prose, matching the style of the example below:
Very well. Setting the type for <API>.
Here is how this will proceed:
I shall research <API> across the internet: official docs, community wrappers, competing CLIs, MCP servers, and npm/PyPI packages
I shall catalog every feature that exists in any tool, then devise novel features of my own that no existing tool offers
I shall present what I found and what I invented — you will have a chance to add your own ideas or adjust the plan before I build
I shall generate a Go CLI, build every feature from the plan, then verify quality through dogfood, runtime verification, and scoring
What you will have at the end: A fully functional CLI at $PRESS_LIBRARY/<api> that you can use yourself, ship on your own, or apply to add to the printing-press library.
Time: 30-60 minutes depending on API complexity.
Things that help if you have them:
An API key (for live smoke testing at the end)
A logged-in browser session (for discovering authenticated endpoints)
A spec file or HAR capture (skips discovery)
If the user provided --spec, adapt: "You have provided a spec, so I shall skip discovery and proceed directly to analysis and generation. Should be faster."
If the user provided --har, adapt: "You have provided a HAR capture, so I shall generate a spec from your traffic and skip browser browser-sniffing."
Then ask via AskUserQuestion:
question:"Anything you want me to know before I begin? A vision for what this CLI should do, specific features you care about, or auth context I should have?"
header:"Briefing"
multiSelect:false
options:
label:"Let's go (recommended)" — description:"Start research now. I'll ask about API keys, browser auth, or other context when I need them."
label:"I have context to share" — description:"Tell me your vision, specific features, or auth context (API key, logged-in browser session) before research starts."
Do not add additional options — auth is already handled by Phase 0.5 (API Key Gate) and Phase 1.6 (Pre-Browser-Sniff Auth Intelligence) downstream. A user who wants to volunteer auth context can do so via option 2's free-text response. The two options above plus the automatic "Other" field is the entire interface.
If the user picks "Let's go (recommended)", proceed to the Multi-Source Priority Gate (or, for single-source runs, directly to Phase 0).
If the user picks "I have context to share", capture their free-text response as USER_BRIEFING_CONTEXT. The response may include:
Vision / specific features — captured as-is. This context will be:
Added to the Phase 1 Research Brief under a ## User Vision section
Used as a 4th self-brainstorm question in Phase 1.5c.5: "Based on the user's stated vision, what features directly serve their stated goals that the absorbed features don't cover?"
Referenced at the Phase Gate 1.5 absorb gate: "You mentioned [summary] at the start. Want to add more, or does the manifest already cover it?"
Auth context — if the user mentions an API key, env var, or logged-in browser session, set the corresponding AUTH_CONTEXT fields so the API Key Gate (Phase 0.5) and Pre-Browser-Sniff Auth Intelligence (Phase 1.6) do not re-ask.
Multi-Source Priority Gate
After the briefing question resolves, inspect the user's original argument AND any USER_BRIEFING_CONTEXT they provided. If together they name two or more distinct services, sites, or APIs (e.g., "Google Flights and Kayak", "Notion + Linear combo CLI", "flightgoat: Google Flights, Kayak.com/direct, and FlightAware"), this is a combo CLI and priority ordering MUST be confirmed before Phase 1 research.
Why this gate exists: Phase 1 research defaults to the first resolvable spec as the primary source. When the user listed services in a specific order, that order is their intent — but the generator's spec-first bias will silently invert it (picking a well-documented paid API over a free reverse-engineered one the user actually wanted as the headline feature). This has caused real user-visible failures where the CLI shipped with the wrong primary and required a paid API key for what the user intended as the free primary command.
Parse the order from the prose. Use the user's wording verbatim. Commas, "then", "and", explicit "primary/secondary", or numbered lists all signal ordering. If the user wrote "Google Flights, Kayak, FlightAware" — that is the order. Do not reorder by spec availability, tier, or ease of generation.
Confirm via AskUserQuestion:
"You mentioned , , and . I'll treat as the primary — it gets the headline commands, the top of the README, and the first-run experience. Is that the right order?"
Options:
Yes, that order is correct — Proceed with SOURCE_PRIORITY=[A, B, C] captured to run state.
Different order — User provides the correct ordering; capture it.
They're peers, no primary — Rare; capture as equal weighting but warn the user that one will still lead the README.
Write the confirmed ordering to $API_RUN_DIR/source-priority.json:
{"sources":["google-flights","kayak-direct","flightaware"],"confirmed_at":"<ISO timestamp>","raw_user_phrasing":"<verbatim text that established the order>"}
Phase 1 MUST consult this file. When selecting a spec source, the primary source wins even if it has no spec and a later source has a clean OpenAPI. When the primary has no official spec, flag that openly in the brief under ## Source Priority (see template below) and route to the browser-sniff/docs path for the primary — do not promote a secondary source just because its spec is cleaner.
Economics check. If the confirmed primary source is free (no API key required) AND the generator's default path would make the primary CLI commands require a paid key (because the auth applies broadly or because a paid secondary source is bleeding into the primary path), surface the tradeoff explicitly before generating:
"The primary source () is free, but the default path would require a for the headline commands because . Options: (1) keep primary free, gate only the secondary commands on the paid key; (2) require the paid key for everything; (3) drop the paid source."
Default to option 1 unless the user overrides. Record the decision in source-priority.json under auth_scoping.
Single-source runs: If only one service is named, skip this gate entirely — no ordering to confirm.
Run Initialization
After you know <api> (from the Orientation & Briefing flow above; preflight already ran at the top), initialize the run-scoped artifact paths:
mkdir -p "$PRESS_RUNSTATE/runs"
RUN_ID=""
API_RUN_DIR=""for attempt in 1 2 3 4 5; do
RUN_SUFFIX="$(LC_ALL=C tr -dc 'a-f0-9' </dev/urandom 2>/dev/null | head -c 8 || true)"if [ -z "$RUN_SUFFIX" ]; then
RUN_SUFFIX="pid$$-$attempt"fi
CANDIDATE_RUN_ID="$(date +%Y%m%d-%H%M%S)-$RUN_SUFFIX"
CANDIDATE_RUN_DIR="$PRESS_RUNSTATE/runs/$CANDIDATE_RUN_ID"ifmkdir"$CANDIDATE_RUN_DIR" 2>/dev/null; then
RUN_ID="$CANDIDATE_RUN_ID"
API_RUN_DIR="$CANDIDATE_RUN_DIR"breakfidoneif [ -z "$RUN_ID" ]; thenecho"could not allocate a unique run directory under $PRESS_RUNSTATE/runs" >&2
exit 1
fi
RESEARCH_DIR="$API_RUN_DIR/research"
PROOFS_DIR="$API_RUN_DIR/proofs"
PIPELINE_DIR="$API_RUN_DIR/pipeline"
DISCOVERY_DIR="$API_RUN_DIR/discovery"
CLI_WORK_DIR="$API_RUN_DIR/working/<api>-pp-cli"
STAMP="$(date +%Y-%m-%d-%H%M%S)"# Session state (live cookies, CSRF tokens captured during authenticated# browser-sniff) lives OUTSIDE $API_RUN_DIR so the Phase 5.5 archive# `cp -r "$DISCOVERY_DIR"` cannot pick it up. Containment by location, not by# manual rm-before-archive.## Base prefix is user-scoped (`printing-press-$(id -u)`) so that on a Linux# host with a shared /tmp, the umask-077 subshell below does not lock the# top-level `printing-press` directory to a single user. macOS already gives# us a per-user $TMPDIR; the $(id -u) suffix keeps semantics identical there.
SESSION_BASE="${TMPDIR:-/tmp}/printing-press-$(id -u)"
SESSION_DIR="$SESSION_BASE/session/$RUN_ID"
SESSION_STATE_FILE="$SESSION_DIR/session-state.json"mkdir -p "$RESEARCH_DIR""$PROOFS_DIR""$PIPELINE_DIR""$CLI_WORK_DIR"# Create $SESSION_DIR inside a subshell with a tight umask so it lands at 0700# at creation, not after a follow-up chmod. The two-step `mkdir; chmod` form# leaves a TOCTOU window where a concurrent process could open the directory# (and any session-state.json written into it) while perms are still# umask-derived (typically 0755 on Linux). The umask propagates to every# directory `mkdir -p` creates; the user-scoped $SESSION_BASE above is what# keeps that from blocking other users on the same host.
(umask 077 && mkdir -p "$SESSION_DIR")
STATE_FILE="$API_RUN_DIR/state.json"
Maintain a lightweight state file at $STATE_FILE so /printing-press-score can rediscover the current run. It should always contain:
{"api_name":"<api>","run_id":"$RUN_ID","working_dir":"$CLI_WORK_DIR","output_dir":"$CLI_WORK_DIR","spec_path":"<absolute spec path if known>"}
run_id is the unique value allocated above from the wall-clock stamp plus a short random suffix. mkdir "$CANDIDATE_RUN_DIR" is the collision guard: if another run already owns a candidate directory, allocate another ID instead of reusing the directory. Persisting this value in state.json makes the state file the source of truth for generate, dogfood acceptance, promote, /printing-press-score, and future state-loading consumers. Without run_id in either state or legacy path fallback, cli-printing-press dogfood --live --write-acceptance refuses to write the gate marker.
Do not create a go.work file in $CLI_WORK_DIR. Generated modules must build and test as standalone modules; a mismatched workspace go directive can break Go 1.25+ toolchains and lefthook checks. Editor/gopls workspace noise is cosmetic and must not be traded for broken go build or go test.
There are exactly three durable writable locations. Every generated artifact this
skill preserves goes to one of them:
$PRESS_RUNSTATE/ — mutable working state for the current run (research, proofs, pipeline artifacts, plans, intermediate docs)
$PRESS_LIBRARY/ — published CLIs (<api-slug>/ subdirectories)
$PRESS_MANUSCRIPTS/ — archived run evidence (research, proofs, discovery)
Short-lived command captures may use /tmp/printing-press/ with unique mktemp
paths and must be deleted after use.
Examples of the current naming/layout:
$PRESS_LIBRARY/notion/ — published CLI directory (keyed by API slug)
notion-pp-cli — the binary name inside the directory
/printing-press emboss notion — emboss accepts both slug and CLI name
discord-pp-cli/internal/store/store.go — internal source paths still use CLI name
linear-pp-cli stale --days 30 --team ENG — binary invocations use CLI name
github.com/mvanhorn/discord-pp-cli — Go module paths use CLI name
Outputs
Every run writes up to 5 concise artifacts under the current managed run and archives them to $PRESS_MANUSCRIPTS/<api-slug>/<run-id>/:
proofs/<stamp>-fix-<api>-pp-cli-live-smoke.md (only if live testing runs)
These do not need to be 200+ lines. Keep them dense, evidence-backed, and directly useful.
Phase 0: Resolve And Reuse
Before new research:
Resolve the spec source.
Local physical device detection. If the user's target is a local Bluetooth/BLE-controlled physical device (for example an appliance, toy, light, sensor, exercise machine, lock, or other device controlled from a phone app over Bluetooth), do not route it through browser-sniff as the primary discovery path. Read and apply references/device-sniff-ble.md. Use device-sniff ble for normalized BLE evidence and bluetooth-sniff as the discoverable alias. Community libraries, docs, Android logs, Wireshark/nRF captures, and manual action journals are evidence inputs; they are not a reason to hardcode a vendor-specific generator path.
BLE mapping research gate. A BLE scan/inspect/read/subscribe pass only discovers identity, services, characteristics, and telemetry candidates; it does not by itself discover what write payloads mean. Before generating callable control commands or running any live write, establish a command mapping from at least one concrete source: user-provided mapping, official docs, community protocol/library code, Android/iOS/Bluetooth logs, Wireshark/nRF captures, or an operator action journal that correlates a real user action with observed writes. If no mapping source is found, generate only read/status/capability metadata or stop and ask the user for mapping evidence. Do not invent mutating payloads or brute-force probe a physical device.
URL Detection — If the argument contains ://, it's a URL. Determine whether it's a spec or a website before proceeding.
Step 1: Content probe. Fetch the URL with the raw docs helper from references/fetch-docs.md and inspect the response status, Content-Type, and first few lines of the returned file:
Check the Content-Type header and the first few lines of the body.
If the fetch fails (timeout, 404, DNS error), record the exact status/error, then skip to Step 2 — treat it as a website.
If the content starts with openapi:, swagger:, or is valid JSON containing an "openapi" or "swagger" key → it's a spec. Treat as --spec and proceed directly. No disambiguation needed.
If the content is a HAR file (JSON with "log" and "entries" keys) → treat as --har and proceed directly.
Step 2: Disambiguation. If the content is HTML or the probe failed, ask the user what they want. Extract the site name from the hostname (e.g., postman.com → "Postman", app.linear.app → "Linear"). Derive <api> from the site name using the same cleanSpecName normalization the generator uses.
Use AskUserQuestion with:
question:"What kind of CLI do you want for <SiteName>?"
header:"CLI target"
multiSelect:false
options:
label:"<SiteName>'s official API" — description:"Build a CLI for <SiteName>'s documented API (e.g. REST endpoints, webhooks, OAuth)"
label:"The <SiteName> website itself" — description:"Build from the website itself — I may open or attach to Chrome during generation to capture site traffic, then generate a lightweight CLI from replayable HTTP/HTML surfaces"
The user can also pick the automatic "Other" option to describe what they're after in free text.
Routing after disambiguation:
"'s official API" → use <api> as the argument, proceed with normal discovery (Phase 1 research, then Phase 1.7 browser-sniff gate evaluates independently as usual)
"The website itself" → use <api> as the argument, set BROWSER_SNIFF_TARGET_URL=<url>. Proceed to Phase 1 research. When Phase 1.7 is reached, skip the browser-sniff gate decision and go directly to "If user approves browser-sniff" (the user already approved temporary browser discovery in Phase 0 — do not re-ask). Use BROWSER_SNIFF_TARGET_URL as the starting URL for browser capture. The printed CLI must still use a replayable runtime surface; do not ship a resident browser transport.
"Other" → read the user's free-form response and adapt
End of URL detection. The remaining spec resolution rules apply when the argument is NOT a URL:
If the user passed --har <path>, this is a HAR-first run. Before invoking browser-sniff, parse the file as JSON and verify it contains traffic: HAR files must have .log.entries | length > 0; enriched capture JSON must have .entries | length > 0. If the file is missing, invalid JSON, or has zero entries, do not continue silently. Print HAR/capture contains no network entries; the export likely recorded no traffic. and ask via AskUserQuestion with exactly these choices: Capture again (Recommended) — "Re-export a HAR after recording a real user flow"; Proceed with docs-only — "Skip HAR/browser-sniff and continue from docs/spec discovery only"; HOLD — "Stop this run until a valid capture is available." Only proceed to browser-sniff when the parsed entry count is non-zero.
For a valid --har <path>, run cli-printing-press browser-sniff --har <path> --name <api> --output "$RESEARCH_DIR/<api>-browser-sniff-spec.yaml" --analysis-output "$DISCOVERY_DIR/traffic-analysis.json" to generate a spec and traffic analysis from captured traffic. If $API_RUN_DIR/source-priority.json exists with two or more sources, add --preserve-hosts so combo-CLI captures retain peer API hosts with per-endpoint base_url overrides instead of collapsing them into secondary evidence. Immediately inspect $DISCOVERY_DIR/traffic-analysis.json: if it contains the empty_response_shapes warning, or if every endpoint cluster has size_class: "empty" and response_shape: {}, use curl/direct HTTP to call each discovered endpoint and capture response structure before writing or trusting the spec. Do not proceed to generate with a sniffed spec that has no type information. Use the generated spec as the primary spec source for the rest of the pipeline only after this quality check passes. Skip the browser-sniff gate in Phase 1.7 (browser-sniff already ran).
If the user passed --spec, use it directly (existing behavior).
Otherwise, proceed with normal discovery (KnownSpecs, apis-guru, web search, and researched docs).
Directory spec-source guard
If any resolved spec source is a local directory, do not pass the directory
itself to cli-printing-press generate and do not silently pick the first
file. Enumerate candidate specs first:
Keep only files whose head looks like an OpenAPI or Swagger root document
(openapi:, swagger:, or JSON with a top-level "openapi" or "swagger"
key). Ignore unrelated JSON/YAML config files.
When the filtered candidate list is empty, abort with:
No OpenAPI/Swagger spec found under <directory>. Pass --spec <file> directly.
Do not continue with the raw directory as the spec source.
When the directory contains exactly one candidate, use that file as the
spec source and write it to state.json as spec_path.
When the directory contains more than one candidate:
Print a prominent warning before generation:
N OpenAPI/Swagger specs found under <directory>; no single file represents the whole API surface.
List every candidate when N <= 20; otherwise list the first 20 sorted
paths and print ...and N-20 more.
Record the directory and candidates in $STATE_FILE before continuing:
spec_path is the directory and spec_candidates is the sorted list.
Ask the user to choose one spec, several specs, or all specs. If this
runtime cannot ask a blocking question, stop after printing the warning
and tell the user to re-run with explicit --spec <file> arguments. This
is the minimum safe floor: never let a directory run finish while hiding
that additional specs were ignored.
After the user confirms the selection, update $STATE_FILE with
selected_spec_paths set to the list that will be generated.
For multiple selected specs, default to one independent printed CLI per
spec using a derived <api>-<spec-slug> name and a distinct working
directory under $API_RUN_DIR/working/. Do not merge all selected specs
into one CLI unless the user explicitly asks for a combined surface and
provides the umbrella name for --name.
Check for prior research in:
$PRESS_MANUSCRIPTS/<api-slug>/*/research/*
Reuse good prior work instead of redoing it.
Library Check — Check if a CLI for this API already exists in the library or is actively being built, and present the user with context and options.
First, check lock status to detect active builds:
LOCK_STATUS=$("$PRINTING_PRESS_BIN" lock status --cli <api>-pp-cli --json 2>/dev/null)
LOCK_HELD=$(echo"$LOCK_STATUS" | grep -o '"held"[[:space:]]*:[[:space:]]*[a-z]*' | head -1 | sed 's/.*: *//')
LOCK_STALE=$(echo"$LOCK_STATUS" | grep -o '"stale"[[:space:]]*:[[:space:]]*[a-z]*' | head -1 | sed 's/.*: *//')
LOCK_PHASE=$(echo"$LOCK_STATUS" | grep -o '"phase"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"phase"[[:space:]]*:[[:space:]]*"//;s/"//')
LOCK_AGE=$(echo"$LOCK_STATUS" | grep -o '"age_seconds"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*: *//')
Then check the library directory:
CLI_DIR="$PRESS_LIBRARY/<api>"
HAS_LIBRARY=false
HAS_GOMOD=false
CLI_RELEASE_VERSION=""
PRIOR_STEINBERGER_SCORE=""
PRIOR_SUB60_REPRINT=falseif [ -d "$CLI_DIR" ]; then
HAS_LIBRARY=trueif [ -f "$CLI_DIR/go.mod" ]; then
HAS_GOMOD=truefi# Read manifest if available
MANIFEST="$CLI_DIR/.printing-press.json"if [ -f "$MANIFEST" ]; then
PRESS_VERSION=$(cat"$MANIFEST" | grep -o '"printing_press_version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"printing_press_version"[[:space:]]*:[[:space:]]*"//;s/"//')
GENERATED_AT=$(cat"$MANIFEST" | grep -o '"generated_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"generated_at"[[:space:]]*:[[:space:]]*"//;s/"//')
PRIOR_STEINBERGER_SCORE=$(jq -r '.scorecard.steinberger.percentage // empty'"$MANIFEST" 2>/dev/null || true)
if [ -n "$PRIOR_STEINBERGER_SCORE" ] && awk "BEGIN { exit !($PRIOR_STEINBERGER_SCORE < 60) }"; then
PRIOR_SUB60_REPRINT=truefifi
RELEASE_MANIFEST="$CLI_DIR/.printing-press-release.json"if [ -f "$RELEASE_MANIFEST" ]; then
CLI_RELEASE_VERSION=$(jq -r '.version // empty'"$RELEASE_MANIFEST" 2>/dev/null || true)
fi# Get directory modification time as fallback
CLI_MTIME=$(stat -f "%Sm" -t "%Y-%m-%d""$CLI_DIR" 2>/dev/null || stat -c "%y""$CLI_DIR" 2>/dev/null | cut -d' ' -f1)
fi
Decision matrix:
Library dir?
Lock?
Stale?
Has go.mod?
Action
No
No
N/A
N/A
Proceed normally
No
Yes
No
N/A
Warn: "Actively being built (phase: <phase>, <age> seconds ago). Wait, use a different name, or pick a different API."
No
Yes
Yes
N/A
Offer reclaim: "Interrupted build detected (stale since <age>s ago). Reclaim and start fresh?"
Yes
No
N/A
Yes
Existing "Found existing" flow (see below)
Yes
No
N/A
No
Debris: "Found <api> directory in library but it appears incomplete (no go.mod). Clean up and start fresh?" If user approves, rm -rf "$CLI_DIR" and proceed normally.
Yes
Yes
No
Any
Warn: "Actively being rebuilt (phase: <phase>, <age> seconds ago). Wait, use a different name, or pick a different API."
Yes
Yes
Yes
Any
Offer reclaim: "Interrupted rebuild detected (stale since <age>s ago). Reclaim and start fresh?"
If actively locked (not stale): Present via AskUserQuestion with options to wait, pick a different API, or force-reclaim (cli-printing-press lock acquire --cli <api>-pp-cli --scope "$PRESS_SCOPE" --force).
If stale lock: Reclaiming is automatic on lock acquire in Phase 2. If user approves, proceed normally — the lock acquire in Phase 2 will auto-reclaim the stale lock.
If library exists with go.mod and no lock (completed CLI): Display context and present options using AskUserQuestion:
Found existing <api> in library (last modified <date>).
If PRESS_VERSION is available, append: Built with printing-press v<version>.
If CLI_RELEASE_VERSION is available, append: Published CLI release: <version>. This is the public-library per-CLI CalVer release, not the generator version; do not use it for the generator staleness comparison below.
If PRIOR_SUB60_REPRINT=true, append: Prior Steinberger score: <score>%. Reprint will require all approved transcendence rows to ship unless you explicitly accept partial coverage.
If prior research was also found (step 2), include the research summary alongside the library info.
Then ask:
"Generate a fresh CLI" — Re-runs the Printing Press into a working directory, overwrites generated code, then rebuilds transcendence features. Prior research is reused if recent.
"Improve existing CLI" — Keeps all current code, audits for quality gaps, implements top improvements. The Printing Press is not re-run.
"Review prior research first" — Show the full research brief and absorb manifest before deciding.
If the user picks option 1, proceed to Phase 1 (research) and then Phase 2 (generate) as normal.
If the user picks option 2, invoke /printing-press-polish <api> to improve the existing CLI.
If the user picks option 3, display the prior research, then re-present options 1 and 2.
MANDATORY when re-using prior research after a binary upgrade. If the user picks "Generate a fresh CLI" (option 1) AND PRESS_VERSION from the manifest differs from the current binary's version (parse both via semver and compare; only fire when the leading minor or major segment changed — patch-level deltas don't trigger this), prompt the user once before kicking off Phase 1 research.
Construct the prompt's "what changed" list from these category buckets — the categories are stable across versions; the specific machine deltas inside each category are not. Read docs/CHANGELOG.md (or run git log --oneline v<PRESS_VERSION>..v<CURRENT> -- internal/) and tag each notable change to one of these buckets:
Category
Affects prior-brief assumption about...
Transport / reachability
Which sources are reachable, what auth/clearance is needed, which clients (stdlib, Surf, browser-clearance) the brief assumed
Scoring rubrics
What Phase 1.5/scorecard dimensions the brief targets, whether prior "high-priority" features still rank as such
Auth modes
Whether brief's auth choice (api-key, cookie, composed, oauth) is still the right pick, whether new modes unlock new endpoints
MCP surface
Whether brief's MCP shape (endpoint-mirror vs intent vs code-orchestration) matches the latest emit defaults
Discovery
Whether browser-sniff / crowd-sniff workflows changed, whether prior gate decisions are still valid
For the prompt itself, list only the buckets that have at least one notable change between the two versions. If the CHANGELOG / git log is unavailable, list all five buckets generically and let the user decide.
"The prior <api> was generated with printing-press v<PRESS_VERSION>. The current binary is v<CURRENT>. Categories where the machine has changed since then: <applicable buckets>. Each can invalidate prior research assumptions. Re-validate the prior brief against the current machine before reusing it?"
Options:
Yes, re-validate the prior research — fold the validation into Phase 1 (briefly re-probe reachability for previously-blocked sources, confirm scoring still classifies the prior CLI's pattern correctly, etc.) before reusing the brief.
No, reuse the prior research as-is — proceed with the brief verbatim, even if the underlying machine assumptions are stale.
The prompt forces the user to acknowledge the version delta and explicitly accept (or refuse) re-validation. Skip it entirely on first generation, on same-version regenerations, or when no prior manifest exists.
If no CLI exists in the local library and no lock is active, run the Public-library check below before proceeding to Phase 1.
The local library check above only sees CLIs this machine has already printed. A user on a fresh checkout — or one who typed a slightly different name than the published slug (Slack vs slack-bot, Cal vs cal-com), or who described what they wanted in their own words (Hacker News reader, Notion clone, prediction market) — will miss CLIs that already exist in the public library. Scan mvanhorn/printing-press-library/registry.json to catch those cases before Phase 1 research begins (the expensive 30-60-minute portion of the pipeline).
The public library also carries blocked-apis.json, a shared journal of APIs that were attempted and put on hold for reachability or buildability reasons. Scan it in the same Phase 0 window so a user does not repeat an already-known dead-end run before the blocking issue is fixed.
Skip this check entirely when:
The local-library check above already prompted (mutual exclusion — do not double-ask).
BROWSER_SNIFF_TARGET_URL is set (the user is building a from-website CLI; the registry indexes API CLIs and naming collisions are unlikely and intentional).
The user passed --har <path> with an explicit --name <api> for a private capture.
Fetch the registry and blocked journal. Match the pattern /printing-press-import and /printing-press-reprint already use:
REGISTRY=$(mktemp)
if ! gh api -H "Accept: application/vnd.github.v3.raw" \
repos/mvanhorn/printing-press-library/contents/registry.json \
> "$REGISTRY" 2>/dev/null; thenecho"Public-library check failed: registry.json is unreachable. Stop here instead of treating the API as unpublished; retry when GitHub/library access is available."rm -f "$REGISTRY"exit 1
fi
BLOCKED_APIS=$(mktemp)
if ! gh api -H "Accept: application/vnd.github.v3.raw" \
repos/mvanhorn/printing-press-library/contents/blocked-apis.json \
> "$BLOCKED_APIS" 2>/dev/null; thenecho"Blocked-API journal check skipped: blocked-apis.json unreachable or absent. Proceeding with the registry check."rm -f "$BLOCKED_APIS"
BLOCKED_APIS=""fi
Do not continue past a registry fetch/parse failure: it can hide an already published nested CLI and route a duplicate greenfield build. Network failure or missing blocked-apis.json is still non-blocking. After step 4 finishes, clean up tempfiles only if the fetch succeeded: [ -n "$REGISTRY" ] && rm -f "$REGISTRY" and [ -n "$BLOCKED_APIS" ] && rm -f "$BLOCKED_APIS". The blocked-journal failure branch above already removed its file and set its variable to empty, so an unconditional rm -f "$BLOCKED_APIS" would run rm -f "".
Read the blocked journal before reasoning about registry matches. If BLOCKED_APIS is non-empty, read it directly. Expected shape:
[{"slug":"1001tracklists","attempted_at":"2026-05-25","verdict":"hold","reason":"Cloudflare Turnstile clearance gate; pure-HTTP cannot mint fsuid","blocking_issue":2140,"permanent":false}]
Entries are API-slug records, not printed-CLI registry entries. Match the user's requested API against slug using the same slug-normalization judgment as the registry check (<api>, <api>-cli, <api>-pp-cli, punctuation and case variants). Do not use vague category or description matching for the blocked journal. A false positive here stops a potentially valid run; only prompt when the entry appears to be the same API under a slug or brand spelling variant.
If a blocked entry matches, prompt before reading registry matches:
"<entry.slug> was attempted on <entry.attempted_at> and held — <entry.reason>. The shared blocked-API journal exists so users do not repeat known unreachable or unbuildable runs before the blocker changes. Proceed anyway?"
Where <tracking suffix> is:
(tracking #<entry.blocking_issue>; marked permanent) when blocking_issue is non-null and permanent is true.
(tracking #<entry.blocking_issue>) when blocking_issue is non-null and permanent is false.
(marked permanent) when permanent is true and blocking_issue is null.
empty when neither applies.
Options:
Stop here (recommended) — end this run. If a tracking issue is present, tell the user to re-attempt only after that issue closes or the journal entry is updated.
Proceed anyway — continue to the registry check and then Phase 1. Use this only when the user has new evidence that the blocker no longer applies or wants a deliberate fresh attempt.
If multiple blocked entries somehow match, pick the most recent attempted_at value and mention that additional older journal entries exist. If blocked-apis.json is malformed, print "Blocked-API journal check skipped: blocked-apis.json is malformed. Proceeding with the registry check." and continue; do not let a bad journal file block fresh prints.
Read the registry and reason about matches — do not gate on string equality alone. The file is small (~88 KB, ~135 entries today); read it directly and use judgment. Each entry has fields name (slug), category, api (brand display), description, path, printer.
The user's argument may arrive in many shapes, and only some are catchable by deterministic match:
Slug or near-slug — Notion, notion-cli, notion-pp-cli
Brand with punctuation — Cal.com, Customer.io, Archive.today, Trigger.dev
Adjacent product — Polymarket when the registry has kalshi (peer prediction market)
Genuinely novel — no useful overlap
Classify the best match at three confidence levels and act only on the top two:
High — same product under a different name (slug variant, brand vs slug form, -cli/-pp-cli suffix variant, well-known alias). Examples: Cal.com ↔ cal-com, Notion ↔ notion-cli, slack ↔ slack-bot.
Medium — same category and overlapping function; a reasonable user would want to know before building. Examples: prediction market finds kalshi, Hacker News reader finds hackernews, Polymarket surfaces kalshi as a peer.
Low — vaguely adjacent (e.g. "payment gateway" finding every payment-related CLI). Skip silently — false-positive prompts get dismissed reflexively at this gate.
Resist over-matching on description keywords. Most descriptions mention several adjacent concepts; matching liberally on description text produces noise. Use the description to confirm a name-or-category candidate, not to discover candidates from scratch.
Combo CLIs. When SOURCE_PRIORITY is set (from the Multi-Source Priority Gate above), skip the single-source High/Medium/No-match branches below. Classify matches per source, then present a single combined prompt rather than asking N times. For combo runs the existing single-source CLIs are usually informational — the user came here to build a combo, so the recommended default is to continue with the combo rather than reprint a component standalone.
Cap displayed reprint options at 2 across all sources combined so the prompt fits the 4-option AskUserQuestion limit (2 reprints + continue + abort). Pick the 2 best candidates by judgment in this order: (1) High over Medium, (2) primary-source over secondary-source (the first entry in SOURCE_PRIORITY wins ties), (3) canonical slug over variant. If additional matches exist beyond the displayed 2, append "(plus N other source matches)" to the prompt body so the user knows the list is truncated. Omit sources with no match rather than listing them as empty rows. If no source has any match at High or Medium, print nothing and proceed to Phase 1.
Found matches across the sources you listed:
<source1>: <entry1.name> (<entry1.api>) [High] — same product as <source1>
This is informational — these components already exist as single-source CLIs. Continue building the combo, switch to reprinting one standalone, or abort?
Options:
Continue with the combo as planned (recommended) — the combo itself is the value-add; proceed to Phase 1 with all sources.
Reprint <entry1.name> standalone instead — invoke /printing-press-reprint <entry1.name> (abandons the combo for now).
Reprint <entry2.name> standalone instead — same, for the second candidate.
Abort — stop here.
The [High] / [Medium] tags surface the confidence so the user can distinguish "this is literally the thing you named" from "this is adjacent." Tag in the bullet, not the option label, to keep options scannable.
Single-source CLIs. When SOURCE_PRIORITY is not set, use the branches below.
High match — prompt strongly. Under Claude Code, use AskUserQuestion; under another harness, use the equivalent native prompt primitive. The option set is the same either way.
Found <entry.api> in the public library (printed by @<entry.printer>, path <entry.path>).
This CLI already exists. What would you like to do?
Options:
Reprint with the current Printing Press (recommended) — end this run and invoke /printing-press-reprint <entry.name>. That skill pulls the existing CLI, carries prior research and post-publish patches into reconciliation, and regenerates under the current binary. Almost always the right choice when a user discovers the CLI exists.
Continue and build a fresh one anyway — proceed with the current run from scratch. Rare; appropriate only for a deliberate fork or variant.
Abort — stop here.
Multiple High matches — present each candidate, do not use the Medium-match phrasing. Rare — typically only happens when the user's argument is ambiguous between siblings like slack and slack-bot. Cap displayed candidates at 2 to stay within the 4-option prompt limit alongside continue/abort. If 3+ High candidates somehow qualify, pick the 2 best by judgment (typically the canonical slug match plus the next-most-likely alternative) and note "(plus N other close matches)" in the prompt body so the user knows the list is truncated.
Found multiple matches for <api> in the public library — each appears to be the same product under a different name:
Reprint <entry2.name> instead — same, for the second candidate.
Abort — stop here.
No High or Medium match: print nothing, proceed to Phase 1.
API Key Gate — Check whether this API requires authentication, then handle accordingly.
First, determine if the API needs auth. Use these signals:
The spec has no security or securityDefinitions section → likely no auth needed
The API's endpoints are accessible without authentication (e.g., ESPN's undocumented endpoints, weather APIs, public data feeds) — note: "no auth required" does NOT mean the service has an official public API
No env var matching the API name exists AND no known token pattern applies
Community docs or npm/PyPI wrappers describe the API as "no auth required"
If no auth is required, skip the key gate entirely. Proceed with: "No authentication required for <API> — skipping API key gate." Do NOT call it "a public API" unless the service officially publishes one. Many services (ESPN, etc.) have unauthenticated endpoints without having an official API. Live smoke testing in Phase 5 will work without a key.
If the API DOES require auth, run the key gate:
Token detection order:
GitHub: GITHUB_TOKEN, GH_TOKEN, or gh auth token
Discord: DISCORD_TOKEN, DISCORD_BOT_TOKEN
Linear: LINEAR_API_KEY
Notion: NOTION_TOKEN
Stripe: STRIPE_SECRET_KEY
Generic: API_KEY, API_TOKEN
If a token IS found, stop and explain:
Found <ENV_VAR> in your environment. This key will be used only for read-only live smoke testing in Phase 5 — listing, fetching, and health checks. It will never be used for write operations (create, update, delete). OK to use it?
If the user approves → proceed with the key available for Phase 5.
If the user declines → proceed without the key and display: "Live smoke testing (Phase 5) will be skipped. The CLI will still be generated and verified against mock responses."
If no token is found, stop and ask:
No API key detected for <API>. You can provide one now for read-only live smoke testing in Phase 5, or continue without it.
Set it with export <ENV_VAR>=<your-key> or paste the key here.
If the user provides a key → proceed with the key available for Phase 5.
If the user declines → proceed without the key and display: "Live smoke testing (Phase 5) will be skipped. The CLI will still be generated and verified against mock responses."
Resolve the API key gate (or skip it for public APIs) before moving to Phase 1.
Phase 1: Research Brief
When BROWSER_SNIFF_TARGET_URL is set: Skip spec/docs search and SDK wrapper search — none of these exist for an undocumented website feature. Focus research on understanding what the site/feature does, who uses it, what workflows it supports, and what competitors offer similar functionality. The spec will come from browser-sniffing in Phase 1.7.
Before reading documentation, read references/fetch-docs.md. Use fetch-docs.sh for the API's primary docs, OpenAPI/Postman links, auth guides, error handling, rate limits, pagination, webhooks, and any per-endpoint reference page. Preserve exact status codes and inspect the returned local file directly so enum values, field constraints, casing, examples, and nav/link variants are not lost through summarization.
There is no built-in catalog lookup in this repo. New CLIs start from researched specs, documentation, browser/crowd/device discovery, or a reprint of an existing local/public-library artifact. If the target already exists in the public library, use the reprint/publish workflow rather than adding source metadata here.
Write one build-driving brief, not a stack of phase essays.
The brief must answer:
What is this API actually used for?
What are the top 3-5 power-user workflows?
What are the top table-stakes competitor features?
What data deserves a local store?
Why would someone install this CLI instead of the incumbent?
What is the product name and thesis?
Research checklist:
Find the spec or docs source. For docs pages whose details affect generation, fetch the raw page with fetch-docs.sh, then read/grep the returned path directly.
Find the top 1-2 competitors
Check GitHub issues on the top wrapper/SDK repo for "403", "blocked", "broken", "deprecated", "rate limit". If multiple issues report the API is inaccessible or broken, flag this in the research brief as a reachability risk. This is critical for unofficial/reverse-engineered APIs.
Find official and popular SDK wrappers on npm (site:npmjs.com) and PyPI (site:pypi.org)
Find 2-3 concrete user pain points
Identify the highest-gravity entities
Pick the top 3-5 commands that matter most
Do not produce separate mandatory documents for:
workflow ideation
parity audit
data-layer prediction
product thesis
Put them in the one brief.
Write:
$RESEARCH_DIR/<stamp>-feat-<api>-pp-cli-brief.md
Suggested shape:
# <API> CLI Brief## API Identity- Domain:
- Users:
- Data profile:
## Reachability Risk- [None / Low / High] [evidence: e.g., "6 open issues on reteps/redfin about 403 errors since 2025"]
- Tier/permission hints from 4xx body: [omit when absent; otherwise quote the matched bounded line(s) from Phase 1.9]
- Probe-safe endpoint used: [omit when absent; otherwise "<METHOD><path>" from `x-pp-safe-probe`]
## Top Workflows1. ...
## Table Stakes- ...
## Data Layer- Primary entities:
- Sync cursor:
- FTS/search:
## Codebase Intelligence- [DeepWiki findings if available, otherwise omit this section]
- Source: DeepWiki analysis of {owner}/{repo}
- Auth: [token type, header, env var pattern]
- Data model: [primary entities and relationships]
- Rate limiting: [limits and behavior]
- Architecture: [key insight about internal design]
## User Vision- [USER_BRIEFING_CONTEXT if provided, otherwise omit this section]
## Source Priority- [Only present for combo CLIs. Copy the confirmed ordering from `source-priority.json`.]
- Primary: <SourceA> — [spec state: official / community-wrapper / no-spec-browser-sniff-required] — [auth: free / paid]
- Secondary: <SourceB> — [...]
- Tertiary: <SourceC> — [...]
-**Economics:** [e.g., "Primary is free; paid key for <SourceB> is scoped to its own commands only."]
-**Inversion risk:** [e.g., "Primary has no OpenAPI; secondary has 53-endpoint spec. Do NOT let spec completeness invert the ordering."]
## Product Thesis- Name:
- Why it should exist:
## Build Priorities1. ...
2. ...
3. ...