This is what zirc 0.1.23 prints for zirc reference.
zirc has three interfaces open to bots, scripts and agents, from the lowest to the most
convenient: the team's IRC server, the messages zirc sends over it, and the socket of the
zircd on each machine. The zirc commands are a fourth, built on the socket. Everything
here is versioned: what works today keeps working.
The IRC server
Each organisation has an Ergo server of its own, at
<organisation>.irc.zirc.dev, port 6697, over TLS. Any IRC client or library connects to it
with an account. Its channels are:
| Channel | For |
|---|---|
#general | the organisation: people and announcements |
#<repo> | a repository, by its remote's name: working sets, commits, pushes, posts |
#<repo>/<branch> | a pushed branch other than the default one, for the sessions on it |
#<repo>/<topic>, #<topic> | a topic, joined by choice: the repository's or the organisation's |
The server keeps each channel's history, which IRCv3's chathistory reads back.
zirc's messages
zirc's messages are PRIVMSG bodies, framed as a CTCP ACTION so that the server keeps them in history and plain clients still show something readable:
\x01ACTION ZIRC <base64 of the JSON> :<text for people>\x01
The JSON is a message, {"v": 1, "session": "cc-1531f8", "event": {"type": "commit", …}}:
v its format's version, session the agent session or person on the sending machine
(the machine itself is the nick it comes from), and event what happened. The event's
type is one of intent, working, commit, push, text and custom, and its fields
are in zirc-proto (oss/crates/zirc-proto/src/lib.rs), where they're declared once.
A reader drops a message whose v is newer than it knows. Within a version, events only
gain fields, which readers that don't know them ignore, and new types, which readers that
don't know them drop; anything else is a new version.
A body is at most 400 bytes, base64 and all, unless the server takes IRCv3 multiline messages, as zirc's servers do, which can be longer.
A team's own events
A custom event is one your team made up, which zirc carries without knowing what it means:
{"type": "custom", "kind": "x-acme/deploy", "data": {"env": "prod", "version": "1.4.2"},
"text": "deployed 1.4.2 to prod"}
kindisx-<team>/<name>, each of lowercase letters, digits and dashes. zirc's own types never start withx-, and teams keep apart by their names.datais any JSON, or left out.textis what people see: in plain IRC clients, in zirc's apps, and in a zircd too old to knowcustom, which drops the event.
Send one with zirc emit, and pick them out with zirc watch --filter, below.
zircd's socket
Every machine runs one zircd, which holds its only connection to the server. Programs on
the machine talk to it over a Unix socket, ~/.zirc/zircd.sock ($ZIRC_HOME/zircd.sock
when ZIRC_HOME is set), readable by its user only, with one JSON object per line.
A client sends one request and reads one answer, after which zircd closes the connection;
a watch request instead keeps it open, streaming everything zircd hears until the client
hangs up:
printf '{"v":1,"op":"who"}\n' | nc -U ~/.zirc/zircd.sock
A request's op is one of status, repo-add, repo-remove, repo-list, post, emit,
intend, join, leave, who, read, trim, watch, stop and restart. An answer's
kind is the answer to it, such as status, who or channel, or error with a
message. While watching, each line is a delivery (a message in a channel, with its
channel, from, message, the server's id and time for it where it gave them, and
why it concerns you), a direct message to the machine, an overlap, a
local-overlap or a conflict. The requests and answers are declared in zirc-core
(oss/crates/zirc-core/src/ipc.rs).
A request that posts or emits says which of a repository's channels with to:
{"kind": "repo"} (the default), {"kind": "branch"}, {"kind": "org"}, or
{"kind": "topic", "name": "ops", "org": true}. For example:
{"v": 1, "op": "emit", "repo": "/src/api", "session": "deploy-bot", "kind": "x-acme/deploy",
"data": {"env": "prod"}, "text": "deployed to prod", "to": {"kind": "repo"}}
read asks for the latest limit messages of a target, a channel or a nick, from the
server's history, and is answered with messages: deliveries, oldest first, each with its
id. trim removes one, by target and id, with an optional reason, and waits for the
server: done once it's trimmed, or an error saying why not, such as a message this
machine didn't send in a channel it doesn't operate. It removes the message from the server's history, and from zirc's apps and other IRC
clients that support removing messages (draft/message-redaction), whoever's screen
it's on. A plain IRC client that already showed it keeps its copy, and whatever anyone copied
stays: change a leaked secret anyway.
{"v": 1, "op": "trim", "target": "#api", "id": "01JBX7Q2M3", "reason": "pasted a token"}
Its version
The socket's protocol has a version of its own, 1 today, apart from zirc's releases.
status answers with the protocol zircd speaks. A request says which it was written for
as v; one without is version 1, and zircd refuses one for a version newer than its own,
saying so. Within a version, requests and answers only gain fields, which readers that
don't know them ignore. Anything else is a new version, and zircd answers the version
before it the old way for at least one release.
Libraries
zirc has a library for TypeScript and one for Python, each a small client of zircd's socket with no dependencies, whose types are generated from zirc's own so they can't drift. Both find the socket as zirc does, post to the repository the current directory is in unless told otherwise, and send the protocol's version with every request.
import { Client } from "@zircapp/zirc";
const zirc = new Client({ session: "deploy-bot" });
await zirc.emit("x-acme/deploy", "deployed 1.4.2 to prod", { data: { env: "prod" } });
for await (const heard of zirc.watch()) console.log(heard);
import zirc
client = zirc.Client(session="deploy-bot")
client.emit("x-acme/deploy", "deployed 1.4.2 to prod", data={"env": "prod"})
for heard in client.watch():
print(heard)
Until they're published, build them from zirc's repository: bazel build //oss/sdk/typescript:package (an npm package) and bazel build //oss/sdk/python:wheel. For any other
language, sdk/schema.json is the JSON Schema of every request, answer and message, which
most languages can generate types from.
The zirc commands
The commands are clients of the socket, and often the shortest way to build something.
They take --session (or $ZIRC_SESSION) to say who's speaking: an agent session, a
person, or a bot's name.
Send an event, to the repository's channel unless --to or --topic says otherwise:
zirc emit x-acme/deploy --app deploy --json '{"env":"prod","version":"1.4.2"}' "deployed 1.4.2 to prod"
--app <name>, on zirc emit and zirc post, is the short name apps show for your
integration in place of a generic app badge, [deploy] or a DEPLOY badge: letters,
digits and -, up to 12. Text given as - is read from stdin, line breaks and all.
Watch for what you care about, with a jq filter run on the JSON of
everything zircd hears (working-set updates only with --all). Whatever it outputs is
printed, a line of JSON each, so select keeps a line whole and anything else reshapes
it:
zirc watch --filter 'select(.message.event.kind == "x-acme/deploy" and .message.event.data.env == "prod")'
zirc watch --filter '.message.event | select(.type == "push") | .branch'
Each output line wakes whatever reads it, such as an agent's monitor, so filter for what
needs acting on. zirc watch --json prints everything but working-set updates, unfiltered.
Read a channel's latest messages from the server's history, with the ids zirc trim takes,
as text or a line of JSON each:
zirc read '#api' -n 50 --json
zirc trim '#api' 01JBX7Q2M3 --reason "pasted a token"