Skip to content

agentui-ai/label-ops

v0.1.0MIT

Print labels and receipts that actually scan — ZPL for Zebra label printers, ESC/POS for 58mm Bluetooth thermal printers, and GS1/Code128/Code39 check digits cross-validated against an independent encoder. Ships a deterministic benchmark.


Label and receipt printers are fire-and-forget. A Zebra accepts every byte you send on TCP 9100 and tells you nothing — out of paper, head open, wrong stock, field off the edge, all the same silence. A Bluetooth thermal printer accepts a write and may print half of it. Nothing in either stack reports failure, so every check has to happen before the bytes leave.

CASE                             SKILL  TRAP   EXPECTED           FROM MEMORY
gs1-check-digit-anchoring        ok     yes   {"ean13":"59012341… {"ean13":"5901234123457…
check-digit-verified-by-another  ok     yes   {"accepted":1,"rej… {"accepted":0,"rejected…
code128-checksum-position-base   ok     yes   {"checksum":26}     {"checksum":22}
code39-checksum-excludes-delimi  ok     yes   {"check":"-"}       {"check":"3"}
itf14-odd-digit-count            ok     yes   {"rejected":true}   {"rejected":false,"prin…
zpl-dots-are-not-millimetres     ok     yes   {"at203":800,"at30… {"at203":800,"at300":80…
zpl-control-characters-in-data   ok     yes   {"safe":true}       {"safe":false}
zpl-field-past-the-label-edge    ok     yes   {"flagged":["FIELD… {"flagged":[]}
escpos-text-is-not-utf8          ok     yes   {"bytes":"63 61 66… {"bytes":"63 61 66 c3 a…

Row two is the interesting one. The check digit is not graded against this repo's own opinion — bwip-js is used as an independent oracle: for a UPC-A payload it confirms this implementation's digit is the one bwip-js accepts, and that all nine other digits are refused. The from-memory version scores accepted: 0 — a second encoder disagrees with it.

Two protocols, one repo

DeviceProtocolTransport
ZPLZebra and compatible label printerstext between ^XA and ^XZTCP 9100, Browser Print, OS raw queue
ESC/POS58mm Bluetooth receipt printersbyte commandsWeb Bluetooth

They share nothing but the word "thermal", and mixing them up is the first mistake. They are in one repo because the same person ends up owning both.

Install

# Any of ~75 agents (Gemini CLI, opencode, aider, …)
npx skills add agentui-ai/label-ops --agent gemini-cli --global

# Cursor
git clone https://github.com/agentui-ai/label-ops.git ~/.cursor/plugins/local/label-ops

# Codex
codex plugin marketplace add agentui-ai/label-ops && codex plugin add label-ops@label-ops

# Claude Code
claude --plugin-dir ./label-ops

No account, no service, no platform. Read skills/label-ops/SKILL.md directly if you would rather not install anything.

Two things you can run right now

Is this barcode right?

npm install
node skills/label-ops/scripts/label-cli.mjs check 5901234123458
5901234123458   EAN-13
  check digit WRONG — last digit is 8, should be 7
  correct code: 5901234123457

What will the printer silently drop?

node skills/label-ops/scripts/label-cli.mjs lint examples/broken-label.zpl
! ORIGIN_OFF_LABEL: ^FO900,40 starts past the declared width (800 dots). Nothing at that origin prints.
! UNESCAPED_CONTROL_CHAR: ^FD data contains ^ or ~ without ^FH: "AC^DC Widget". The printer reads it as a command.
   Emit ^FH before ^FD and hex-escape those bytes as _5E / _7E.
  INFO_GEOMETRY: ^PW800 is 100.0mm at 203 dpi. If the stock is a different width, this ZPL is for a different printer.

It lints ZPL you did not write, which is most ZPL. Exits non-zero on a real problem, so it works as a CI gate on a label template repo.

The check-digit rule

One rule covers EAN-8, EAN-13, UPC-A, ITF-14, GTIN-14, SSCC-18 and GLN: starting from the digit next to the check digit and moving left, weights alternate 3, 1, 3, 1…

Anchoring at the left instead is right for even-length payloads and wrong for odd — so EAN-13 works, UPC-A does not, and the conclusion usually drawn is that UPC-A is strange.

What is in it

label-ops/
├── skills/label-ops/
│   ├── SKILL.md              # nine traps, ZPL, ESC/POS, GS1-128, the three ways to reach a Zebra
│   └── scripts/
│       ├── barcodes.mjs      # GS1 / Code 39 / Code 128 check digits, GS1-128 with FNC1
│       ├── zpl.mjs           # mm→dots per dpi, ^FH escaping, bounds validation, 9100 socket
│       ├── escpos.mjs        # code-page encoding, receipt builder, chunked BLE writes
│       └── label-cli.mjs     # check a barcode, lint a .zpl file
├── examples/broken-label.zpl
└── benchmark/
    ├── cases.mjs             # nine cases, each naive vs skilled
    └── run.mjs               # the two ledgers

Run the benchmark

npm install && npm run bench

Instant. Deterministic, offline, free — no LLM, no network, no fixtures on disk. Two ledgers: SKILL (do the recipes produce something that scans?) gates the exit code; TRAP (does the from-memory version get it wrong?) is reporting, and a case both sides pass is named rather than counted.

Two of these cases started out as false traps — the naive implementations were accidentally no-ops, and the TRAP ledger said so instead of taking the credit. Both were rewritten to the mistake people actually make.

Also see

excel-ops · pdf-ops · oee-ops — the same treatment for spreadsheets, generated PDFs and manufacturing OEE. If the user wants a label station people log into rather than a script, AgentUI hosts that and agentui-tools is the agent plugin for it. Everything here works without either.

License

MIT — see LICENSE.