Skip to content

Repository files navigation

ftown

A remote CLI agent orchestrator that lets you manage and stream Claude Code, Cursor Agent, Codex, Grok, Pi, Kimi Code, opencode, and shell sessions through a web dashboard. Terminal streaming climbs a transport ladder — local loopback, then WebRTC P2P, then Centrifugo — so output stays on your machine or network whenever possible, and automatically upgrades back to a direct connection once it can. Recurring agent work runs as scheduled loops: cron or interval triggers that spawn full sessions with guardrails, instead of a hand-rolled polling script.

Demo

Desktop

demo-desktop.mp4

Mobile

demo-mobile.mp4

Architecture

                        ┌─────────────┐
                   ┌────┤  Centrifugo  ├────┐
                   │    │  (pub/sub)   │    │
              WebSocket └─────────────┘ WebSocket
       (control · signaling · rung 3: cloud fallback)
                   │                     │
                   ▼                     ▼
            ┌─────────────┐       ┌─────────────┐
            │   Browser   │◄─────►│   Bridge    │
            │  (Next.js)  │       │ (node-pty)  │
            └─────────────┘       └─────────────┘
       rung 1: loopback WS — ws://127.0.0.1 (same machine)
       rung 2: WebRTC DataChannel — direct P2P (localhost / LAN)
     self-healing: browser keeps retrying rung 1 → 2 while on rung 3
  • UI (/ui) — Next.js 15 web dashboard with real-time terminal streaming, climbing the transport ladder (local → P2P → Centrifugo) to reach the bridge
  • Bridge (/bridge) — CLI tool that runs on remote machines, spawns processes via PTY, serves a loopback WebSocket for same-machine access, and relays I/O directly to the browser over WebRTC (or through Centrifugo when neither direct rung is possible)
  • Centrifugo (/centrifugo) — WebSocket messaging server handling sessions, presence, and signaling, and serving as the last-resort (rung 3) data plane for terminal I/O

Features

Direct transport ladder

  • Every session tries three rungs in order, falling through automatically: Local (loopback WebSocket, same machine, plain TCP so it survives VPNs/endpoint filters like Cloudflare WARP that block UDP/WebRTC) → P2P (WebRTC DataChannel over LAN/localhost, STUN only, no TURN) → Cloud (Centrifugo relay, the reliable fallback used only when both direct rungs fail)
  • Live Local / P2P / Cloud badges in the terminal UI, each with a hoverable diagnostic tooltip explaining exactly why you're on that rung (e.g. "P2P unavailable — connection blocked (VPN or firewall may be interfering)")
  • Self-healing auto-upgrade: while on Cloud, the browser keeps retrying the ladder in the background (backoff 15s → 30s → 60s → 120s with jitter, plus immediate retry on network-online / tab-visible events) and seamlessly flips a live session from Cloud to Local/P2P with a full screen resync — no reload
  • Local-first, private transport: terminal output never touches the cloud while you're on the same machine or network; Centrifugo relays only while a remote viewer is actively watching
  • Loopback WS is gated by a per-process random nonce plus Origin check; presence adverts are scoped to the owning user's JWT

Loops

  • First-class scheduled recurring agent runs — interval or cron+timezone — each firing a normal session with guardrails (preflight/postflight, max runtime, overlap/retention policy); see Loops below

