A Tailscale-first console for native PowerShell terminals, native iOS control surfaces, and optional remote desktop streaming.
- Native PowerShell is the terminal runtime on Windows
- Direct persistent PTY sessions for PowerShell
- Multi-session mode by default: each terminal is an independent PowerShell console
- REST API for session lifecycle (
/api/sessions) - Session-scoped WebSocket streaming (
/ws?sessionId=...) - Optional remote desktop sidecar integration (
Remotetab, decoupled from Console)- Browser stream via proxied WS (
/ws/remote) over the same tailnet access boundary - View-only by default, with explicit control enable toggle
- Touch controls for iOS (tap/long-press/drag/two-finger wheel)
- Browser stream via proxied WS (
- Optional command sidecar for Windows host automation over Tailscale
- Token-gated command runs, live output streaming, run history, stop support
- Useful for fetching/pulling on Windows, restarting
npm start, and closing the Mac/iOS/Windows loop
- Native iOS console, stream presets, remote cursor relay, live gateway stats, and desktop shortcut actions
- Idle session cleanup + max session guardrails
- Graceful shutdown persistence:
- Server shutdown detaches web clients; direct PTY sessions are recreated from saved metadata
- Active sessions are saved and restored on next server start
- Structured server modules (
src/config,src/http,src/sessions,src/ws,src/remote) - iOS touch scroll stability update in the frontend (no private xterm monkey-patching)
- Touch and wheel scrolling support for terminal history across desktop and iOS clients
- Top pill view switch for Console and Remote
- Windows with PowerShell available
- Node.js 18+
- Tailscale installed, signed in, and running on the host
From the project root on the host where the tailscale CLI is available:
npm install
npm startnpm start verifies Tailscale is running, configures Tailscale Serve for http://127.0.0.1:<PORT>, starts the optional remote sidecar when REMOTE_ENABLED=true, and then starts the app.
Open the printed tailnet URL:
https://<node>.<tailnet>.ts.net
The backend always binds to 127.0.0.1. Plain localhost browser access is rejected; Tailscale Serve identity headers, Tailscale source addresses, and tailnet ACLs are the access layer.
The server auto-loads .env from the project root.
cp .env.example .envThen edit .env with your local settings.
Useful standalone service commands:
npm run remote-agent
npm run command-sidecarTailscale is required. Startup fails if tailscale status --json cannot prove the node is connected, or if tailscale serve --bg http://127.0.0.1:<PORT> cannot be configured.
HTTP and WebSocket requests are accepted only when they arrive through Tailscale Serve identity headers or from Tailscale source ranges (100.64.0.0/10, fd7a:115c:a1e0::/48). Local browser requests to localhost are blocked.
The SwiftUI iPhone app lives in ios/OnlineCLI.xcodeproj. The Console tab is native SwiftUI/UIKit and talks directly to /ws?sessionId=...; it no longer embeds the browser console. It supports native PowerShell terminal creation, hardware/software keyboard input, resize messages, scrollback, paste/copy, and terminal control keys.
The iOS app is intentionally focused on Console, Remote, and Settings. CLI agents and other tools can still be used normally inside any PowerShell terminal or through the remote desktop.
Run npm start, keep Tailscale connected on the iPhone, then open the Xcode project and build the OnlineCLI scheme. When the app is not connected, it opens a simple connection screen where the user enters the printed tailnet URL manually.
The native remote tab uses the backend's capabilities contract instead of guessing: /api/remote/capabilities returns stream presets, supported shortcut actions, live gateway counts, input limits, and display metadata. The WebSocket also accepts set-stream messages, so the app can switch between Economy, Balanced, Fluid, and Sharp profiles while connected.
Remote desktop is disabled by default and does not affect terminal behavior until enabled.
Set in .env:
REMOTE_ENABLED=true
REMOTE_AGENT_URL=http://127.0.0.1:3390
REMOTE_DEFAULT_MODE=viewWhen REMOTE_ENABLED=true, the main npm start command installs missing sidecar dependencies and starts remote-agent automatically.
View only: stream only, no input executionControl enabled: mouse/touch/keyboard routed to desktopOpen Keyboardbutton (mobile-friendly) explicitly summons software keyboard for remote typingFullscreen+ zoom/pan/minimap controls make widescreen desktops usable from phones- Quick Controls overlay now includes:
- collapsible/draggable launcher when hidden
- one-tap shortcut buttons (mouse, arrows, common desktop chords)
Touch Mouse On/Offtoggle for touch-to-mouse behavior
- iOS true fullscreen path:
- If opened in Safari tab, the fullscreen button shows Home Screen install guidance
- Use
Share -> Add to Home Screen, then launch the app icon for stable standalone fullscreen with touch controls
If the sidecar is offline or input automation is unavailable, the UI degrades to view-only/offline states and the terminal remains fully usable.
The command sidecar is separate from the main server so it can fetch, pull, and start the main server even when the main app is not already running. It binds to loopback, requires a bearer token, and is meant to be exposed privately with Tailscale Serve.
On Windows:
npm --prefix command-sidecar run token
$env:COMMAND_SIDECAR_TOKEN = "<generated-token>"
$env:COMMAND_SIDECAR_ROOTS = "C:\Users\yagof\Projects\codex-shared-online-cli"
$env:COMMAND_SIDECAR_BASE_PATH = "/cmd"
npm run command-sidecarExpose it through Tailscale Serve:
cd command-sidecar
.\tailscale-serve.ps1 -Path /cmd -Port 3777From Mac:
export WINDOWS_COMMAND_URL="https://desktop-cguakc2.tailbca5e0.ts.net/cmd"
export WINDOWS_COMMAND_TOKEN="<generated-token>"
npm run windows:command -- --powershell --cwd 'C:\Users\yagof\Projects\codex-shared-online-cli' 'git fetch --all --prune; git pull --ff-only'
npm run windows:command -- --powershell --timeout-ms 0 --cwd 'C:\Users\yagof\Projects\codex-shared-online-cli' 'npm start'See command-sidecar/README.md for the REST API and more examples. The Tailscale docs describe tailscale serve --set-path for path-based routing to loopback HTTP services: https://tailscale.com/kb/1242/tailscale-serve
- Start everything:
npm install
npm start- Open the printed
https://<node>.<tailnet>.ts.netURL on desktop or iPhone while Tailscale is connected. - Create/select a PowerShell session in each web client using the session picker.
- Open the same session from another client to mirror the live console over WebSocket.
- Keep
SINGLE_CONSOLE_MODE=false(default). - Use
New PowerShellto create separate terminal sessions. - PowerShell sessions are managed through the app/web socket and can be mirrored by selecting the same session from another client.
- On iPhone, choose the matching terminal in the Console controller to mirror that same console.
- Setup guide:
docs/tailscale-setup.md - Windows helper script:
scripts/tailscale-serve.ps1
Quick command example:
npm start- If startup fails with
@lydell/node-pty ... could not find the binary package, reinstall dependencies in the same environment where you run the server:
rm -rf node_modules package-lock.json
npm installGET /api/healthGET /api/sessions- Response includes
singleConsoleMode,defaultTerminalProfile, andterminalProfiles; each terminal snapshot includesterminalProfileandbackend
- Response includes
GET /api/sessions/:sessionIdPOST /api/sessions- Optional body:
{ "terminalProfile": "powershell" }
- Optional body:
POST /api/sessions/:sessionId/restartDELETE /api/sessions/:sessionIdPOST /api/sessions/:sessionId/commandGET /api/remote/statusGET /api/remote/capabilities- Includes stream presets, supported desktop actions, display metadata, and live remote gateway stats
WS /ws/remote?mode=view|control- Optional query params:
fps,quality - Client control messages:
set-mode,set-stream,input,ping
- Optional query params:
PORT(default:3000)REMOTE_ENABLED(default:false; enables backend remote proxy + UI tab)REMOTE_AGENT_URL(default:http://127.0.0.1:3390)REMOTE_DEFAULT_MODE(view|control, default:view)REMOTE_STREAM_FPS(default:10)REMOTE_JPEG_QUALITY(default:62)REMOTE_INPUT_RATE_LIMIT_PER_SEC(default:120)REMOTE_INPUT_MAX_QUEUE(default:300)COMMAND_SIDECAR_PORT(default:3777; used bynpm run command-sidecar)COMMAND_SIDECAR_BASE_PATH(default: empty; use/cmdwhen exposing through Tailscale Serve path routing)COMMAND_SIDECAR_TOKEN(required unlessCOMMAND_SIDECAR_ALLOW_NO_TOKEN=true)COMMAND_SIDECAR_ROOTS(semicolon-separated allowed working directories)MAX_SESSIONS(default:24)SESSION_IDLE_TIMEOUT_MS(default:2700000)SESSION_SWEEP_INTERVAL_MS(default:60000)DEFAULT_COLS(default:120)DEFAULT_ROWS(default:30)POWERSHELL_COMMAND(default on Windows:powershell.exe, elsewhere:pwsh)POWERSHELL_ARGS(default:-NoLogo)PTY_CWD(working directory for new sessions)SINGLE_CONSOLE_MODE(default:false; when set totrue, forces one shared terminal and disables create/delete)SESSION_STATE_FILE(default:<project>/.online-cli/sessions-state.json; persisted session metadata used to restore sessions after restart)WS_HEARTBEAT_MS(default:30000)LOG_LEVEL(debug|info|warn|error, default:info)
server.js: startup entry pointscripts/start.js: Tailscale Serve + optional remote sidecar orchestratorsrc/server.js: app bootstrap + graceful shutdownsrc/config.js: runtime config parsingsrc/network/tailscaleAccess.js: loopback/Tailscale source and same-origin access checkssrc/http/remoteRoutes.js: remote status/capability endpointssrc/sessions/: native PowerShell PTY runtime and managersrc/ws/sessionGateway.js: WebSocket routing and heartbeatssrc/ws/remoteGateway.js: tailnet-gated remote stream/control websocket proxysrc/remote/remoteClient.js: sidecar health/connection helpersrc/http/sessionRoutes.js: session APIpublic/: browser app and stylesremote-agent/: Windows host sidecar service (desktop capture + input automation)command-sidecar/: token-gated Windows command runner for tailnet automationscripts/windows-command.js: local client for the command sidecardocs/tailscale-setup.md: private tailnet access guidescripts/tailscale-serve.ps1: Windows helper to configuretailscale serve