relay
Relay and daemon
A watch cannot run a server, and an iPhone keyboard extension only runs while it is open. So devices dial out to a relay hub, and senders cast through it. Desktops run a daemon that injects keystrokes locally and can register with the same hub.
How it fits together
| Role | Command | Does |
|---|---|---|
| Relay hub | omni serve | Accepts device registrations and sender casts on one port, routes each cast to one device or all, and answers the sender with a receipt |
| Device | the omni apps, or omni daemon --relay | Registers with an operator-assigned id and platform, then receives command lines |
| Sender | omni send --relay, omni cast | Casts to a device id, or to every connected device |
| Desktop | omni daemon | Injects each command once, with an input backend, and mirrors it to local subscribers |
| Subscriber | omni listen, omni get | Reads what the daemon received, as NDJSON; opens no port of its own |
A session, end to end
A hub, a desktop daemon registered as device quill and a subscriber, on local test ports. The daemon runs in log-only mode, so it reports what it would type instead of typing it. All three share a token through OMNI_RELAY_TOKEN:
omni serve --listen 127.0.0.1:28765 &omni relay listening on 127.0.0.1:28765omni daemon --relay 127.0.0.1:28765 --device quill --listen 127.0.0.1:28766 --dry-run &omni daemon listening on 127.0.0.1:28766 (backend=log, auth=token) omni daemon registering to relay 127.0.0.1:28765 as device 'quill' (backend=log, platform=linux) omni listen --daemon 127.0.0.1:28766 &omni devices --relay 127.0.0.1:28765quillomni send --relay 127.0.0.1:28765 --device quill text "hello quill"omni send --relay 127.0.0.1:28765 --device quill key enter
The daemon replays both, and the subscriber prints the same two lines:
# daemon logomni daemon: replay {"v":1,"type":"text","text":"hello quill"} omni daemon: replay {"v":1,"type":"key","key":"enter"} # omni listen{"v":1,"type":"text","text":"hello quill"} {"v":1,"type":"key","key":"enter"}
Delivery receipts
After routing a cast, the hub tells the sender how many connected devices received it. A send to a device that is not connected warns, and with --require-delivery exits 3:
omni send --relay 127.0.0.1:28765 --device ghost --require-delivery text "anyone?"omni send: warning: device 'ghost' is not connected to the relay (delivered to 0 devices)echo $?3
Streaming omni cast stays fire-and-forget for latency. A hub too old to send receipts never blocks a sender; the send simply counts as unconfirmed.
The desktop daemon
omni daemon owns keystroke injection on a desktop. It listens on 127.0.0.1:8766 by default for senders and subscribers, and with --relay it also registers with a hub, which reaches desktops behind NAT.
--backend | Injects with |
|---|---|
auto (default) | Whatever the operating system, display server and installed tools allow |
xdotool | X11 |
wtype, ydotool | Wayland |
osascript | macOS |
log | Nothing: logs what it would replay (also --dry-run) |
Every received command is injected once and mirrored to all subscribers on the same port, so any number of apps, such as agent extensions, can follow keystrokes without competing for the device registration. omni listen streams them; omni get takes the next one and exits; both reconnect after transient disconnects. A subscriber can also speak the handshake directly: one line, {"v":1,"op":"subscribe","token":"…"}, then read.
Tokens and trust
- One shared token gates registration and casts on the hub, and casts and subscriptions on a non-loopback daemon. Loopback connections to a daemon use local trust.
- The token comes from
--tokenorOMNI_RELAY_TOKEN; the service modules export it from a file, never on a command line or in the Nix store. - Surrounding whitespace is trimmed on both sides, so a token read from a secret file with a trailing newline still matches.
- A rejected registration or cast gets a typed error line,
{"op":"error","v":1,"reason":"unauthorized"}, which apps match by type. - Device ids are assigned by the operator. Idle device sockets use TCP keepalive (after 20 seconds, every 5) and an application-level
ping/pong.
Running as a service
On NixOS and macOS, the flake's modules run the daemon, the hub or both (see the reference). Elsewhere, including Nix-on-Droid, the daemon writes its own service definition:
omni daemon --print-service systemd --user[Unit]Description=omni daemon — replay omni keystrokes on this host (com.harryaskham.omni.daemon)After=network.target[Service]ExecStart=/nix/store/…-omni-cli-0.3.0/bin/omni daemon --listen 127.0.0.1:8766 --backend autoRestart=on-failureRestartSec=2EnvironmentFile=-%h/.config/omni/daemon.env[Install]WantedBy=default.target
--install-service systemd|launchd|supervisord|auto writes it to the standard path and prints the command that enables it.