npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

termfleet

v0.2.0

Published

An ergonomic control center for AI terminal sessions (Claude Code, Codex, Gemini) across tools, projects, and hosts.

Readme

termfleet

An ergonomic control center for the AI coding sessions you run — Claude Code, Codex, and Gemini — across tools, projects, and machines.

termfleet is an orchestration layer over the agents already running in your terminals: one place to see which sessions need you and act on any of them by name, with a visual fleet canvas when you want it. It attaches to the real terminals on your host(s) — iTerm2, WezTerm, or a managed virtual-tmux server — and gives every session a durable identity you can list, triage, read, reply to, wait on, continue, and close, from a CLI built for an agent to drive, an SDK, or a browser console. It is not another "spawn N isolated agents" runner; it controls the agents you already have.

Quickstart (local, no Docker/registry/account)

Install the CLI (Node 20+):

npm install -g termfleet

Then start a console + one local provider and launch a session:

# a console (UI + aggregator) and one local provider:
termfleet console serve --name dev --port 7373 &
termfleet provider serve --kind virtual-tmux --prefix dev --count 1 --port 7402 &

# the CLI auto-discovers the local provider — no --url needed:
termfleet claude new --prompt "hello"               # launch a Claude session
termfleet sessions recent --live --sort attention   # which sessions need you?

Open http://127.0.0.1:7373 for the visual console. That's the whole local experience. (termfleet --help lists every command; termfleet --version prints the version.)

Working from a clone (contributing)? You don't need a global install — run the CLI straight from source with npx tsx src/cli.ts … in place of termfleet …:

git clone https://github.com/volter-ai/termfleet.git
cd termfleet && npm install && npm run build   # build powers the browser console UI
npx tsx src/cli.ts console serve --name dev --port 7373 &

Concepts

  • Console — the UI + aggregator: serves the board, proxies providers, merges the fleet across hosts. One per user.
  • Provider — a process that owns terminal windows on a host (iTerm2 mirror, WezTerm, or virtual-tmux). The console talks to many.
  • Sessionthe unit you actually work with: a Claude/Codex/Gemini conversation, addressed by agentSessionId — top-level sessions, send, and close, plus per-agent claude|codex|gemini get|wait|continue — never by hunting for a terminal id.

What it's good at — and what it isn't

The edge is the combination: cross-host + cross-tool (Claude/Codex/Gemini) + a visual canvas + optional team/org sharing, all through one session-first model. Single-host triage of Claude sessions alone is also free in Claude Code's Agent View; termfleet's value is spanning hosts, tools, and people.

Per-tool capability today:

| tool | discover / read | launch (new) | wait / get | continue | |---|---|---|---|---| | Claude | ✅ | ✅ | ✅ | ✅ | | Codex | ✅ | ✅ (no caller-bound session id) | ✅ | ✖ (no --resume) | | Gemini | ✅ | ✅ | ✅ | ✖ (resumes by index, not id) |

Project status: pre-1.0, moves fast, primarily one maintainer, heavily AI-assisted. The macOS native path (iTerm2 via osascript) is most-tested; the portable path is virtual-tmux/Docker; WezTerm-on-WSL is unvalidated. Providers run real processes and serve host files — treat "provider reachable" as "full host access granted." Default is loopback-only; read SECURITY.md before exposing anything.

Requirements

  • Minimum (local): Node.js 20+ and tmux (brew install tmux).
  • To launch agents: the agent CLI you'll use — claude, codex, or gemini — installed on PATH and logged in. The Quickstart's first claude new fails after ~45s against a missing or not-logged-in CLI, so install and sign in first (e.g. run claude, then /login).
  • Optional, per feature: Docker (Docker workers) · macOS + iTerm2 (iTerm provider) · a deployed registry (team/remote — see below).

Team & remote (opt-in): Docker Worker, Registry, Tunnel

This is the recommended path for a new user who wants terminals in Docker, shared team discovery, and a dry run before handing the setup back to a human.

The registry is opt-in. With TERMFLEET_REGISTRY_URL unset, termfleet is local-only and makes no outbound requests — that is the default experience (see the local Quickstart above). The steps in this section apply only if you have deployed and configured your own registry.

Deploy + seed it first (the worker has no sign-up flow — accounts come from seeded users set at deploy time). From packages/registry-worker:

npx wrangler secret put TERMFLEET_SEEDED_PASSWORD --env=""   # required; no default
npx wrangler secret put TERMFLEET_SEEDED_USERS    --env=""   # JSON array of your team's accounts
npx wrangler deploy --env=""

See packages/registry-worker/README.md for the TERMFLEET_SEEDED_USERS shape. Then point TERMFLEET_REGISTRY_URL at the deployed worker's /providers endpoint, and sign in (step 3) as one of those seeded emails:

TERMFLEET_REGISTRY_URL=https://your-registry.example.com/providers

The Docker worker is still your machine's responsibility. The registry stores auth, organization membership, shared provider records, canvas layout/comments, and short-lived provider access tokens. It does not run Docker or tmux.

1. Install

git clone https://github.com/volter-ai/termfleet.git
cd termfleet
npm install
npm run build

Make sure Docker is running before starting a Docker worker. If you use Colima, start it first and export the Docker socket if your shell needs it:

colima start
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"

2. Start A Local Console

