One phone, every AI coding CLI — drive Claude Code, Codex, Antigravity (and whatever ships next) from your phone browser. Self-hosted, plugin-based, no cloud relay.
🌐 Languages: English · 中文
- One UI, every agent — Claude Code, Codex, and Antigravity in the same PWA; no per-tool app to install.
- Plugin-based, day-one for new CLIs — a new agent drops in as a ~50-line adapter; even unsupported CLIs work immediately in raw PTY mode.
- Truly self-hosted — bytes stay on your LAN or Tailscale. No cloud relay, no account, no key escrow.
- Wraps your existing terminal — keep your normal
claude/codexworkflow on the desktop; the phone attaches to that live session instead of spawning a parallel one. - Phone never logs into the AI vendor — no ban risk — Switchboard only relays terminal I/O between your dev box and your phone; the phone never authenticates to (or directly connects to) Anthropic / OpenAI / Google. Every API call still originates from your dev box under your normal identity, so the vendor sees the same desktop client you've always used — nothing to flag as "anomalous mobile / multi-device login."
- Phone ↔ dev-box file transfer — drop files from your phone straight into a folder on the dev box (and download them back) via the in-app file manager. Great for moving a screenshot, an APK to test, or a one-off text snippet without spinning up a cloud bucket.
5-minute Quickstart → · Architecture in 30s · Full SPEC
- Why Switchboard
- What it does
- Architecture in 30 seconds
- Install
- Run
- Phone access (LAN / Tailscale)
- File transfer (phone ↔ dev box)
- Camera (phone as webcam + remote camera viewer)
- Alarm notifications (fall detection)
- Firewall — opening the port
- Supported agents
- FAQ
- Project layout
- License
A typical session:
┌─ your dev machine ─────────────────┐ ┌─ phone ─────────────────┐
│ PowerShell / Terminal │ │ http://192.168.x.x:5173│
│ ┌──────────────────────────────┐ │ WS │ ┌────────────────────┐ │
│ │ $ sw run claude │ │ ────▶ │ │ claude@my-project │ │
│ │ │ Welcome to Claude Code │ │ ◀──── │ │ > what should I… │ │
│ │ │ > _ │ │ LAN / │ │ [Esc][Tab][↑][↓] │ │
│ │ └──────────────────────────────┘ │ Tailsc │ └────────────────────┘ │
│ Server: switchboard listening :8787│ │ │
└─────────────────────────────────────┘ └─────────────────────────┘
The wrapper spawns the CLI in a real PTY, mirrors output to both your local terminal and any connected phone/desktop browser, and forwards input either direction. Closing the phone browser doesn't kill the session; your desktop terminal keeps working.
Or start cold from the phone — if nothing is wrapped yet, tap + New passthrough session in the web UI to spawn a fresh shell on the dev box, then launch claude / codex / anything in it. No SSH client on the phone, no need to wake the desktop.
sw(one binary): theservesubcommand runs a Fastify HTTP+WS server (port8787); therunsubcommand wraps any command in a PTY and registers it with the local server.- Browser UI: React + xterm.js, served by Vite in dev (
5173) or any static host in prod. - Adapters ship as packages — built-ins:
passthrough(any shell),codex,antigravity. Claude works via passthrough. - No auth in v0.1: bind to a trusted network (LAN, Tailscale). Auth is on the roadmap.
Full design in SPEC.md.
You need Node.js ≥ 18.18 (22 LTS recommended). Then clone and run the installer for your OS.
git clone https://github.com/newtv-ai/switchboard.git
cd switchboard./scripts/install.sh# If you've never run a script before, allow signed scripts for your user first:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
.\scripts\install.ps1
# or, with firewall ports pre-opened (needs admin PowerShell):
.\scripts\install.ps1 -OpenFirewallWhat the installer does:
- Verifies Node version.
npm install(workspaces handle every package).- Builds
@switchboard/sdk,@switchboard/core,@switchboard/server. npm linkso theswandswitchboardcommands are on your PATH.
node-pty native build (Windows only): if npm install fails on node-pty, install Visual Studio Build Tools 2022 with the "Desktop development with C++" workload, then re-run the installer.
Two terminals:
# Terminal A — start the Switchboard server + web UI in dev mode
npm run dev
# Server on http://0.0.0.0:8787, web on http://0.0.0.0:5173One-click launch: the repo ships a
start.bat(Windows, double-click) andstart.sh(Linux / macOS,bash start.sh) at the project root. They free up ports5173/8787from any prior dev process and then runnpm run dev. Equivalent to Terminal A above — Terminal B (sw run …) is still separate.
# Terminal B — wrap an AI CLI so phones can attach to it
sw run claude # Anthropic's Claude Code
sw run codex # OpenAI Codex CLI
sw run agy # Google Antigravity CLI
sw run -- bash # any other command works tooThen open http://localhost:5173 (or your LAN IP) in any browser. Tap the session and you're in.
For production-style serving (no Vite), build the web bundle and serve it with any static file server:
npm run build -w @switchboard/web
# serve packages/web/dist/ behind nginx / caddy / Cloudflare Tunnel / etc.- Find your dev box's LAN IP:
- macOS:
ipconfig getifaddr en0 - Linux:
ip -4 addr show | awk '/inet / && !/127.0.0.1/ {print $2}' - Windows:
ipconfig→ look for IPv4 under your active adapter
- macOS:
- On the phone browser, open
http://<dev-ip>:5173. - If it times out, your firewall is blocking inbound
5173(and/or8787). See Firewall.
Install Tailscale on both the dev box and the phone, log in to the same tailnet, and use the dev box's Tailscale IP (100.x.y.z) in place of the LAN IP. No firewall changes needed; Tailscale handles NAT traversal.
Open the web UI and click Upload in the header — this opens a small file manager that lets you:
- Phone → dev box: pick one or more files and upload them. Files land in
<repo-root>/downloads/on the dev box. Uploads are streamed in 5 MB chunks so multi-GB files work without holding the whole file in memory. - Dev box → phone: click Download next to any file in the list; the browser saves it through its normal download flow.
This is intentionally a single shared folder per dev box, with no auth — same trust model as the rest of Switchboard (bind to LAN / Tailscale only).
On phones, the browser is sandboxed and can't always read the file you selected — most notably:
- Media stored inside chat apps (WhatsApp / WeChat / Telegram / QQ folders).
- Files in app-private storage (Documents folder of another app, etc.).
- Some vendor "Files" apps return a file URI the browser can't open.
The fix is the same in every case: copy the file to your phone's public Downloads folder first, then re-select it from there.
The folder is called slightly different things depending on phone and language — they're the same place:
| Phone | Folder name (English) | Chinese system |
|---|---|---|
| Android (most) | Download or Downloads |
下载 |
| iOS Files app | Downloads |
下载 |
The Switchboard upload dialog will surface this hint automatically when an upload error looks permission-related.
Switchboard includes an optional camera module powered by go2rtc. Two directions:
| Direction | What it does | Use case |
|---|---|---|
| Phone → Desktop | Use your phone camera as a webcam for Zoom/Teams/WeChat | Video calls, screen recording |
| Desktop → Phone | View IP cameras (RTSP) or USB webcams from your phone | NAS monitoring, baby cam, 3D printer |
- Start the server normally (
start.batornpm run dev). go2rtc is downloaded automatically on first launch. - Open the web UI → click Cameras.
- To view an IP camera: paste an RTSP URL (e.g.
rtsp://admin:pass@192.168.1.100:554/Streaming/Channels/1) → click Add → click View. - To use phone as webcam: on your phone, open
https://<your-ip>:5173→ Cameras → Start Camera. Then on desktop, openhttp://localhost:1984/stream.html?src=phone_camto see the stream. To use in Zoom/Teams: OBS → Media Source → URLhttp://localhost:1984/api/stream.mp4?src=phone_cam→ Start Virtual Camera.
| Port | Protocol | Features | Recommended for |
|---|---|---|---|
http://<ip>:5174 |
HTTP | Terminal, file transfer, camera viewer | Daily use (desktop + phone terminal control) |
https://<ip>:5173 |
HTTPS | All of the above + phone camera push | Only when you need phone-as-webcam |
Most users should use HTTP 5174 for daily work. HTTPS 5173 is only needed when you want to use your phone's camera as a desktop webcam (browsers require HTTPS for getUserMedia). Don't keep both tabs open on your phone — use one or the other.
HTTPS certificates are auto-generated on first start (self-signed, valid 5 years, stored in certs/). Desktop browsers show a one-time "not secure" warning — click through once and it won't appear again. Note: phone camera push is unavailable on HTTP 5174.
- H.264 and H.265 both supported. go2rtc handles codec negotiation automatically.
- Camera configs persist across server restarts (
~/.switchboard/cameras.json). - Phone camera persists across page navigation — start streaming on the Cameras page, then switch to Terminal, the stream keeps going.
- go2rtc auto-download: ~15MB binary downloaded from GitHub on first use. If GitHub is blocked (e.g. mainland China), see the FAQ below for manual install.
- Firewall: go2rtc uses port 8555 (UDP+TCP) for WebRTC. If phone camera push doesn't connect, open this port in your firewall alongside 5173/5174/8787.
The Cameras page accepts standard streaming URLs. Common formats:
IP cameras (RTSP)
rtsp://admin:password@192.168.1.100:554/Streaming/Channels/1 # Hikvision main stream
rtsp://admin:password@192.168.1.100:554/Streaming/Channels/2 # Hikvision sub stream
rtsp://admin:password@192.168.1.100:554/cam/realmonitor?channel=1&subtype=0 # Dahua
rtsp://admin:password@192.168.1.100:554/stream1 # generic ONVIF
rtsp://192.168.1.100:8554/mystream # RTSP server (no auth)
HTTP streams
http://192.168.1.100:8080/video # MJPEG / HTTP-FLV
https://example.com/live/stream.m3u8 # HLS
RTMP
rtmp://192.168.1.100/live/stream
Tips:
- Most IP cameras use port
554for RTSP. Check your camera's admin page for the exact URL path. - Use the sub stream (lower resolution) to reduce bandwidth if the main stream is too heavy.
- If unsure about the URL, try your camera's ONVIF address:
rtsp://<ip>:554/onvif1. - Test the URL with VLC first (
Media > Open Network Stream) to confirm it works before adding to Switchboard.
The camera module auto-downloads go2rtc from GitHub Releases on first use. If you're behind a firewall that blocks GitHub (common in mainland China), you can install it manually:
-
Download the correct binary for your platform from a mirror or another machine:
- Windows x64:
go2rtc_win64.zip - macOS Apple Silicon:
go2rtc_mac_arm64.zip - macOS Intel:
go2rtc_mac_amd64.zip - Linux x64:
go2rtc_linux_amd64 - Linux ARM64:
go2rtc_linux_arm64
Official releases: https://github.com/AlexxIT/go2rtc/releases
- Windows x64:
-
Extract and place the binary:
# Windows — extract go2rtc.exe to: %USERPROFILE%\.switchboard\bin\go2rtc.exe # macOS / Linux — extract and chmod: mkdir -p ~/.switchboard/bin # (copy go2rtc binary here) chmod +x ~/.switchboard/bin/go2rtc
-
Alternatively, put
go2rtcanywhere on your system PATH. -
Restart the server. You should see
[camera] module loadedin the logs.
Switchboard can push a Web Push notification to your phone when an external detector fires an alarm — built for fall detection, but any source can POST the webhook. Your phone buzzes and rings even when the PWA is closed; tap the notification to jump straight to the camera page.
How it works: a detector sends POST /api/alarm; Switchboard signs a Web Push with its own VAPID keys and hands it to the browser vendor's push service (FCM on Android/Chrome, APNs on iOS), which wakes the PWA's service worker on your phone. No vendor account — VAPID keys are auto-generated on first start into certs/vapid-keys.json.
The alert label comes from the upstream. /api/alarm is the single integration point; the POST body's optional alarm_type field becomes the notification — send {"alarm_type":"暴力"} and the phone shows "检测到暴力,点击查看", omit it and it defaults to 跌倒 (so a detector that omits it keeps working; alarm_type is trimmed + length-capped server-side). Other body fields are optional and only logged; the notification uses the server's receive time (not the payload's video offset), and repeats collapse per <alarm_type>-<track_id> via the notification tag. To wire a real detector, point it — or any script — at http://<host>:8787/api/alarm (plain HTTP is fine server-to-server). When SWITCHBOARD_ALARM_SECRET is set, HMAC-SHA256 the raw request body and send it as X-Falldown-Signature: sha256=<hex>. Per-type icons or sounds: extend the one notification block in packages/server/src/alarm-handler.ts. For time-critical production alerts, also send the push with high urgency + a short TTL (the default is normal urgency / long TTL) — the broadcast() call is in packages/server/src/push.ts.
Only the part that touches the phone's browser requires HTTPS — it's a hard browser rule (Service Worker + Push API only work in a "secure context"), not a Switchboard choice:
| Leg | HTTPS? |
|---|---|
| Phone enabling alarms / opening the PWA / service-worker registration | Required. Use https://<ip>:5173, not the HTTP :5174 port. (http://localhost is also a secure context, but that only helps the dev box itself — a phone reaching you by IP is not localhost.) |
The webhook POST /api/alarm from your detector |
Not required — it's a server-to-server call. Plain http://<host>:8787/api/alarm is fine on a trusted LAN. |
| Sending the push / the phone receiving it | N/A — goes through FCM/APNs over the OS push channel, independent of how the phone reached your server. Works once subscribed, even with the app closed. |
Trusted-cert caveat: the auto-generated cert is self-signed, so browsers flag it. Android Chrome works once you accept the warning. iOS Safari is stricter — it only registers a service worker behind a trusted cert. Getting one is independent of which VLAN you use (Tailscale, ZeroTier, Nebula, WireGuard, plain LAN, …): either (a) install + trust the self-signed CA on the device (iOS: Settings → General → VPN & Device Management; Android: Settings → Security → install a CA certificate), or (b) put a genuinely-trusted cert in front for whatever hostname the phone uses — e.g.
tailscale certfor a*.ts.netname, or your own domain + Let's Encrypt behind a reverse proxy.
Reachability (censored networks / no Google Play): On Android, both Chrome and Firefox deliver Web Push through Google FCM — Firefox for Android bridges via Firebase too (Mozilla source), so switching browser doesn't dodge it. If the phone can't reach Google (e.g. mainland China) or has no Google Play Services, the subscription still succeeds but pushes silently never arrive. Fix: route the phone through a node that can reach FCM — e.g. a Tailscale exit node on a VPS outside the blocked region (
tailscale up --advertise-exit-nodeon the VPS, approve it in the admin console, then pick it as Exit Node in the phone's Tailscale app) — or any VPN — and it must stay on whenever an alarm could fire: Web Push rides a persistent connection, so an alarm that fires while the phone's route to FCM is down won't arrive until that route is back (and a late fall alarm is useless). iOS is different: Safari PWA push uses Apple APNs, which is reachable inside mainland China, so iPhones generally work without an exit node (they still need the trusted cert above). Chinese OEM push (Huawei HMS / Xiaomi MiPush / vivo·OPPO Push) is a native-app SDK channel, not a path for standard browser Web Push. The server also needs its own outbound reach to FCM/APNs at the moment an alarm fires (not just during setup) to send.
Enable on your phone:
- Open
https://<your-ip>:5173(or your Tailscalehttps://…ts.netURL). On iOS, "Add to Home Screen" first (Safari 16.4+); Android Chrome works in a normal tab. - Tap the 🔕 告警 bell on the home screen → allow notifications. It becomes 🔔 告警开.
Test without a real fall (server running, no alarm secret set):
curl -X POST http://localhost:8787/api/alarm \
-H 'content-type: application/json' \
-d '{"event":"fall_alarm","track_id":1,"stgcn_action":"Fall Down","stgcn_fall_prob":0.7,"source":"manual-test/1"}'Your phone should buzz immediately.
Server config (env vars):
| Var | Default | Purpose |
|---|---|---|
SWITCHBOARD_ALARM_SECRET |
(unset) | When set, /api/alarm requires a valid X-Falldown-Signature: sha256=<hmac> (HMAC-SHA256 of the raw body). Strongly recommended when the server is exposed beyond the LAN (e.g. Tailscale Funnel); leave unset for pure-LAN. |
SWITCHBOARD_VAPID_CONTACT |
admin@example.com |
Contact identifier embedded in VAPID (protocol requirement; just an identifier). |
Web Push transport always transits the vendor's push service (FCM/APNs) — the server needs outbound internet to send. The payload is end-to-end encrypted and there's no vendor account, but it isn't "zero cloud" in the transport sense.
iOS Safari only registers the service worker behind a trusted cert, and the self-signed pair trips warnings everywhere else. If your dev box is on Tailscale with HTTPS enabled, Switchboard ships a helper that fetches and renews a real *.ts.net cert:
node packages/web/refresh-cert.js # writes certs/tailscale.{crt,key}; a no-op while the cert is still freshThen point the dev server at it — these two env vars take priority over the self-signed pair, and Vite hot-swaps the cert every 12 h, so a renewal applies without restarting the server:
# Windows (persist as user env vars; use absolute paths)
setx SWITCHBOARD_TLS_CERT "<repo>\certs\tailscale.crt"
setx SWITCHBOARD_TLS_KEY "<repo>\certs\tailscale.key"# Linux / macOS
export SWITCHBOARD_TLS_CERT=<repo>/certs/tailscale.crt
export SWITCHBOARD_TLS_KEY=<repo>/certs/tailscale.keyrefresh-cert.js is best-effort — it never throws and always exits 0, skipping quietly if Tailscale isn't running or the tailnet hasn't enabled HTTPS. Tailscale renews the cert near expiry on its own; run the helper on a schedule so the fresh cert lands on disk unattended:
# Windows — weekly Scheduled Task (or use the Task Scheduler GUI)
schtasks /create /tn "Switchboard cert renew" /sc weekly /tr "node <repo>\packages\web\refresh-cert.js" /f# Linux / macOS — weekly cron
0 4 * * 0 node <repo>/packages/web/refresh-cert.jsSwitchboard binds to 0.0.0.0 so anything on the network can reach it (web on 5173, server on 8787). If the phone can't connect, the OS firewall is blocking inbound TCP.
The easiest path is the bundled installer flag:
# in an admin PowerShell
.\scripts\install.ps1 -OpenFirewallOr do it by hand:
# admin PowerShell
New-NetFirewallRule -DisplayName 'Switchboard server (8787)' -Direction Inbound `
-Protocol TCP -LocalPort 8787 -Action Allow -Profile Private,Domain
New-NetFirewallRule -DisplayName 'Switchboard vite dev (5173)' -Direction Inbound `
-Protocol TCP -LocalPort 5173 -Action Allow -Profile Private,DomainImportant — Public vs Private network: Windows refuses to apply firewall rules for Private,Domain profiles when your Wi-Fi is classified as Public. Symptoms: rules added, port test still fails. Fix:
- Settings → Network & Internet → Wi-Fi → click the network name → Network profile type: Private
- or pass
-Profile Anyin the rule (less safe).
macOS's stock firewall is per-application, not per-port. If you've enabled it (System Settings → Network → Firewall), allow inbound connections for node the first time you start the server — a dialog will pop up. If you blocked it by accident:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --remove $(which node)
# next start of `sw` will re-promptFor users on a custom pf-based firewall, allow inbound TCP 5173 and 8787 on your LAN interface.
sudo ufw allow from 192.168.0.0/16 to any port 5173 proto tcp # adjust subnet
sudo ufw allow from 192.168.0.0/16 to any port 8787 proto tcp
sudo ufw reloadsudo firewall-cmd --permanent --add-port=5173/tcp
sudo firewall-cmd --permanent --add-port=8787/tcp
sudo firewall-cmd --reloadsudo iptables -A INPUT -p tcp --dport 5173 -s 192.168.0.0/16 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8787 -s 192.168.0.0/16 -j ACCEPT
# persist with iptables-save / netfilter-persistentFrom another machine on the LAN:
curl http://<dev-ip>:8787/health # expect {"ok":true,"sessions":0}If that works but the phone doesn't, the phone is on a different VLAN/SSID, or you're using a guest Wi-Fi with client isolation.
| Adapter id | CLI command | Auto-detected from | Special handling |
|---|---|---|---|
passthrough |
any | (default) | Spawns a plain shell; sw run claude uses this |
codex |
codex |
command name | Injects --no-alt-screen + isolated CODEX_HOME (avoids SQLite contention) |
antigravity |
agy |
command name | Bare wrap; OAuth happens on first run |
Override the auto-detect with --adapter <id>. New adapter: implement the AgentAdapter interface from @switchboard/sdk and register it in packages/server/src/server.ts.
- Make sure
npm run devis actually running (look forServer listening+vite ready). curl http://<dev-ip>:8787/healthfrom your dev box and from a second machine on the same Wi-Fi. If the dev-box version works but external doesn't, it's the firewall — see above.- On Windows, double-check that the Wi-Fi profile is Private, not Public. Public profile blocks LAN-inbound by default no matter what rules you add.
- Some routers / guest networks have "AP isolation" or "client isolation" turned on, which forbids device-to-device traffic. Switch to your main Wi-Fi or use Tailscale.
Some mobile browsers (Xiaomi MIUI, iOS Safari in low-power mode) cull WebSockets they think are idle. Switchboard sends an app-level keepalive every 5 s, so this should not happen. If it does, file an issue with the phone model / browser.
The wrapper sends \x1b[8;rows;cols t to physically resize your terminal window so it matches the PTY. This requires "Window resize reporting" enabled in your terminal:
- Windows Terminal: enabled by default since v1.18+.
- iTerm2 / Apple Terminal / Alacritty / WezTerm: enabled by default.
- xterm: enabled by default; some
*termforks (urxvtetc.) disable it.
In the codex login screen, pick "Sign in with Device Code". Codex prints a short code + URL; open the URL on any device (your phone works), paste the code, authorize. Codex on the remote machine completes the flow.
Google blocks Antigravity at the account level for mainland China, Russia, Iran, etc. A VPN alone is not enough — you also need a Google account whose Country Association is set to a supported region. There is no Switchboard-side workaround.
- claude: yes, no shared state.
- codex: yes — Switchboard sets
CODEX_HOME=$(mktemp -d)per session to avoid the SQLite-lock deadlock reported in openai/codex#20213. - agy: not currently isolated; concurrent sessions share
~/.gemini/. If you hit issues, run them with differentHOME=$(mktemp -d)(full workaround pending — tracked in our issues).
PORT=9000 sw # or `switchboard`Pass the same port to clients via sw run --server ws://127.0.0.1:9000 ….
Don't, yet. There's no auth in v0.1 — anyone reaching :8787 can drive your terminal. Use Tailscale, a private VPN, or a reverse proxy with HTTP basic-auth on top. Auth is on the roadmap.
SWITCHBOARD_DEBUG=1 sw # server side
# logs lines like:
# [switchboard:debug] refit session=abcd1234 clients=2 ownSize={...} -> resize(47,30)
# [switchboard:debug] /ws close code=1006 reason=… hasHandle=true …This is a known upstream issue, not a Switchboard bug. Claude Code uses Ink (React-for-CLI), which performs full-screen re-renders on every state change (loading observations, dismissing dialogs, SIGWINCH, etc.). Each re-render sends ESC[H (cursor to viewport origin) then redraws every line with ESC[K. When the drawn content exceeds the viewport height, the excess overflows into the scrollback buffer. The next ESC[H can only reach the current viewport top -- it cannot erase the overflow already pushed into scrollback. Result: each re-render deposits one "ghost frame" in scrollback. 22 re-renders = 22 duplicates. The same artifacts exist on a desktop terminal if you scroll up; Switchboard simply makes them more visible. See claude-code#49086, claude-code#52027 for upstream reports. Current mitigation: the mobile web client auto-scrolls to the latest output so duplicate frames stay out of sight during normal use; the terminal also supports dual-mode scroll handling (native browser scroll in default mode, PgUp/PgDn translation in fullscreen mode). Feedback welcome via Issues.
switchboard/
├── packages/
│ ├── sdk/ # public AgentAdapter contract — what third-party adapters import
│ ├── core/ # Session, RingBuffer, WrapperBackend — agent-agnostic
│ ├── server/ # Fastify HTTP+WS server + `sw run` CLI + built-in adapters
│ ├── web/ # React + xterm.js frontend
│ └── camera/ # Optional: go2rtc sidecar for camera streaming
├── scripts/
│ ├── install.sh # Linux & macOS installer
│ └── install.ps1 # Windows installer
├── start.sh # one-click dev launcher (Linux / macOS)
├── start.bat # one-click dev launcher (Windows)
├── downloads/ # phone↔dev-box file-transfer drop folder (gitignored)
├── SPEC.md # full design + roadmap; source of truth for architectural decisions
└── README.md # this file
MIT — do whatever you want, no warranty.
The PTY-wrap architecture is parallel to slopus/happy — credit to them for proving it scales. Switchboard is built around direct LAN/Tailscale connections and a browser-only client (no native app required).