src/server.js: HTTP routes, lifecycle, health, pairingsrc/tmux.js: tmux capture, pane listing, resize, input forwardingsrc/ansi.js: ANSI SGR to HTML conversionsrc/auth.js: token extraction andauthLevelsrc/config.js: config defaults, CLI args, pairing payloadssrc/ngrok.js: supervised ngrok child processpublic/: browser viewer, probe page, terminal assetstools/airc: background server wrapperandroid-app/: native Kotlin Android client
The browser viewer is the canonical terminal UI. The Android app renders terminal frames in a WebView and keeps controls native, while sharing the same server frame/input APIs.
Runtime paths:
Android app -> LAN URL or ngrok URL -> Node server -> tmux
Browser/Tesla -> ngrok URL -> Node server -> tmux
The server can supervise ngrok when ngrok.enabled is true. tools/airc itself
is not systemd-backed; it starts a detached Node process and tracks pid/state/log
files.
Airc defaults to 8080.
Local mode:
tools/airc local --session aircThis prepends:
--host 0.0.0.0 --no-ngrokPairing payload behavior:
- ngrok mode:
baseUrlis the configured/public ngrok URL. - local mode:
baseUrlprefers the first LAN URL. lanUrlsandpublicUrlare included so Android can try LAN first and fall back to ngrok/public access.- Browser pairing commands print QR/URL/token values instead of Android JSON.
pair-appusescontrolToken.pair-webusesviewToken.pair-web-controlusescontrolToken.
GET /api/configGET /api/pairingGET /api/tmux/frameGET /api/tmux/panesPOST /api/tmux/inputGET /api/attention(panes whose agent needs interaction; see attention.md)POST /api/agent/event(agent hook; control token)GET /api/statusGET /probeGET /api/probe/pollGET /healthzWebSocket /api/tmux/ws
Unauthenticated static assets:
/app.js/app.css/fonts/FiraCode-Regular.ttf
Auth uses generated tokens in config.json. viewToken can view only;
controlToken can view and send input. Requests can use:
?k=<token>X-Airc-Auth: <token>X-Swyd-Auth: <token>during migrationAuthorization: Bearer <token>airc_authcookieswyd_authcookie during migration
GET /api/config returns canControl based on the token presented. Browser UI
controls are shown only when canControl is true. /api/tmux/input and
/api/pairing require the control token and return 404 otherwise.
Unauthorized protected routes return 404, not 401, to avoid advertising the
private surface.
GET /api/config also returns non-secret URL metadata (publicUrl, lanUrls)
for clients.
GET /api/status returns live ngrok/public-tunnel status and local battery
summary for the browser dashboard.
Input payloads:
{ "target": "active", "text": "hello" }{ "target": "pane", "paneId": "%5", "key": "Enter" }Text uses tmux send-keys -l; named keys use tmux send-keys.
- Client polls
/api/tmux/frame. - Server resolves the active pane for
config.session, unless?pane=%Npins a concrete pane. - Server runs
tmux capture-pane -p -e -t <pane-id>. - ANSI SGR is converted to HTML.
- Response includes pane metadata, cursor position, HTML, and
ETag. - Client sends
If-None-Match; unchanged frames get304.
See Implementation Notes for tmux edge cases and sizing details.
The browser prefers WebSocket /api/tmux/ws for frame updates and falls back to
HTTP polling if the socket is unavailable or closes.
The repo-root Makefile wraps the common flow (make help lists targets).
It models the APK as a file target whose prerequisites are the Kotlin / res /
manifest / gradle sources, so make push only rebuilds when something changed:
make check # npm run check
make build # cd android-app && ./gradlew assembleDebug
make push # rebuild if needed, then adb install -r
make deploy # push and launch on the deviceEquivalent raw commands:
Node/server check:
npm run checkAndroid build:
cd android-app
./gradlew --no-daemon assembleDebugInstall:
~/android/platform-tools/adb install -r android-app/app/build/outputs/apk/debug/app-debug.apkSDK/tooling assumptions:
- Android SDK root:
~/android - Gradle install:
~/android/gradle-9.4.1 - JDK 17:
~/android/jdk-17 android-app/local.propertiesis machine-local and ignored
make release-apk (run by the /release flow) assembles a signed
airc-<version>.apk from ./gradlew assembleRelease. Signing reads a
git-ignored android-app/keystore.properties:
storeFile=airc-release.jks
storePassword=…
keyAlias=airc
keyPassword=…The keystore (android-app/airc-release.jks) and this file live only on the
release machine and are never committed — *.jks and keystore.properties
are git-ignored. Without them, assembleRelease falls back to the debug key
(so a plain build still works), but make release-apk hard-fails rather than
ship a debug-signed artifact. Keep a secure backup of the keystore: losing it
means future APKs can't upgrade an installed copy in place.
For fresh coding-agent contexts, also read AGENTS.md.
Do not commit:
config.json.airc-server.*node_modules/android-app/local.properties- Android/Gradle build outputs