Run the console locally, pointed at your registry via TERMFLEET_REGISTRY_URL (the registry is opt-in; without this env var the console is local-only).

TERMFLEET_REGISTRY_URL="https://your-registry.example.com/providers" \
  npx tsx src/cli.ts console serve \
  --name local-console \
  --host 127.0.0.1 \
  --port 7373 \
  --local-adapters none

Open:

http://127.0.0.1:7373/?session=local-console#view=providers

3. Sign In To The Registry

Use the browser sign-in flow or the CLI. The CLI stores its session in ~/.termfleet/registry-auth.json and uses the same registry as the console.

npx tsx src/cli.ts registry login \
  --identifier [email protected] \
  --password-stdin

Confirm the active user and organization:

npx tsx src/cli.ts registry session
npx tsx src/cli.ts registry list

4. Start Your Docker Worker

From the console UI, click Start or register provider, then Start Docker worker.

The same action is available from the CLI:

npx tsx src/cli.ts registry start-docker-worker \
  --label "My Docker Worker"

This builds/starts the Compose worker and registers http://127.0.0.1:7374 with your local console. The worker runs as the unprivileged termfleet user, and created sessions start in /workspace.

The raw Docker provider URL is an API endpoint for the console and CLI. Do not expect http://127.0.0.1:7374/ to be the full app. Use the console at http://127.0.0.1:7373/ to see and control it.

5. Dry Run Locally

Use the browser first. In Playwright or a normal browser:

  1. Open http://127.0.0.1:7373/?session=local-console#view=providers.
  2. Confirm My Docker Worker appears as connected.
  3. Confirm the canvas shows a Virtual tmux Space.
  4. Create or open a terminal, then type:
whoami && pwd && touch /workspace/termfleet-dryrun-ok && ls /workspace/termfleet-dryrun-ok

Expected output includes:

termfleet
/workspace
/workspace/termfleet-dryrun-ok

Then verify the CLI can reach the same worker:

npx tsx src/cli.ts providers health --url http://127.0.0.1:7374
npx tsx src/cli.ts providers snapshot --url http://127.0.0.1:7374
npx tsx src/cli.ts list --url http://127.0.0.1:7374

For raw shell repair-only diagnostics, create a temporary shell and capture it:

npx tsx src/cli.ts new --repair \
  --url http://127.0.0.1:7374 \
  --break-glass \
  --cwd /workspace \
  --command "whoami && pwd"

Use capture on the returned terminalId value.

6. Log In Inside The Docker Worker

The Docker worker has isolated volumes for /home/termfleet and /workspace. Credentials for Claude Code, Codex, GitHub CLI, SSH, and workspace files live inside those Docker volumes, not on the host unless you explicitly mount them.

Use the console terminal for interactive logins:

claude /login
codex
gh auth login

Or open a repair shell from the CLI when the UI is not enough:

npx tsx src/cli.ts new --repair \
  --url http://127.0.0.1:7374 \
  --break-glass \
  --cwd /workspace \
  --command "gh auth status"

After login, prefer agent windows for normal work:

npx tsx src/cli.ts claude new \
  --url http://127.0.0.1:7374 \
  --name claude-worker \
  --cwd /workspace \
  --prompt "Inspect the workspace and report setup status."

npx tsx src/cli.ts codex new \
  --url http://127.0.0.1:7374 \
  --name codex-worker \
  --cwd /workspace \
  --prompt "Inspect the workspace and report setup status."

7. Make Your Docker Worker Available To The Team

Public/shared providers must be authenticated. Start a provider that requires a registry-issued provider token, expose it with an HTTPS tunnel, then register the tunnel URL as a shared provider.

Critical: set TERMFLEET_PROVIDER_PUBLIC_URL to the provider's public tunnel origin (the same URL you register below). That is what switches the provider into shared mode — accepting only org-scoped provider tokens minted for the owning org. Without it, a public provider falls back to bare-session auth and any signed-in registry user of any org can reach it. You must therefore decide your tunnel origin first (reserve the subdomain).

Start an auth-required Docker provider on a local port (replace the public URL with your reserved tunnel origin):

docker run --rm \
  --name termfleet-team-worker \
  -p 127.0.0.1:7374:7373 \
  -e TERMFLEET_AUTH_REGISTRY_URL=https://your-registry.example.com \
  -e TERMFLEET_ALLOWED_ORIGINS=https://your-registry.example.com \
  -e TERMFLEET_PROVIDER_PUBLIC_URL=https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.com \
  termfleet:local \
  node /app/dist/cli.js provider serve \
    --kind virtual-tmux \
    --host 0.0.0.0 \
    --port 7373 \
    --prefix termfleet-docker \
    --count 0 \
    --cwd /workspace \
    --tunnel

Expose 127.0.0.1:7374 through any HTTPS tunnel. For most users a standalone tunnel like ngrok or cloudflared is the simplest option:

ngrok http 7374

If you run your own tunnel relay instead, point TERMFLEET_LOCAL_TUNNEL_BIN at your own tunnel client binary and set TERMFLEET_TUNNEL_SERVER_URL to the relay URL (the --tunnel flag then spawns that client for you). Whatever client you use, it must forward to the provider port and not add its own auth gate here; the Termfleet provider is already enforcing registry-issued provider tokens because it was started with --tunnel.

