Quickstart
Install
Section titled “Install”No version has been released yet. The release pipeline is wired — goreleaser
builds six targets and generates a Homebrew cask
(brew install --cask deemwar-products/oss/device-bridge) and winget manifests —
but nothing has been tagged, so nothing is installable that way today. Build from
source.
The module has one dependency (gorilla/websocket) and needs cgo, which on macOS
means the system clang you already have.
git clone https://github.com/deemwar-products/device-bridgecd device-bridgego build -o device-bridge ./cmd/device-bridge./device-bridge --helpGo 1.24 or newer. macOS only today — see what actually works.
Grant Accessibility once
Section titled “Grant Accessibility once”Keyboard and mouse injection goes through CGEvent, which macOS gates behind
Accessibility. Without the grant, injection fails with an explicit error — it
never silently does nothing.
./device-bridge devicesinput available=trueIf it reports unavailable, add your terminal (or whatever binary is calling) under System Settings → Privacy & Security → Accessibility, then run it again.
Drive the desktop, no daemon
Section titled “Drive the desktop, no daemon”The one-shot verbs talk to the OS directly. No socket, no token, no serve.
./device-bridge type "hello world"./device-bridge key cmd+shift+t./device-bridge click --x 400 --y 300./device-bridge move-mouse --x 200 --y 200./device-bridge scroll --dy -120./device-bridge drag --x1 100 --y1 100 --x2 400 --y2 300Read the screen back
Section titled “Read the screen back”./device-bridge screenshot --out shot.pngwrote shot.png (3456x2234, 4812901 bytes)--display N picks a display (0 is the main one); --window ID captures a single
window by its CoreGraphics window id. A 1920×1080 one-shot measured 110 ms
cold in spike S5 — that figure is dominated by ScreenCaptureKit setup, so a
persistent stream costs far less per frame.
Get a licence key
Section titled “Get a licence key”Everything above works unlicensed. The streaming surface — serve, start and
install — does not: it needs a licence key, issued by hand and checked
entirely offline. The binary never makes a network call, so this works on an
air-gapped or egress-filtered build agent.
./device-bridge license requestYour device-bridge request key:
DBREQ-1.YNTV-OOXT-75NO-SR7ZEmail that key to [email protected]. We reply with a licence bound to that
machine; install it once:
./device-bridge license install DBRIDGE-1.eyJ2IjoxLCJpZCI6…./device-bridge license showIn CI, skip the file and set DEVICE_BRIDGE_LICENSE to the whole token — it
takes precedence over ~/.config/device-bridge/license.
One licence, one machine. No seats, no tiers, no activation call, no renewal. A leaked token is inert on anyone else’s computer — which is the point of binding it — but this is a speed bump against casual sharing, not DRM, and the ADR says so plainly.
Running a gated verb without a licence exits 3 and prints the three steps above, so you never have to come back here to find them.
Open a stream
Section titled “Open a stream”Two shapes. The standalone one is a single stream on a fixed port:
export WINDOWCTL_TOKEN=$(openssl rand -hex 16)./device-bridge start video-out --port 9334Then connect and read frames. Each WebSocket binary message is one 16-byte header followed by the raw payload:
const ws = new WebSocket(`ws://127.0.0.1:9334/?token=${process.env.WINDOWCTL_TOKEN}`);ws.binaryType = 'arraybuffer';ws.onmessage = (ev) => { const h = new DataView(ev.data, 0, 16); const kind = h.getUint8(2); // 4 = screen const format = h.getUint8(3); // 11 = raw BGRA const seq = h.getUint32(4); const stamp = h.getUint32(8); // producer monotonic ms — subtract for latency const len = h.getUint32(12); const pixels = new Uint8Array(ev.data, 16, len);};The standalone video-out server captures at the display’s native size, 10 fps. Use the control plane if you need to negotiate a different shape.
Or run the control plane
Section titled “Or run the control plane”./device-bridge serve --port 9330One authenticated connection starts and stops every stream, and each start
reply carries the port that was actually allocated plus a one-time ticket:
{ "type": "hello", "token": "…" }{ "type": "start", "stream": "screen", "video": { "width": 1280, "height": 720, "fps": 30 } }{ "type": "stop", "stream": "screen" }{ "type": "list" }Full message set in Wire protocol; the auth rules are in Security.
What you cannot do yet
Section titled “What you cannot do yet”Nothing above feeds into the app. The two inbound lanes — virtual microphone
and virtual camera — do not work today: the audio driver is written but
coreaudiod will not load it without a notarized Developer ID build, and the
camera extension is not built at all. device-bridge install camera tells you so
and exits non-zero. See what actually works.