oomniv0.3.0

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

A cast through the relayFour lifelines: a sender running omni send, the relay hub running omni serve, a desktop running omni daemon registered to the hub as device quill, and a local app subscribed to that daemon. First the daemon dials out and registers with the hub: register, device quill, its platform and the shared token. The app subscribes to the daemon's local port. The sender casts to device quill with the token. The hub forwards the bare command line to quill and replies to the sender with a result: delivered one. The daemon injects the keystroke with its backend and mirrors the same line to the subscriber. Idle connections exchange ping and pong. sender omni send relay hub omni serve device quill omni daemon local app omni listen register · quill · token subscribe cast · device quill · cmd · token {"v":1,"type":"text",…} result · delivered 1 same line, mirrored inject xdotool, wtype, … idle sockets: ping / pong
A cast through the relay.Devices only ever receive bare command lines; everything else (registration, routing, receipts) is the hub's job.
RoleCommandDoes
Relay hubomni serveAccepts device registrations and sender casts on one port, routes each cast to one device or all, and answers the sender with a receipt
Devicethe omni apps, or omni daemon --relayRegisters with an operator-assigned id and platform, then receives command lines
Senderomni send --relay, omni castCasts to a device id, or to every connected device
Desktopomni daemonInjects each command once, with an input backend, and mirrors it to local subscribers
Subscriberomni listen, omni getReads 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.

--backendInjects with
auto (default)Whatever the operating system, display server and installed tools allow
xdotoolX11
wtype, ydotoolWayland
osascriptmacOS
logNothing: 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

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.