Register the HTTPS tunnel URL in the active registry organization:

npx tsx src/cli.ts registry register-shared \
  --url https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.com \
  --label "Your Name Docker Worker" \
  --requires-auth

Team members in the same active organization will see it under Shared Providers in the cloud console and any local console using the same registry.

8. Use It Remotely

Open the cloud console from another machine:

https://your-registry.example.com#view=providers

Sign in, confirm the shared provider is connected, then use the canvas normally.

The CLI can also use shared providers by name after login:

npx tsx src/cli.ts registry login --identifier [email protected] --password-stdin
npx tsx src/cli.ts providers snapshot --provider "Your Name Docker Worker"
npx tsx src/cli.ts claude new \
  --provider "Your Name Docker Worker" \
  --cwd /workspace \
  --prompt "Confirm this remote Docker worker is usable."

Remove a shared provider when the tunnel or worker is no longer meant to be available:

npx tsx src/cli.ts registry unregister-shared \
  --url https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.com

Feature Guide

Termfleet exposes the same provider features through the browser console, CLI, and SDK.

  • Providers: start local virtual tmux, Docker, iTerm, or WezTerm providers; register local URLs; register shared team URLs; remove stale providers.
  • Canvas: pan, zoom, fit view, inspect provider groups, and arrange terminal windows.
  • Terminal windows: create agent windows, move/resize windows, close windows, capture output, and send input.
  • Agents: launch Claude Code, Codex, or Gemini in provider-owned terminals and read structured session state with claude get, codex get, gemini get, and wait.
  • Files: read, write, stat, and copy files through provider filesystems.
  • Folders/files: open folder and file views on the provider canvas.
  • Iframes: open an HTTP app or preview URL on the provider canvas.
  • Multiplayer: registry-backed canvas layout, comments, presence, cursors, viewport, and selection sync are scoped to the active organization.
  • Security: shared public providers should use provider auth (--tunnel plus shared registration with --requires-auth); raw terminal shells are repair-only with --break-glass.

Provider Model

Console / CLI
  -> ProviderClient SDK
    -> Provider Instance URL
      -> ProviderEngine
        -> ItermDriver, VirtualTmuxDriver, or WeztermDriver
          -> iTerm, tmux virtual display, or WezTerm terminals

The console server owns the browser UI, registry/auth, and local provider materialization. Provider servers own terminal/window/display state only. The CLI uses the same ProviderClient SDK as the console for provider operations, so it does not need the console server for normal provider control.

Console + providers, in depth

A deeper look at the local setup the Quickstart covers: run the console on your computer, start one or more provider servers, then drive them from the console or CLI. No registry and no tunnel are required — leave TERMFLEET_REGISTRY_URL unset and termfleet makes no outbound requests. Locally the console auto-discovers running providers (and the CLI auto-resolves a target), so explicit registration is optional — the SDK/CLI registration shown below is for when you want to register a provider explicitly or by a remote URL.

The numbered walkthrough below runs from a clone (npx tsx src/cli.ts …); with a global install, substitute termfleet … (no npm install/npm run build).

For the fastest path into a ready control session, use the idempotent chat command:

termfleet chat

chat ensures the local console is running, ensures the configured local providers exist, optionally opens or focuses the console, then reuses or starts the configured Claude/Codex chat inside a Termfleet provider. Flags override saved settings for one run; add --save to make those choices the new default:

termfleet chat --save --agent claude --agent-provider iterm --providers docker-worker,iterm --open-console
termfleet chat --agent none --providers virtual-tmux --no-open-console
  1. Start the console:
npm install
npm run build
npx tsx src/cli.ts console serve --name local-console --host 127.0.0.1 --port 7373
  1. Start a provider server:
npx tsx src/cli.ts provider serve --kind virtual-tmux --prefix termfleet-virtual --count 2 --host 127.0.0.1 --port 7376
  1. (Optional — local providers are auto-discovered.) To register a provider with the console explicitly (e.g. a remote URL), use the SDK:
import { ProviderClient, providerRefFromUrl } from "termfleet";

const consoleClient = new ProviderClient(providerRefFromUrl("http://127.0.0.1:7373", "Local console"));

const provider = await consoleClient.registerLocalProvider({
  baseUrl: "http://127.0.0.1:7376",
  label: "Virtual tmux"
});

console.log({ provider });
consoleClient.disconnect();

The console can also materialize local providers itself through startLocalProvider. Docker workers use the same provider protocol: the container runs a virtual tmux provider and the console registers that provider URL.

  1. Expose a provider with a tunnel before registering a shared URL:
ngrok http 7376

Use any tunnel or LAN URL that other users can reach. Keep both the provider and tunnel running while the provider is shared. Providers are unauthenticated by default; mark a provider as auth-required when its own --tunnel mode exposes it publicly.

  1. The CLI exposes the same SDK path as a convenience wrapper:
npx tsx src/cli.ts registry login --identifier [email protected] --password-stdin
npx tsx src/cli.ts registry start-docker-worker
npx tsx src/cli.ts registry register-shared \
  --url https://your-provider-tunnel.example.com \
  --alias your-worker \
  --label "Your Name Provider" \
  --requires-auth