Everything else

  • Seven coding-agent CLIs plus raw shells as full interactive sessions
  • Parent/child agent trees, durable cross-session mail, and session reparenting
  • Native resume support for Claude, Cursor, Codex, and Pi; workdir-based continuation for Kimi Code
  • Live per-session token/model usage for harnesses with structured native session logs
  • Hook forwarding to the dashboard (Claude/Codex settings, Cursor hooks, and ftown's bundled Pi extension)
  • Native Pi tools for session discovery/control, durable ftown mail, token usage, terminal inspection, archives, and loop management
  • Multiple concurrent sessions with session management
  • Multi-bridge support (connect multiple machines)
  • Mobile-optimized responsive UI
  • Authentication via NextAuth with rate-limited login
  • Auto-refreshing bridge tokens (30-day refresh tokens)
  • Connection diagnostics overlay for troubleshooting

Loops

Loops replace fragile "keep an agent awake and polling" patterns with a durable scheduled primitive — cron for agents, with the full session UX (watch live, scroll back, take over) on every run.

Each loop is a schedule + harness + task prompt owned by the bridge; every fire spawns a normal ftown session, grouped under the loop in the dashboard.

ftown-sessions loop create \
  --cron "0 9 * * 1-5" --tz America/New_York \
  --harness claude --workdir ~/projects/ftown \
  --preflight "git fetch && git log origin/main..HEAD --oneline" \
  "Review overnight commits on main: {{preflight}}. Flag anything that needs attention."
  • Schedules — interval (--every 30s|5m|2h|1d) or cron with timezone (--cron "0 9 * * 1-5" --tz America/New_York); create via the dashboard's loop modal or the CLI
  • Harness choice — claude, cursor, codex, grok, pi, kimi-code, opencode, or plain shell, each with its own configurable workdir and model
  • Guardrails--preflight <cmd> (a non-zero exit skips the run; its stdout is injected into the prompt via {{preflight}}), --postflight <cmd> (receives run status, session id, and output), --max-runtime to force-stop a run
  • Overlap & retention — overlapping runs are skipped by default (--allow-overlap to permit them); retention keeps only the newest N runs
  • Lifecycle — pause/resume, fire a one-shot run manually, edit a loop live, and see run history with status dots (running/done/error/skipped/paused) plus next-due time in the dashboard

Prerequisites

  • Node.js 22+
  • PostgreSQL database (e.g., Neon)
  • Centrifugo v5 server
  • On bridge machines: install and authenticate whichever agent CLIs you plan to run. For Pi: npm install -g @mariozechner/pi-coding-agent, then run pi and /login (or provide a supported provider API key).

Quick Start

1. Centrifugo

docker run -d --name centrifugo \
  -p 8000:8000 \
  -v $(pwd)/centrifugo/config.json:/centrifugo/config.json \
  centrifugo/centrifugo:v5

2. UI

cd ui
cp .env.local.example .env.local
# Edit .env.local with your Centrifugo secret, database URL, etc.
npm install
npm run dev

Create the database tables:

-- Run against your PostgreSQL database
\i schema.sql

3. Bridge

cd bridge
npm install
npm run build

Start a bridge with a bootstrap token. In the dashboard, click Generate bridge token (it fetches a fresh, short-lived ~10 min token from the session-gated POST /api/auth/bridge/bootstrap, audience ftown:bridge-bootstrap, and copies it to your clipboard):

npx ftown-bridge --token <bootstrap-jwt> --api-url http://localhost:3000

The bootstrap token is used once to onboard the bridge; it is then exchanged for a 30-day rotating refresh token stored under the bridge data dir (~/.ftown/data/refresh-token, 0600). Restarts resume from that token, so the bootstrap token does not need to stay valid — generate a fresh one each time you onboard a new bridge.

On start, the bridge auto-deploys a local harness CLI:

  • ~/.ftown/bin/ftown-harness — list/tail/grep sessions, resolve workspace via here
  • ~/.ftown/harness-agent.md — agent cheat sheet (regenerated each start)
  • ~/.ftown/bridge.json — port + token while bridge is running
~/.ftown/bin/ftown-harness status
~/.ftown/bin/ftown-harness here -n 20
~/.ftown/bin/ftown-harness ls

Configuration

Environment Variables (UI)

Variable Description
CENTRIFUGO_TOKEN_SECRET Shared HMAC secret (must match Centrifugo config)
NEXT_PUBLIC_CENTRIFUGO_URL WebSocket URL for Centrifugo
AUTH_SECRET NextAuth.js secret (npx auth secret)
DATABASE_URL PostgreSQL connection string

Centrifugo

Development config is at centrifugo/config.json. For production, create a config.prod.json (gitignored) with real secrets and TLS settings.

Deployment

Local Network

Run all three components on a single machine accessible to your LAN.

1. Generate secrets:

# Generate a strong HMAC secret for Centrifugo + UI
openssl rand -base64 64
# Generate an auth secret for NextAuth
npx auth secret

2. Start PostgreSQL (or use a hosted service like Neon):

docker run -d --name ftown-db \
  -p 5432:5432 \
  -e POSTGRES_USER=ftown \
  -e POSTGRES_PASSWORD=changeme \
  -e POSTGRES_DB=ftown \
  -v ftown-pgdata:/var/lib/postgresql/data \
  postgres:16

# Create tables
psql postgresql://ftown:changeme@localhost:5432/ftown -f ui/schema.sql

3. Start Centrifugo:

Edit centrifugo/config.json — replace token_hmac_secret_key and api_key with your generated secrets. Update allowed_origins to include your LAN IP:

"allowed_origins": ["http://192.168.1.100:3000"]
docker run -d --name centrifugo \
  -p 8000:8000 \
  -v $(pwd)/centrifugo/config.json:/centrifugo/config.json \
  centrifugo/centrifugo:v5

4. Start the UI:

cd ui
cp .env.local.example .env.local

Edit .env.local:

CENTRIFUGO_TOKEN_SECRET=<same secret as centrifugo config>
NEXT_PUBLIC_CENTRIFUGO_URL=ws://192.168.1.100:8000/connection/websocket
AUTH_SECRET=<your nextauth secret>
DATABASE_URL=postgresql://ftown:changeme@localhost:5432/ftown
npm install
npm run build
npm start  # Runs on port 3000

5. Connect a bridge from any machine on the LAN:

npx ftown-bridge --token <jwt> --api-url http://192.168.1.100:3000

Get the token via Generate bridge token in the dashboard at http://192.168.1.100:3000 (see Quick Start above).


Hosted production

The target production layout uses Fly.io for Centrifugo, Vercel for the UI, and Neon for the database. See the hardened, rollback-safe Fly.io rollout runbook.

Legacy Centrifugo on EC2

The EC2 instructions below are retained for rollback and self-hosting. New hosted deployments should use the Fly.io runbook above.

1. Launch an EC2 instance (t3.micro is sufficient):

  • Amazon Linux 2023 or Ubuntu 22.04
  • Security group: allow inbound TCP 80, 443 from 0.0.0.0/0
  • Attach an Elastic IP for a stable address

2. Install Docker and start Centrifugo:

sudo yum install -y docker  # Amazon Linux
sudo systemctl enable --now docker

# Create config directory
sudo mkdir -p /opt/centrifugo

3. Create production config at /opt/centrifugo/config.json:

{
  "token_hmac_secret_key": "<your-secret>",
  "api_key": "<your-api-key>",
  "admin": false,
  "token_audience": "ftown:centrifugo",
  "allowed_origins": ["https://your-domain.vercel.app"],
  "allow_anonymous_connect_without_token": false,
  "allow_user_limited_channels": true,
  "allow_publish_for_subscriber": false,
  "allow_subscribe_for_client": false,
  "allow_history_for_subscriber": true,
  "presence": true,
  "join_leave": true,
  "force_push_join_leave": true,
  "allow_presence_for_subscriber": true,
  "history_size": 500,
  "history_ttl": "24h",
  "force_recovery": true,
  "namespaces": [
    {
      "name": "terminal",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true,
      "allow_history_for_subscriber": true,
      "history_size": 10000,
      "history_ttl": "24h",
      "force_recovery": true
    },
    {
      "name": "sessions",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true,
      "history_size": 0,
      "history_ttl": "0s",
      "force_recovery": false
    },
    {
      "name": "terminal-input",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true
    },
    {
      "name": "events",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true,
      "history_size": 100,
      "history_ttl": "1h"
    },
    {
      "name": "commands",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true
    },
    {
      "name": "bridges",
      "allow_publish_for_subscriber": true,
      "allow_subscribe_for_client": true,
      "allow_user_limited_channels": true,
      "presence": true,
      "join_leave": true,
      "force_push_join_leave": true,
      "allow_presence_for_subscriber": true
    }
  ],
  "tls_autocert": true,
  "tls_autocert_host_whitelist": "centrifugo.yourdomain.com",
  "tls_autocert_cache_dir": "/centrifugo/autocert",
  "tls_autocert_http": true,
  "tls_autocert_http_addr": ":80",
  "address": "0.0.0.0",
  "port": 443,
  "internal_port": 9000,
  "client_channel_limit": 256,
  "ping_interval": "10s",
  "pong_timeout": "5s",
  "log_level": "info"
}

4. Run Centrifugo with TLS:

sudo docker run -d --name centrifugo \
  --restart unless-stopped \
  -p 80:80 -p 443:443 \
  -v /opt/centrifugo/config.json:/centrifugo/config.json \
  -v /opt/centrifugo/autocert:/centrifugo/autocert \
  centrifugo/centrifugo:v5

5. Point DNS — Create an A record for centrifugo.yourdomain.com pointing to the Elastic IP. Centrifugo handles Let's Encrypt TLS automatically via tls_autocert.

UI on Vercel

1. Push to GitHub and import the repo in Vercel.

2. Set root directory to ui in project settings.

3. Add environment variables in Vercel dashboard:

Variable Value
CENTRIFUGO_TOKEN_SECRET Same secret as Centrifugo config
NEXT_PUBLIC_CENTRIFUGO_URL wss://centrifugo.yourdomain.com/connection/websocket
AUTH_SECRET Your NextAuth secret
DATABASE_URL Your Neon connection string

4. Deploy. Vercel auto-deploys on push to main.

Database on Neon

  1. Create a project at neon.tech
  2. Run ui/schema.sql against the database once for a fresh install (use the Neon SQL editor or psql)
  3. Copy the connection string to Vercel env vars
Migrations (auto-applied on merge to main)

Incremental schema changes live in ui/migrations/*.sql. They are applied automatically by the .github/workflows/migrate.yml GitHub Actions workflow on every push to main (and via manual workflow_dispatch) — no more manual "apply the SQL to Neon before deploy" step.

One-time setup: add a repo secret MIGRATION_DATABASE_URL = a Neon direct postgresql:// connection string (the standard TCP host, not the @neondatabase/serverless HTTP host the app uses). The runner (ui/scripts/migrate.mjs) tracks applied files in a schema_migrations table, runs each pending migration in a single transaction, and is idempotent (safe to re-run, and safe even if a migration was already applied by hand). The workflow runs concurrently with Vercel's deploy on the same push; migrations are kept additive so that window is safe.

Bridge

On any machine where you want to run Claude or Cursor Agent sessions:

npx ftown-bridge --token <jwt> --api-url https://your-domain.vercel.app

Development

# Terminal 1: Centrifugo
docker run -p 8000:8000 -v $(pwd)/centrifugo/config.json:/centrifugo/config.json centrifugo/centrifugo:v5

# Terminal 2: UI
cd ui && npm run dev

# Terminal 3: Bridge
cd bridge && npm run dev -- --token <jwt> --api-url http://localhost:3000

License

MIT

About

AI Orchestrator that streams claude/codex/cursor CLI into your browser (and your phone) from/to anywhere in the world (including from multiple computers)

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages