Running the server · How it is built · The tz command line · The extension · Technical notes
tz
A command-line client for a [NotReliable server](/docs/server). Zero dependencies — fetch and the shared wire contract in ../protocol.js, which is the same file the extension and the server import, so all three ask for the same things in the same spelling.
It is a client. It has no database and no way to reach inside the server; everything it knows it learned over HTTP, which is why it works just as well against a server on another continent as one on this laptop.
Install
cd cli
npm link # or: ln -s "$PWD/tz.mjs" ~/.local/bin/tz
There is nothing to install — npm link only puts tz on your PATH. You can skip it entirely and run node cli/tz.mjs ….
Point it somewhere
The short way: when the server shows you a new token (Account page, or tz token add you --save conn.json), it offers a connection file. Then:
tz connect conn.json # verifies it works, then writes ~/.tzrc
The same file imports into the extension: ⚙ → Servers → Or import a connection file. It contains the token, so it deserves the same care.
The long way:
export TZ_URL=http://localhost:3000
export TZ_TOKEN=$(tz token add mmarc --password '…' | grep ^token | cut -f2)
Or put them in ~/.tzrc, which keeps the token out of your shell history:
TZ_URL = http://localhost:3000
TZ_TOKEN = tz_…
The command line beats the environment, which beats the file: --url and --token win over TZ_URL/TZ_TOKEN, which win over ~/.tzrc.
Use it
tz boards # every board you can see
tz cards # every card on all of them, flat
tz cards WEB --list Doing --mine # one board, filtered
tz cards WEB --archived # the archive, and nothing else
tz card WEB-12 # one card in full
tz search 'due:week label:bug' # the server's own search
tz add WEB/Doing "Write the release notes"
tz move WEB-12 Done --pos 1
tz move WEB-12 OPS/Doing # to another board: new key, labels dropped
tz comment WEB-12 "shipped, watching the graphs"
tz tick WEB-12 "write the tests"
tz done WEB-12
tz start WEB-12 2026-09-10 # the start date; --off clears it
tz item WEB-12 "write the tests" --assign pat --due 2026-09-12
tz comment edit <id> "what I meant" # the id is in `tz card`
tz delete WEB-12 --yes # gone for good, not archived; admins only
tz lists WEB # the columns, with a count on each
tz list add WEB "In review" # a new one, at the bottom
tz list rename WEB/Review "In review"
tz list archive WEB/Old --off # archive one (cards and all), or bring it back
tz board add "Operations" --key OPS # a board, with the three lists on it
tz board rename OPS "Ops"
tz board archive OPS --off
tz labels WEB # the board's labels
tz label add WEB bug --color red
tz label rename WEB/bug defect
tz label color WEB/defect orange # or `none`
tz label WEB-12 defect # on a card; --off takes it off
tz label delete WEB/defect # and off every card that had it
Files go up from a path and come back down a pipe:
tz attach WEB-12 ./screenshot.png --type image/png
tz attach WEB-12 https://example.com/spec --name "The spec"
tz attachments WEB-12
tz fetch WEB-12 screenshot.png > screenshot.png # bytes on stdout, notes on stderr
tz unattach WEB-12 screenshot.png
Docs are Markdown on stdin and stdout, which is the whole reason they are bare:
tz doc get WEB-4 > handbook.md
$EDITOR handbook.md
tz doc put WEB-4 < handbook.md
tz doc history WEB-4 # every version kept
tz doc history WEB-4 --version <id> | tz doc put WEB-4 # …and back to one
And when there is no command for what you want:
tz api /1/boards/WEB/events?since=0
tz api PUT /1/cards/WEB-12 --data '{"due":"2026-09-01"}'
Naming things
A card is named by its key (WEB-12), its full id, or the end of that id — these ids start with a timestamp, so two cards made in the same second share their first eight characters and it is the tail that tells them apart. Lists and checklist items are named by their name or the start of it.
Anything that could mean two things is refused, with both named. tz will not guess which card you meant.
Output
One record per line, tab-separated, in a column order that does not move between runs. The header line starts with # so you can drop it. No colour, no spinners, nothing that needs a wide terminal — the two readers are a person and a language model, and they want the same thing.
--json gives you the payload as the server sent it instead of our reading of it. --limit n caps a list, and says on stderr when it did — nothing here is silently truncated.
Errors are one line on stderr; stdout stays clean for the pipe. Exit codes mean something:
| Code | Means |
|---|---|
| 0 | fine |
| 1 | you typed something wrong (unknown command, ambiguous name) |
| 2 | not found |
| 3 | not allowed — no token, wrong token, not yours |
| 4 | the server said no, or could not be reached |
Not here
Making users and groups. Those have no HTTP API: accounts are made at the server console (node server.mjs user add …) or by an invite link from the web admin. tz user add says so rather than failing in a way you have to investigate.