The CLI stores its registry session token in ~/.termfleet/registry-auth.json. The console also uses this token for registry reads when the browser has not supplied its own signed-in session.

  1. Open the console:
open http://127.0.0.1:7373/

The console starts local iTerm and WezTerm adapters by default, so they appear under Console-Managed Providers when their backing terminal apps are available. Virtual tmux, Docker worker, and shared providers appear in their own sidebar groups after they are started or registered.

WezTerm on WSL/Windows (experimental)

On WSL/Windows the WezTerm provider is the native-terminal candidate. It's implemented and smoke-tested on macOS but not yet validated on a real WSL machine — keep the virtual-tmux provider as the WSL fallback until it is.

To try it: install WezTerm on Windows, use a WSL distro with node/npm/tmux and the termfleet checkout, and confirm wezterm.exe and powershell.exe are visible from WSL (it drives the terminal via wezterm.exe and Win32 window positioning via powershell.exe). Then:

npx tsx src/cli.ts provider serve --kind wezterm --prefix wsl-wezterm --count 1 --host 0.0.0.0 --port 7377

http://localhost:7377/healthz should return "provider":"wezterm". If enumeration or window positioning misbehaves from WSL, that's the unvalidated path — please file an issue (see CONTRIBUTING).

Start A Provider

A provider owns terminal windows on a host. Run one on its own port (distinct from the console's):

termfleet provider serve --kind virtual-tmux --prefix agent-a --count 2 --host 127.0.0.1 --port 7402

Open the provider canvas, or control it entirely from the CLI:

open http://127.0.0.1:7402/
termfleet providers health   --url http://127.0.0.1:7402
termfleet providers snapshot --url http://127.0.0.1:7402

Run The Console

Run a console when you want to see the Termfleet app in the browser and load providers from the local pool and shared registry.

termfleet console serve --name local-console --host 127.0.0.1 --port 7373

Open the app:

open http://127.0.0.1:7373/

The console automatically loads providers from .termfleet-registry.json and, when TERMFLEET_REGISTRY_URL is set, from the shared registry (it is opt-in; without that env var the console stays local-only). The Providers sidebar can register any reachable URL that implements the provider API. Provider names shown in the sidebar come from the registry's label field.

The sidebar can also start additional local providers for you. Start virtual tmux and Start Docker worker add those provider URLs to the console runtime provider list. They do not need to be written to .termfleet-registry.json because the console process owns those materialized providers directly. The local iTerm provider is on by default.

Start The Docker Worker

Use the Docker worker when you want Termfleet terminals to run inside a Docker container. The container exposes a normal virtual tmux provider endpoint to the console or CLI.

  1. Clone and install the project:
git clone https://github.com/volter-ai/termfleet.git
cd termfleet
npm install
  1. Start the Docker worker with Compose:
docker compose up --build

Compose publishes the provider at http://127.0.0.1:7374. The provider itself listens on port 7373 inside the container.

  1. Open the provider canvas:
open http://127.0.0.1:7374/

The Docker worker's virtual tmux provider should render its terminal windows in the browser.

To see the worker provider from a separate console, keep this Docker process running and register its reachable URL from the console's Providers sidebar.

To run without Compose:

npm run docker:build
docker run --rm \
  -p 127.0.0.1:7374:7373 \
  termfleet:local

By default the worker provider starts with no panels. Panels are created explicitly with claude new, codex new, folder open, or repair commands. To seed initial terminal panels for debugging or to change the prefix, run the provider CLI directly in the built image:

docker run --rm \
  -p 127.0.0.1:7374:7373 \
  termfleet:local \
  node dist/cli.js provider serve --kind virtual-tmux \
    --host 0.0.0.0 \
    --port 7373 \
    --prefix my-docker-worker \
    --count 2

Bundled Agent CLIs

The Docker image ships with agent tooling so the terminals inside the worker are ready to drive: the GitHub CLI (gh), Claude Code (@anthropic-ai/claude-code), and Codex (@openai/codex). The Gemini CLI is not bundled — install it in the worker (or a derived image) if you want gemini new there. The binaries are installed in the image; credentials live in the container filesystem or mounted home directory supplied when the worker is created.

The termfleet:local image does not include or mount local machine agent credentials. Each Docker worker gets its own Compose-project-scoped Docker volumes for /home/termfleet and /workspace, so Claude, Codex, GitHub CLI, SSH config, and workspace files are isolated per worker. Run claude /login, codex, or gh auth login inside the worker that should own those credentials.

Create Another Docker Worker

The Start Docker worker button starts the default worker at http://127.0.0.1:7374. To run another worker provider at a different local port, build the image and run a second container:

npm run docker:build
docker run --rm \
  -p 127.0.0.1:7405:7373 \
  termfleet:local \
  node dist/cli.js provider serve --kind virtual-tmux \
    --host 0.0.0.0 \
    --port 7373 \
    --prefix termfleet-docker-7405 \
    --count 2

This creates another Docker worker provider at http://127.0.0.1:7405. To make it available through the app, create a tunnel for port 7405 and register that tunnel URL.

Tunnel And Register A Provider

To share a provider with the team, expose its local port through a tunnel and register the tunnel URL under Shared Providers.

For the default Docker worker provider:

ngrok http 7374

For an additional worker provider running on port 7405:

ngrok http 7405

Copy the HTTPS forwarding URL from the tunnel output, then open the console at http://127.0.0.1:7373/ and register it in the Providers sidebar:

  • Section: Shared Providers
  • Name: a useful team-visible name, such as Alice Worker
  • URL: the tunnel HTTPS URL
  • Click Register

Registered tunnel URLs are written to the shared registry. The tunnel process and provider process both need to stay running while the team uses that provider.

Add It To The Registered Pool

A Termfleet console loads runtime local providers, console-local saved providers, and shared remote providers. Runtime local providers are the iTerm and WezTerm adapters started by the console process; they appear in the provider list while that console process is running and do not need to be saved to .termfleet-registry.json. Manually added local providers can still be stored in .termfleet-registry.json. Remote providers are read from and written to the shared registry URL you configure via TERMFLEET_REGISTRY_URL:

https://your-registry.example.com/providers

The registry is opt-in: with TERMFLEET_REGISTRY_URL unset, termfleet is local-only and never contacts a remote registry. Set it to point every user at the same persistent remote pool — deploy your own registry worker from packages/registry-worker. The registry must return either an array of provider records or an object with a providers array. Each provider needs a reachable baseUrl; label is optional but recommended.

A deployed registry worker is the normal team registry. Only run a local registry when developing the registry service itself:

npx tsx src/cli.ts serve-registry \
  --host 127.0.0.1 \
  --port 7401 \
  --file .termfleet-remote-registry.json

If you deliberately expose that local development registry, point any HTTPS tunnel at port 7401 with its own auth gate disabled so the registry service can handle /auth/* and /providers authentication itself:

ngrok http 7401

(ngrok and cloudflared are the simplest standalone tunnels. To use a self-hosted relay instead, set TERMFLEET_LOCAL_TUNNEL_BIN to your own tunnel client binary and TERMFLEET_TUNNEL_SERVER_URL to the relay URL.)

Keep both the registry server and the tunnel process running. The remote registry file should contain only remote or tunnel URLs, not localhost providers.

Open the console at http://127.0.0.1:7373/ and use the Providers sidebar. The sidebar groups providers by role:

  • Console-Managed Providers: console-started iTerm and WezTerm adapters.
  • Virtual Servers: local virtual tmux providers.
  • Docker Workers: Docker-hosted virtual tmux providers.
  • Shared Providers: remote provider URLs from the shared registry.

You only need a reachable provider URL; the person running the provider does not have to be the same person who registers it.

  • Name: Worker Provider or any display name you want.
  • URL: a tunnel URL for your provider or someone else's provider.
  • Requires auth: enable this only when the provider URL points at a termfleet provider serve --tunnel instance.
  • Click Register in the Shared Providers form.

The local iTerm and WezTerm start buttons materialize default loopback URLs into the console runtime provider list only. The register form writes tunnel URLs to the shared registry. Refreshing the console will load persisted local virtual tmux, Docker worker, and shared provider URLs from /api/registry/providers.

Termfleet uses one configured remote registry service with organization-scoped provider records. Switching the active organization changes which shared providers you see and manage. Removing a local provider deletes only this console's local registry entry. Unregistering a shared provider deletes that provider from the active organization's shared registry so other org members stop seeing it.

Example shared registry response:

{
  "providers": [
    {
      "baseUrl": "http://127.0.0.1:7374",
      "label": "Docker Worker"
    }
  ]
}

Use a URL that the intended console or CLI client can reach. A local console can register http://127.0.0.1:7374 into its local pool. To share the same provider with other users, register a LAN hostname or tunnel URL so their machines can reach it.

To use a different registry, start the console with TERMFLEET_REGISTRY_URL pointed at that registry endpoint:

TERMFLEET_REGISTRY_URL="https://registry.example.com/providers" \
  npx tsx src/cli.ts console serve --name local-console --port 7373

Open the console and it will automatically connect to every local provider plus every provider returned by the remote registry:

open http://127.0.0.1:7373/

Use the Providers sidebar to verify what the console sees.

Small Public Deployment

For a small public deployment, expose the console through tunnel mode instead of publishing the raw Termfleet port:

TERMFLEET_CONSOLE_TOKEN="a-strong-random-secret" \
TERMFLEET_ALLOWED_ORIGINS="https://your-console-tunnel.example.com" \
TERMFLEET_CONSOLE_ALLOWED_ORGS="org_your_org" \
  npx tsx src/cli.ts console serve --name public-console --port 7373 --tunnel --tunnel-id your-stable-console-id

A tunnel always has an auth backend — it never serves the fleet unauthenticated. The backend resolves by precedence: TERMFLEET_CONSOLE_TOKEN (a strong operator secret) → a configured registry (user accounts) → else the console auto-provisions a 6-digit access passcode and prints it at startup (chat prints it too). The passcode is a rate-limited login factor exchanged at /console-login — fine for personal use, but set TERMFLEET_CONSOLE_TOKEN (or use a registry) for anything shared or long-lived, where a strong secret beats a 6-digit code. Loopback access is always exempt; only remote/tunnel peers are gated. If a passcode leaks, run termfleet console rotate-passcode and restart the console.

Tunnel client setup

The simplest way to publish the console is a standalone tunnel — run ngrok or cloudflared yourself against the console port and open the HTTPS URL it prints:

ngrok http 7374

The console still gates remote peers as long as it was started with --tunnel (or otherwise bound to a non-loopback interface).

If you instead want the console to spawn and supervise the tunnel for you (so --tunnel brings it up automatically), point TERMFLEET_LOCAL_TUNNEL_BIN at your own tunnel client binary and set TERMFLEET_TUNNEL_SERVER_URL to your relay URL. Termfleet ships no tunnel client — this is the seam for plugging in your own.

If your client isn't a directly executable file — for example a script entry the shell can't run as-is — wrap it in a small launcher and point TERMFLEET_LOCAL_TUNNEL_BIN at the launcher:

mkdir -p ~/.termfleet/bin
cat > ~/.termfleet/bin/my-tunnel <<'SH'
#!/bin/sh
exec /path/to/your/tunnel-client "$@"
SH
chmod +x ~/.termfleet/bin/my-tunnel
export TERMFLEET_LOCAL_TUNNEL_BIN="$HOME/.termfleet/bin/my-tunnel"
export TERMFLEET_TUNNEL_SERVER_URL="https://your-relay.example.com"

These two values (plus TERMFLEET_REGISTRY_URL) are persisted: run termfleet chat --save once from a shell that has them set, and every later console serve/chat recovers them from ~/.termfleet/settings.json — so a console launched from a different shell still finds its relay and client.

Keep provider and Docker ports bound to 127.0.0.1. If you run the registry on a public interface, set TERMFLEET_SEEDED_PASSWORD and TERMFLEET_REGISTRY_ALLOWED_ORIGINS explicitly; the registry refuses public startup with the default seeded password.

AI Agent Workflow

termfleet is built to be driven by an AI orchestrator, and the model is session-first: you discover, read, and act on sessions by agentSessionId — you almost never name a terminal or window. (For browser-terminal validation, run the agent CLIs in their interactive TUI mode; don't use non-interactive shortcuts like claude -p / codex exec, which prove a different execution path than a user driving the terminal through termfleet.)

1. Triage — which sessions need you?

termfleet sessions recent --live --sort attention   # the attention / triage view
termfleet sessions search --query "deploy"           # find a session by content

One list across every provider, sorted by which sessions are waiting on you.

2. Read a session — by id, with a since-cursor for deltas:

termfleet claude get --agent-session-id claude:<uuid> --cwd /workspace
termfleet claude get --agent-session-id claude:<uuid> --since 12   # only items after #12

Or block until the session finishes its current turn — the primitive you build loops on:

termfleet claude wait --agent-session-id claude:<uuid> --after-count 5 --timeout-ms 600000

--after-count N waits for a turn after the Nth message, so a wait issued right after a send returns the new reply, not the one already there.

3. Act on a session — reply or close, keyed by id:

termfleet send  --agent-session-id claude:<uuid> --text "run the tests" --enter
termfleet close --agent-session-id claude:<uuid>

termfleet resolves session → window → terminal internally; you never supply a terminalId.

4. Launch or resume:

termfleet claude new --cwd /workspace --prompt "Inspect this repo and report before editing."
termfleet codex  new --cwd /workspace --name todo-codex
termfleet claude continue --agent-session-id claude:<uuid>   # resume an ended Claude session

claude new starts Claude Code with an explicit --session-id and prints it as the agentSessionId (pass --agent-session-id ID to choose it). codex new doesn't accept a launch-bound id yet, and continue is Claude-only (Codex has no --resume; Gemini resumes by index). The CLI is biased toward reuse — new refuses to add a panel when a reusable one exists and prints its capture so you can decide where to send next. --cwd is passed to the provider at creation time and must exist in the new terminal environment (for Docker workers, /workspace is the container filesystem).

For multiple machines, target a provider with --provider <alias> / --url <url> (localhost, a remote host, or a tunnel URL); with no target the CLI auto-resolves a local one. Auth stays off unless the provider record sets requiresAuth.

Break-glass: terminals and windows (plumbing)

Below the session layer is the raw transport — terminals and board windows. You should rarely touch it; these verbs name a terminalId / window id directly and exist for diagnosis and repair:

termfleet list --url <url>                                   # board windows
termfleet capture --url <url> --terminal-id <id> --lines 40  # raw pane capture
termfleet send --url <url> --terminal-id <id> --break-glass --text "pwd" --enter  # --break-glass required for a non-agent shell
termfleet terminals move --url <url> --id 1 --left 80 --top 80 --width 900 --height 520
termfleet close --url <url> --id 1                           # by window id

# raw shell creation — repair-only, when Claude/Codex can't start or the provider needs diagnosis:
termfleet new --repair --url <url> --break-glass --cwd /workspace --command "gh auth status"

MCP server

The same session-first control verbs the CLI exposes are also available over MCP, so any MCP-capable agent (Claude Code, Claude Desktop, or your own host) can drive the fleet — list sessions, read a transcript, reply, launch, continue, and close — without shelling out. The termfleet-mcp bin is a stdio MCP server and a thin client of the same provider resolution the CLI uses (TERMFLEET_PROVIDER_URLtermfleet use context → live local discovery), so it adds no new behavior, just a second front door.

Tools: list_sessions, get_session (supports since for catch-up and cwd to resolve some transcripts), send_to_session, new_session (accepts cwd / name / prompt), continue_session, close_session. Sessions are addressed by their prefixed agentSessionId (e.g. claude:<uuid>); window/terminal resolution happens internally.

Register it with an MCP host, e.g. in Claude Code's config:

{
  "mcpServers": {
    "termfleet": { "command": "termfleet-mcp" }
  }
}

termfleet-mcp is on PATH after a global install (npm install -g termfleet); otherwise use { "command": "npx", "args": ["-y", "termfleet-mcp"] }, or from a clone { "command": "npx", "args": ["tsx", "<repo>/src/mcp/server.ts"] }.

Point it at a specific provider by setting TERMFLEET_PROVIDER_URL in the server's env; otherwise it auto-discovers a local provider on first use.

Library: reading agent transcripts (@termfleet/core)

The transcript layer is usable on its own, beyond the console. Install it standalone — a small Node-only library that reads and normalizes Claude Code / Codex / Gemini sessions off disk into one model:

npm install @termfleet/core
import { readLocalAgentSession } from "@termfleet/core/agent-session.js";
import { parseAgentSessionId } from "@termfleet/core/agent-session-id.js"; // claude:/codex:/gemini: grammar

const session = readLocalAgentSession({ cwd: process.cwd(), sessionId: "claude:<uuid>" });
console.log(session.messages.length, session.lastAssistantText);

See packages/core/README.md for the full surface — per-agent readers, the id grammar, the streaming tail, and the provider SDK (ProviderClient, for driving a provider). If you already depend on termfleet, the same readers are re-exported from termfleet/sessions as a convenience.

CLI Commands

Sessions (the primary surface — tier 1)

Work by agentSessionId; termfleet resolves window→terminal internally. This is what an orchestrator should use — windows and terminals (below) are plumbing.

  • sessions recent [--live] [--sort recent|attention] [--fleet] [--limit N] [--cursor C] [--console-url URL]: list sessions across providers. --sort attention is the triage view; --live streams updates.
  • sessions search --query TEXT [--limit N] [--console-url URL]: find sessions by content.
  • sessions watch [--interval-ms N] [--console-url URL]: poll the session list on an interval.
  • <claude|codex|gemini> get --agent-session-id ID [--cwd PATH] [--since N]: read a session's normalized transcript; --since N returns only items after #N.
  • <claude|codex|gemini> wait --agent-session-id ID [--after-count N] [--timeout-ms N] [--poll-ms N]: block until the session's current turn ends. --after-count N waits for a turn after the Nth message (use it right after send).
  • claude continue --agent-session-id ID [--cwd PATH] [--prompt TEXT]: resume an ended Claude session (claude --resume). Claude only.
  • <claude|codex> list: list that agent's sessions on the provider.
  • <claude|codex> kill (--id ID | --name NAME): stop a running agent session.
  • send --agent-session-id ID --text TEXT [--enter]: reply to a session.
  • close --agent-session-id ID: close a session.

send / close also accept --terminal-id / --id as break-glass — see the window/terminal plumbing below.

Provider resolution

Every command that talks to a provider accepts --url URL or --provider TARGET, but both are optional. When neither is given, termfleet resolves a provider in this order (the same model Docker/kubectl/tmux use for local defaults):

  1. --url / --provider — explicit, wins over everything.
  2. $TERMFLEET_PROVIDER_URL — the env var agents and CI use to inject a target.
  3. Current context — a default you pin with termfleet use <url|name>.
  4. Live local auto-discoveryprovider serve self-advertises to ~/.termfleet/providers/, and the CLI picks the single one that is actually serving (verified via /healthz, so a stale record never wins).
  5. Otherwise a structured error (no_provider or ambiguous_provider) with a remedy field telling you how to start or pick one.

This means termfleet claude list, termfleet claude new …, etc. just work when one provider is running locally — no URL required. Helpers:

  • termfleet resolve [--url URL | --provider TARGET] — print which provider would be chosen and why (the source field), read-only. Use this instead of guessing when a command can't find a provider.
  • termfleet use <url|name> — pin a default provider; use --show / use --clear to inspect or unset it. Agents should prefer $TERMFLEET_PROVIDER_URL (which outranks the pinned default) so they never depend on shared mutable state.
  • termfleet doctor — now also reports live local providers and what the default would resolve to.

Resolution failures are emitted as JSON on stderr with a stable error code and a remedy, and always exit nonzero — so an agent can branch on the cause rather than parse prose.

  • SDK: import ProviderClient and providerRefFromUrl from termfleet. Console setup methods include signInToRegistry, registrySession, switchRegistryOrganization, registryProviders, startDockerWorker, startLocalProvider, registerLocalProvider, unregisterLocalProvider, registerSharedProvider, unregisterSharedProvider, and signOutOfRegistry.
  • providers health --url URL: prints provider health.
  • providers snapshot --url URL: prints display bounds and terminal windows.
  • providers lifecycle --url URL: prints observed process/session lifecycle.
  • Provider targets passed with --provider can be a saved alias, provider id, exact label, or URL. Save aliases during registration with --alias or --aliases, or update an existing provider with providers alias set.
  • registry login [--console-url URL] --identifier EMAIL (--password PASSWORD | --password-stdin): signs in to the shared registry through the console SDK.
  • registry logout [--console-url URL]: clears the saved CLI registry session.
  • registry session [--console-url URL]: prints the current registry account and active organization.
  • registry list [--console-url URL]: lists providers visible to the local console, including shared providers when signed in.
  • registry start-docker-worker [--console-url URL] [--url URL] [--label LABEL] [--alias ALIAS] [--aliases A,B] [--terminal-theme-profile PROFILE]: asks the console to start the default Docker worker provider.
  • registry register-local [--console-url URL] --url URL [--label LABEL] [--alias ALIAS] [--aliases A,B] [--requires-auth]: stores a local/loopback provider in the console-local registry.
  • registry register-shared [--console-url URL] --url URL [--label LABEL] [--alias ALIAS] [--aliases A,B] [--requires-auth]: registers a reachable provider URL in the active organization. Use --requires-auth for public or tunneled providers.
  • registry unregister-shared [--console-url URL] (--provider TARGET | --url URL): unregisters a shared provider URL from the active registry organization.
  • providers alias list: lists saved provider aliases.
  • providers alias set (--provider TARGET | --url URL) --alias ALIAS: saves an alias for an existing provider.
  • providers alias unset (--provider TARGET | --url URL) --alias ALIAS: removes an alias from an existing provider.
  • list --url URL: lists provider-owned terminals.
  • capture --url URL --terminal-id TERMINAL_ID [--lines N]: captures terminal output.
  • send --url URL --terminal-id TERMINAL_ID --text TEXT [--enter]: sends input to a provider-owned agent terminal.
  • close --url URL --id ID: closes a provider-owned terminal window.
  • claude new --url URL [--name NAME] [--cwd PATH] [--prompt TEXT]: creates an agent terminal and starts claude --dangerously-skip-permissions.
  • codex new --url URL [--name NAME] [--cwd PATH] [--prompt TEXT]: creates an agent terminal and starts codex --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust.
  • terminals move --url URL --id ID --left N --top N --width N --height N: moves a provider-owned terminal window.
  • terminals resize-display --url URL --width N --height N: resizes a virtual provider display.
  • new --repair --url URL --break-glass [--name NAME] [--cwd PATH] [--command COMMAND]: creates a raw provider-owned shell for repair use only. Prefer claude or codex for normal work.
  • console serve --name NAME [--port PORT] [--host HOST] [--local-adapters iterm[:PORT],wezterm[:PORT]|none] [--no-auto-local-adapters] [--tunnel] [--tunnel-id ID]: starts the browser console server. --tunnel publishes it at a stable public URL (see "Small Public Deployment" and SECURITY.md); --local-adapters picks the always-on iTerm/WezTerm mirrors.
  • console rotate-passcode [--name NAME]: rotates the 6-digit tunnel access passcode (new code, same signing secret — existing sessions survive); takes effect on the next console start.
  • provider serve --kind iterm --name NAME [--port PORT] [--host HOST]: starts an iTerm-backed provider server.
  • provider serve --kind virtual-tmux [--prefix PREFIX] [--count N] [--port PORT] [--host HOST]: starts a virtual tmux provider server. --count defaults to 0.
  • provider serve --kind wezterm [--prefix PREFIX] [--count N] [--port PORT] [--host HOST]: starts an experimental WezTerm provider server. --count defaults to 0.
  • registry start-docker-worker [--console-url URL]: asks the console to start the Docker worker and register its provider URL.

Driving a session from your terminal

The console mirrors every session in the browser, but you don't have to leave the terminal to type into one. Two ideas, kept lexically separate so they never blur into each other — create (boot a session) vs connect (attach to a live one):

  • attach [SESSION | --agent-session-id ID]: connect the calling terminal directly to a live session — the raw PTY, the same stream the browser shows. With no argument it attaches to the single live session; otherwise pass an agentSessionId (or a prefix / window name). Detach with Ctrl-b d — the session keeps running (it's durable on disk and reaped independently); only an explicit exit ends it. attach never boots anything.
  • chat --attach: the normal chat bring-up (console + fleet + a launched agent), but instead of printing JSON it drops you straight into the new session. Bare chat still reports JSON and exits, so scripts are unaffected.
  • control (alias ctl): a shortcut — a fresh orchestrator chat in your fleet-control folder (~/fleet-control by default; override with --cwd, persist with --save) using the default agent, and because you ran it from a terminal it attaches by default. --no-attach for a scriptable boot. This is the one-word front door to driving the fleet from your own shell.
termfleet control            # boot + attach the orchestrator, here, now
termfleet ctl --cwd ~/fleet-control --save   # remember the folder for next time
termfleet chat --attach --cwd ~/myproject    # boot a chat here and stay attached
termfleet attach             # reconnect to the one live session
termfleet attach claude      # reconnect to the most-recent claude session

Design

tmux owns terminal sessions because it has stable IDs, reliable input/output commands, capture APIs, and session persistence.

Provider instances own terminal/window/display state. The console owns board layout. Local and remote registries provide discovery and remote reachability without becoming the authority for terminal state.

Transport split:

  • HTTP: health, bootstrap, debug, terminal capture.
  • Socket.IO: provider commands, acknowledgements, snapshots.
  • Raw WebSocket: live terminal PTY streams for the browser UI.

License

Apache-2.0. The standalone library is published as @termfleet/core; the CLI + console as termfleet.