gqr CLI guide

Last updated: 2026-10-31

gqr creates listings, uploads photos, prints QR stickers and, if you want, drafts listings from photos with Claude. It talks to the public API with a personal access token.

Overview

The CLI is a thin wrapper over the Node SDK. Photos are converted from HEIC, resized and stripped of EXIF and GPS data on your machine before any byte is uploaded, and the server checks again before a photo goes live. If you would rather drive the same API from Claude Code or Claude Desktop, see the MCP server guide.

Install

The CLI ships inside the garage-qr repository and is built from a checkout. It needs Node 20 or newer. Native image dependencies are installed by pnpm; workspace packages are bundled into a single file.

pnpm install
pnpm --filter @garage-qr/cli build
node apps/cli/dist/gqr.js --help

# optional: put gqr on your PATH
cd apps/cli && pnpm link --global
gqr --help

Sign in

  1. Open your account page and create a personal access token with the scopes listings:read, listings:write and photos:write. The token is shown once.
  2. Run gqr login and paste it at the hidden prompt. The CLI checks the format, verifies it against the API, stores it and prints your email.
gqr login
gqr whoami [--json]
gqr logout

The token is never accepted as a command line argument, so it cannot end up in shell history. Resolution order:

  • Token: the GQR_TOKEN environment variable, then ~/.config/gqr/credentials.json.
  • API base URL: --api-url, then GQR_API_URL, then the value stored at login, then https://api.garageqr.app.
  • Public site URL for printed links: GQR_WEB_URL, default https://garageqr.app.

Listings

gqr list [--status active|sold|reserved] [--json]
gqr show <id> [--json]
gqr sold <id>
gqr reserved <id>
gqr active <id>

Create a listing

gqr new IMG_1.heic IMG_2.heic \
  --title "Trek road bike" --price 120 \
  --condition good --category sports_outdoors \
  [--description "..."] [--original-price 450] [--original-url https://...]
  • --price is in dollars (45, 45.5 or $45.00) and is stored as cents.
  • --condition: new, like_new, good, fair, for_parts.
  • --category: furniture, electronics, clothing, books_media, tools, toys_games, kitchen, sports_outdoors, home_garden, kids_baby, vehicles_parts, other.
  • Missing required fields (title, price, condition, category) are prompted for. With --no-input, or when stdin is not a terminal, the command fails and lists the missing flags.
  • Photos are uploaded in argument order and the first one is the cover. One progress line per file goes to stderr.
  • On success it prints the listing URL and the permanent QR URL.

Add photos later:

gqr photos add <id> more1.jpg more2.jpg

QR stickers

gqr qr <id>                      # print the QR URL
gqr qr <id> --pdf sticker.pdf    # single US-Letter sticker
gqr qr <id> --svg qr.svg
gqr qr --sheet 01H... 01J... sheet.pdf          # multi-up sheet for these listings
gqr qr --sheet --since 24h sheet.pdf            # listings created in the last 24 hours
gqr qr --sheet --from drafts.yaml sheet.pdf     # one sticker per published draft entry

PDFs are generated on your machine. Every sticker points at a permanent opaque URL, so editing the listing afterwards never requires a reprint.

AI drafting

export ANTHROPIC_API_KEY=sk-ant-...
gqr new IMG_1.heic IMG_2.heic --ai
  • The key is read only from ANTHROPIC_API_KEY and is never stored.
  • Up to 4 photos are downscaled to at most 1024 px, with EXIF already stripped, and sent in one request. The model must answer through a single tool whose schema is the shared listing draft schema. The answer is validated; on failure the CLI retries once with the issues attached and then gives up. Free text is never scraped.
  • Default model claude-sonnet-5-5, override with --model.
  • Cost guard: --max-cost in USD, default 0.10. The CLI estimates the input cost before the call and aborts above the limit, then prints the actual cost from the response.
  • You always review the draft: a table of field, value and confidence is shown and you can accept, edit a field or abort. The suggested price is only a suggestion.
  • --yes skips the review but is rejected unless you also pass an explicit --price. --no-input with --ai prints the draft and exits without creating anything.
What the model may fill
FieldModel fills it?
title, description, category, condition, priceYes, each with a confidence.
originalPriceCentsOnly if a price tag or label is visible, otherwise null.
originalProductUrlNever. The schema has no such field.
cover photo, photo orderNever.

Explicit flags always override the draft.

Batch workflow

One subfolder per item. Flat folders are not grouped automatically.

sale/
  bike/   1.heic 2.heic
  lamp/   IMG_0100.jpg
  1. Draft: gqr import sale --ai --out drafts.yaml. Add --max-cost per item and --max-total-cost for the run. Nothing is published yet.
  2. Edit the drafts file. This is the review checkpoint: check every price.
  3. Publish: gqr publish drafts.yaml. Use --dry-run to validate only and --yes to skip the single confirmation.
  4. Stickers: one multi-up sheet for everything in the file.
gqr import sale --ai --out drafts.yaml
# edit drafts.yaml, check every price
gqr publish drafts.yaml [--dry-run] [--yes]
gqr qr --sheet --from drafts.yaml stickers.pdf

Drafts file format

drafts.yaml
version: 1
api_url: https://api.garageqr.app
items:
  - dir: ./sale/bike
    status: pending # pending | published | failed
    photos: [./sale/bike/1.heic, ./sale/bike/2.heic]
    fields:
      title: '...'
      description: '...'
      category: sports_outdoors
      condition: good
      priceCents: 4500
      originalPriceCents: null
    confidence: { title: high, priceCents: low }
    notes: '...'
    result: null # after publish: { id, slug, opaque_id, url, photos: [...] }
    error: null

Paths are relative to the drafts file. Publish validates every pending or failed entry first, then creates each listing, uploads its photos in order and rewrites the file after every step. Published entries are skipped, so re-running resumes. If a run stops after a listing was created but before all photos uploaded, the entry is marked failed with its result kept, and the next run uploads only the missing photos instead of creating a duplicate. Ctrl-C finishes the current entry, saves and exits with code 130.

Rate limits

Creating listings is limited to 10 per 60 seconds per seller. Publish keeps a sliding window and sleeps with a visible countdown when needed. For any 429 the SDK retries briefly, and the CLI then waits out the Retry-After header with a countdown, up to 3 times, before failing. Repeated requests with a bad token from one address are also limited.

Exit codes

Exit codes
CodeMeaning
0Success.
1Generic failure, including a failed publish entry or AI failure.
2Usage error: bad flags, missing options, --yes without --price.
3Authentication or permission error (401, 403, no stored token).
4Validation error in a listing or drafts file.
130Interrupted with Ctrl-C during publish.

Security notes

  • The credentials file is written with mode 0600 in a 0700 directory. gqr logout deletes it.
  • In CI or on shared machines prefer GQR_TOKEN from your secret manager and skip gqr login.
  • Revoke a token on your account page. The last-used column shows whether a leaked token is in use.
  • Photos are stripped of EXIF and GPS before leaving your machine, and the AI only ever sees the stripped, downscaled copies.