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

@sailorbridge/client

v0.2.41

Published

SailorBridge client CLI: relay access, enrollment descriptors, worker host runtime, and a tmux supervisor for Claude/Codex agents.

Readme

SailorBridge Client

Standalone Node/TypeScript client for SailorBridge: a relay HTTP client, a debug CLI, a worker host runtime, and a tmux supervisor that owns one local Claude or Codex agent process and reports monitor state to the relay. Host pairing login stores a host-scoped relay key for subsequent local host commands.

Install

One channel, one line:

curl -fsSL <origin>/api/client-binary/install.sh | sh
sail --version

Use the exact <origin> shown by the SailorBridge console's copyable install command; it reflects the deployment this host belongs to.

It installs a self-contained binary into /usr/local/bin under both names (sail and sailorbridge), after verifying the download against the published sha256. Both commands work in that same shell; there is no PATH to edit. /usr/local/bin belongs to root, so re-run the line with sudo sh in place of sh if the installer says it cannot write there. Linux and macOS are the supported targets; Windows packaging support is deferred.

The npm package (npm i -g @sailorbridge/client) is a legacy compatibility rail for machines that already installed that way. It is not offered anywhere in the product, and new installs must not use it: npm's global prefix is routinely absent from PATH, a postinstall script cannot change its parent shell's PATH, and editing somebody's shell profile is not the installer's to do.

Building the client from source requires Perl with Fcntl/flock support, in addition to Node and Bun; the build lock uses it to serialize atomic releases.

The host service records the PATH of the shell that installed it, because no systemd user unit or launchd agent inherits one. After changing where your tools live — an nvm/asdf version switch, a new Homebrew prefix — re-run sailorbridge host service install so the service picks up the new PATH.

Upgrade a client and reload a running managed host daemon in one step — the same installer line, which replaces the binary the host service runs and restarts that service:

curl -fsSL <origin>/api/client-binary/install.sh | sh

On a machine still on the legacy npm rail, sailorbridge upgrade remains the in-process equivalent (compatibility only, never guidance):

sailorbridge upgrade

The command installs @sailorbridge/client@latest, compares the running and installed versions, smoke-checks the new CLI with --version, and restarts the product-managed user systemd unit or LaunchAgent when it is running. The npm install is killed after 120 seconds; its bounded stdout/stderr is returned in the error instead of inheriting the host log stream. A failed version read or smoke check reinstalls and verifies the pre-upgrade global package before returning the failure. If the host runs in the foreground or under another supervisor, the command prints the exact manual restart action.

If npm reports an ENOTDIR rename for the global @sailorbridge/client entry, upgrade checks the path against npm root -g, removes the client entry only when lstat proves it is not a directory, clears only sibling .client-* npm residue, and retries once. A second failure keeps npm's bounded stderr and prints exact, shell-quoted cleanup and retry commands.

The host daemon also keeps itself current: the relay broadcasts the latest released client version on every register/heartbeat, and host run upgrades through the binary rail — or, on a legacy npm install, that npm channel — as soon as it hears about a target. The restart that loads it waits out one random per-host delay and nothing else: the window it is drawn from is the relay's restart_jitter_max_seconds (ten minutes if the relay names none), and it exists only to keep a whole fleet from restarting on the same broadcast. A busy host is not a reason to wait — a daemon restart leaves every member pane running — and no member is asked for a checkpoint first. A manual sailorbridge upgrade and a 426 handshake rejection skip the delay entirely. If three service restarts fail to advance the running version, the host persists a fuse and reports its self-service recovery through update_error. Other failed attempts are logged and reported to the relay as update_error, then retried on a cooldown. Opt out with sailorbridge host run --disable-auto-update or "auto_update": false in ~/.sailorbridge/host-config.json — but hosts below the relay's minimum supported version are refused at the handshake (HTTP 426) until upgraded, so a disabled host must be kept current manually.

The daemon also probes its own launch artifact on disk. If an external installer has placed a newer release (or a different release build at the same version) beside the still-loaded process, it does not reinstall or republish the old bytes. A managed daemon enters the same idle/24-hour/checkpoint/restart-fuse state machine and restarts only the exact service definition whose credential fingerprint matches this host. A standalone daemon is never killed by PID; its host row carries a copyable, topology-only restart command until the new process starts and clears the observation.

For a managed host daemon, use the compiled single-binary path instead of a Node-based global install. Build or download the native sailorbridge binary, then log in once — login installs and starts the user service itself:

bun run build:binary
./bin/sailorbridge login
./bin/sailorbridge host service status

The service runs sailorbridge host run from the binary and reads the host-scoped credential file. The unit/plist never embeds bearer tokens. After the binary is built or installed, the managed host runtime does not require Node, Bun, or Python on PATH.

Quickstart

Configure relay credentials:

export SAILORBRIDGE_RELAY_URL=http://127.0.0.1:8765
export SAILORBRIDGE_TOKEN=...
export SAILORBRIDGE_AGENT_NAME=session_agent
export SAILORBRIDGE_CAPTAIN_ID=100
export SAILORBRIDGE_SESSION=session
export SAILORBRIDGE_HOST_ID="$(hostname)"
export SAILORBRIDGE_HOST_DISPLAY_NAME="$(hostname)"

Or pair this machine through the product API:

sailorbridge login

login connects to https://www.sailorbridge.com. The command prints a short host pairing code, waits for console/mobile approval, writes a 0600 credentials file at ~/.sailorbridge/credentials.json, and then installs and starts the host service so the console can start workers and agents on this machine without a second command. A pending pairing is also stored with mode 0600: restarting login within the 10-minute code lifetime resumes the same code. Bare 401/403/404, 429/5xx, and network failures use capped exponential backoff until that deadline while preserving the same code. Only a successful poll that reports an expired/rejected/consumed terminal state fails immediately and tells you to run sailorbridge login for a new code.

Before pairing, login asks what the host is for and prints one preflight checklist for that answer, so a missing prerequisite surfaces while the user is still at the keyboard instead of when their first member fails to start:

| Requirement | Member host | Worker-only host | | --- | --- | --- | | Node.js >= 20 | required | required | | sailorbridge and sail on PATH | required | not used | | git executable | required | not used | | tmux | required | not used | | claude or codex CLI, logged in | required (either one) | not used |

Pass --worker-only (or SAILORBRIDGE_WORKER_ONLY=1) to skip the member-host items; an interactive login asks once, and a non-interactive login checks the full member-host list. The CLI row is checked twice — once against your shell's PATH and once against the PATH the installed service will run with — because members drive their own pane with sail: a service that cannot resolve it leaves every member's control command failing long after login reported success. The login preflight checks that git is installed but does not inspect the current directory; the repository is named when a team is created. Workers advertise podman, git, kubectl, perf and bpftrace as capabilities when present; none are required.

Installed engines receive a bounded real prompt. The final stdout answer is preferred, with stderr accepted only when stdout is empty; a fully empty successful response is retried once and then reported as a transient empty response. Authentication failures point to that engine's login command, and timeouts report the elapsed limit.

A failing item never blocks pairing — the report prints the exact command that fixes it. tmux can be installed with consent (the exact command is shown first); engine CLIs and their logins are always left to the user, since the credential is theirs.

Common commands:

sailorbridge --help
sailorbridge login
sailorbridge upgrade
sailorbridge workflow
sailorbridge run --fold bun test
sailorbridge mailbox peek
sailorbridge mailbox claim
sailorbridge reply --in-reply-to MESSAGE_ID REPLY --body-file ./reply.txt
sailorbridge mailbox ack MESSAGE_ID --lease-token-file ./lease-token
printf '%s\n' 'hello $USER `date` $(pwd)' | sailorbridge mailbox send --to-agents ab_hand --tag BTW
sailorbridge mailbox send --to-roles reviewer --tag PLAN-REVIEW --body-file ./plan.txt
sailorbridge mailbox send --to-agents captain --tag REPORT --body-file ./report.txt
sailorbridge create team --name "My team" --template classic3 --repo-path "$PWD" --host my-host
sailorbridge host register
sailorbridge host consume --once --dry-run-start
sailorbridge host service install --binary /opt/sailorbridge/bin/sailorbridge
sailorbridge host service status
sailorbridge worker list
sailorbridge worker run --worker dev-1 -- uname -s
sailorbridge worker job JOB_ID
sailorbridge worker serve
sailorbridge supervise --cli codex --cwd "$PWD" --i-own-this-agent

sail run --fold <cmd...> is for finite, non-interactive test/build commands whose full output would waste the member's context. It writes complete stdout and stderr under ~/.sailorbridge/logs/, then prints only a bounded verdict, failure block and tail with the command's exit code, duration and log path. It runs argv directly rather than through a shell; name sh -c yourself when shell syntax is intentional. Interactive programs must be run normally without --fold.

worker list, worker run, and worker job are member-runtime commands; worker serve instead uses a worker enrollment credential. The paired host credential created by login authorizes neither surface.

The package ships only the built CLI/runtime files under dist/src/ plus this README. It deliberately does not publish src/, tests/, or dist/tests/. npm run pack:staged performs an ephemeral packaging check without mutating the live dist/ symlink. It runs only from a clean checkout whose HEAD exactly matches the freshly fetched origin/main, writes the tarball under a private temporary directory, and deletes it before returning; its JSON filename is informational, not a reusable artifact. Use npm run pack:verify for package-content verification on a feature branch. Releases must use:

npm run publish:release

For an operational reinstall from the current checkout, build first and invoke the same packaging helper used by the pristine-install test. This path is for a local install only; it does not apply the release gate and must never feed npm publish. The temporary directory is the recovery boundary: rerun after an interruption, and the trap removes the local artifact.

package_dir="$(mktemp -d)"
trap 'rm -rf -- "$package_dir"' EXIT INT TERM
bun run build
node --input-type=module - "$package_dir" <<'NODE'
import { createPackage } from "./scripts/create-package.mjs";
process.stdout.write(await createPackage(process.argv[2]));
NODE
npm install -g "$package_dir"/sailorbridge-client-*.tgz

That entrypoint fetches origin/main before any check or packaging work. It refuses a dirty worktree, a HEAD different from origin/main, or a history that does not contain the fetched main tip; non-main failures print both SHAs and the ahead/behind counts. After checks it explicitly rebuilds dist, then reruns the Git gate immediately before packaging so source changes during the long check cannot pair with stale output. It removes reusable sailorbridge-client-*.tgz files from the package root, packs into a randomly named one-shot tarball in an isolated temporary directory, publishes it, and removes it after success, failure, SIGINT, or SIGTERM. The package-root cleanup intentionally runs before fetch/provenance checks, so starting the wrapper removes those known reusable artifacts even when the release attempt is subsequently rejected.

Only npm run publish:release is supported. npm lifecycle hooks are not a security boundary: npm publish <tarball> does not run the source package's prepublishOnly, and npm publish --ignore-scripts disables it explicitly. Those direct npm forms can therefore bypass repository checks and must not be used; the wrapper cannot stop a user who deliberately invokes npm outside it.

An emergency hotfix branch requires a Hotfix-Reason: line in the HEAD commit message plus the exact same CLI reason. This is self-recorded intent, not independent approval: the wrapper prints it as a warning, but terminal stderr is transient and an unpushed commit can still be rewritten. Non-main releases are forced to the npm next dist-tag so the override cannot replace latest.

git commit -m "fix(client): emergency release" \
  -m "Hotfix-Reason: reason for the emergency hotfix"
npm run publish:release -- --allow-non-main "reason for the emergency hotfix"

package.json intentionally remains at 0.2.12 while this release-safety change is not itself published. npm versions are immutable, so the next owner-approved release must bump the version in its own release commit before running the wrapper.

Development

bun install
bun run check

Builds retain immutable releases for at least one hour so in-flight readers remain loadable; older non-current releases are reclaimed.

The package installs a sailorbridge bin after build:

bun run build
node dist/src/cli.js workflow
node dist/src/cli.js mailbox peek

Build a native standalone binary for the current host:

bun run build:binary
./bin/sailorbridge --help

build:binary defaults to the native Bun compile target. Release automation can pass --target through scripts/build-binary.mjs for the supported matrix:

linux-x64   -> bun-linux-x64
linux-arm64 -> bun-linux-arm64
darwin-x64  -> bun-darwin-x64
darwin-arm64 -> bun-darwin-arm64

Configuration

Preferred environment:

SAILORBRIDGE_RELAY_URL=http://127.0.0.1:8765
SAILORBRIDGE_TOKEN=...
SAILORBRIDGE_AGENT_NAME=session_agent
SAILORBRIDGE_CAPTAIN_ID=100
SAILORBRIDGE_SESSION=session
SAILORBRIDGE_HOST_ID=mac
SAILORBRIDGE_HOST_DISPLAY_NAME="Mac"
SAILORBRIDGE_HOST_CONFIG_FILE=~/.sailorbridge/host-config.json
SAILORBRIDGE_API_URL=https://www.sailorbridge.com
SAILORBRIDGE_CREDENTIALS_FILE=~/.sailorbridge/credentials.json
SAILORBRIDGE_WORKER_NAME=dev-1
SAILORBRIDGE_WORKDIR=~/sailorbridge-workdir
SAILORBRIDGE_MAX_CONCURRENCY=1

SAILORBRIDGE_CAPTAIN_ID is required by worker list, worker run, and worker job; it must be a numeric string. worker list and worker run also require SAILORBRIDGE_SESSION so an unbound admin credential cannot accidentally discover workers across teams. These commands require a member-runtime credential; a paired host credential is not a member credential and is rejected before any relay request. Use the console Workers page to list workers, or run the command inside a member workspace where the product provides the token, captain id, and session. Agent routing uses the separate SAILORBRIDGE_AGENT_NAME.

SAILORBRIDGE_HOST_ID and SAILORBRIDGE_HOST_DISPLAY_NAME identify the captain-scoped local host for Tier 6 console start-request polling. When omitted, host commands default both values from the OS hostname. A host keeps no list of "its" repositories: a repository belongs to a team, and its absolute path on the host is named per team (create ... --repo-path, or the path field in the console's team wizard). create team --repo-path verifies the clone through that host and registers it as the team's task repository in the same team-creation transaction. The host resolves origin/HEAD, verifies that branch has at least one commit, and registers it as the immutable default branch for the team pipeline. Task-team member start requests do not carry a workspace choice: the relay derives and stores a worktree spec from this verified (team, host) mapping, ignoring any supplied override. Chat-team start requests keep their explicit fresh/worktree/git modes. The daemon never scans or reports the directories on a host. Each create subcommand rejects flags outside its documented command-specific surface before reading credentials or contacting the network.

SAILORBRIDGE_HOST_CONFIG_FILE redirects the local host config (~/.sailorbridge/host-config.json by default) that host account writes and the daemon reads.

SAILORBRIDGE_DOORBELL_NO_PROGRESS_MIN (default 10) is how long a member may show no progress before its unclaimed doorbell is escalated to the moderators as DOORBELL-TIMEOUT. Members claim on a turn boundary, so a queued doorbell is never by itself a delivery failure — the supervisor renews it while the pane keeps painting, and reports once nothing has moved for the whole window (a pane nobody can read counts as no progress) or once the renewal itself has run for 6× the window, whichever comes first. That ceiling exists because a pane in an active turn repaints on the spinner's own clock, which would otherwise renew the escalation for as long as the member keeps working. Raise the variable for fleets whose members run longer silent stretches; it scales the ceiling by the same factor, and delays the escalation by the same amount when a member really does disappear.

Running members on more than one engine account

By default every member on a host uses that host's own engine directory (CLAUDE_CONFIG_DIR/CODEX_HOME, else ~/.claude/~/.codex) — one account, zero configuration. To run members on several logins, register each login directory under a name of your choosing and point members at it:

sailorbridge host account add C1 ~/.claude-accounts/C1
sailorbridge host account add C2 ~/.claude-accounts/C2
sailorbridge host account set alice C1
sailorbridge host account set bob C2
sailorbridge host account list

add refuses a directory that holds no engine credential, so an account nothing could start on is rejected while you are still at the keyboard. A member with no pointer keeps using the host default directory. Moving a member is host account set MEMBER ACCOUNT (or default to clear it); it takes effect on that member's next start or relaunch, because the engine reads its credential at boot. Members sharing an account share one physical credential file, so a token refresh in any of their sessions is visible to all of them. SailorBridge stores only the pointer for engine-owned credentials — those credentials never enter product storage, logs, or the relay, and the console shows the account NAME read-only. A named custom Codex provider can instead keep its API key in the account directory's private SailorBridge-owned record:

# New provider-only account: key comes from config.toml's detected env_key.
sailorbridge host account add D1 ~/.codex-accounts/D1 --engine codex --provider-key-from-env

# Rotate from a pipe, inspect without showing the value, or remove it.
printf %s "$DEEPSEEK_API_KEY" | sailorbridge host account provider-key set D1 --stdin
sailorbridge host account provider-key status D1
sailorbridge host account provider-key remove D1

The key is never accepted in argv or printed. env_key is detected from the selected config.toml provider, the record is strict JSON with mode 0600, and each start/relaunch (including after a daemon restart) reloads it from disk.

Every 6 hours the host daemon also runs a scratch sweep over each member worktree it owns, reaping <root>/.agent/tmp/ (a git-ignored scratch directory): it deletes git-untracked files older than 48 hours, then evicts oldest-first until the directory total is under 512 MiB. It only ever touches untracked files inside that directory, never follows symlinks, and skips (with a log) any workspace whose .agent/tmp/ resolves out of bounds or is not a git repository. Set SAILORBRIDGE_SCRATCH_SWEEP_ENABLED=0 (or false/no/off) to disable it; it is on by default. A normal member retirement clears that member's eligible .agent/tmp/ immediately before archiving its workspace.

The same pass reaps Claude/Codex session transcripts older than 48 hours from configured engine-account directories, but only when transcript metadata names the exact workspace whose ownership ledger belongs to this host. Claude sidecar tool results derived from that verified session id are removed with it. A member retirement reaps all such verified sessions immediately. Unknown metadata, unowned workspaces, other projects in a shared account, credentials, and account configuration are never deletion candidates.

The same daemon runs Podman GC every 5 minutes. It removes stopped task/test containers in the sbtc- namespace (including their anonymous volumes) and volumes/images that Podman itself reports as dangling after 48 hours. It never uses --force, removes a running container, removes an attached or named shared volume, or removes a tagged image.

Rootless Docker is a separate, conservative surface. Every five minutes the daemon probes the active Docker context and its actual DockerRootDir; if Docker is absent or unreachable, the probe is a silent no-op. The heartbeat reports aggregate/reclaimable bytes and the five largest images with age and container-reference facts. Images older than 48 hours with no container reference are offered in Console → Hosts. SailorBridge never deletes one automatically: the captain must select it and confirm, and the host rechecks container references plus the built-in agent-bridge relay/worker/captain keep-list immediately before docker image rm (without --force).

The same five-minute maintenance cycle bounds every registered Codex account's logs_2.sqlite: seven-day retention, at most 250,000 rows and 256 MiB of estimated retained log payload, deleted in 5,000-row transactions with a 100,000-row per-cycle work bound. It uses a short SQLite busy_timeout and a PASSIVE WAL checkpoint; it never renames, replaces, or unlinks a live database. A persisted 1 GiB max_page_count bounds a new database; an already larger database is capped at its current high-water mark and reuses pages freed by deletion. Account bytes, reusable pages and incomplete/locked maintenance are reported on the Hosts page and retry automatically.

Each host heartbeat reports filesystem usage plus the five largest owned member scratch envelopes, combining workspace .agent/, product engine sandboxes, and positively attributed engine-account sessions. At 90% the Hosts page and every affected team's moderator receive a warning. At 95% the relay leaves new task cards queued and the host rejects new member starts; both resume automatically after usage drops below 95%. Open Console → Hosts → the affected host to see the current percentage and top scratch consumers. No periodic manual cleanup is part of the recovery path.

The register/heartbeat report also carries Linux host capacity from /proc: MemAvailable, load1, and each managed tmux member's process-tree RSS. Each probe fails soft and independently — an unreadable value is omitted, never reported as zero. If the whole observation is unavailable the daemon omits the top-level report, so the relay preserves the last known snapshot rather than turning one failed cycle into invented capacity.

After its first register/drain and every 6 hours, the daemon also reaps OS temporary entries older than 48 hours when their top-level names match one of seven hard-coded prefixes with production producers. Each prefix also declares the KIND its producer makes, and an entry whose kind does not match is left alone — a directory wearing a file producer's prefix is somebody else's object, not residue. Six are directories, removed with their contents: sailorbridge-env-, sailorbridge-tmux-, sailorbridge-patch-, sailorbridge-console-retention-, sailorbridge-api-pg-smoke-, and sb-usage-rescan-. One is a file: sail-listener. (the listener's outgoing message files), unlinked singly and never removed recursively. It checks the daemon uid where the platform exposes one; Windows uses the daemon's per-user temporary root as that boundary. Each loop has match, removal, and elapsed-time budgets and continues unfinished work on the next loop. It never matches generic tmp.* or test-fixture prefixes, follows symlinks, blocks host registration, or lets one unreadable entry stop the daemon. Normal command paths still remove their temporary entry in finally — and create it inside the try, with nothing that can throw between the two — so this sweep only recovers production residue left by SIGKILL or power loss.

sailorbridge worker serve runs the TypeScript host worker against the relay without importing the Python worker package. It registers the configured worker name, long-polls /pull, executes jobs inside SAILORBRIDGE_WORKDIR, posts results with the relay lease token, and streams stdout/stderr for stream jobs. Use --worker, --workdir, and --max-concurrency or the matching SAILORBRIDGE_WORKER_NAME, SAILORBRIDGE_WORKDIR, and SAILORBRIDGE_MAX_CONCURRENCY environment variables. SAILORBRIDGE_MODE=readonly blocks mutating job kinds locally.

sailorbridge worker list [--format plain|json] discovers registered workers in the current team. Plain output shows at most 20 and points to JSON for the complete response. An empty session points to sailorbridge worker ensure [--host H], which reuses the verified repo host, quota gate, worker-start request, and host daemon, then waits for online registration; it never creates from the read-only list command. A registered but offline worker is automatically relaunched by the host daemon's durable-runtime reconcile; ensure validates its online verified host and waits without minting a second seat. sailorbridge worker run [--detach] --worker NAME [--timeout SEC] [--format plain|json] -- ARGV... submits one bash job and preserves ARGV as an array end to end; --format after -- belongs to the remote argv. The timeout is the remote execution budget; synchronous mode waits that budget plus a 30-second queue/result grace for the persisted terminal result. Plain output writes remote stdout/stderr to the matching local streams. A remote negative signal code remains exact in JSON; plain output states that code and uses the conventional local status 128 + signal. Synchronous mode then confirms inline delivery so the relay closes the duplicate JOB-DONE mailbox obligation; timeout, interruption, or confirmation failure leaves it open. Detached runs and worker job polling never close it. For long work, --detach returns the submit response immediately; sailorbridge worker job ID [--format plain|json] reports queued/running or the terminal result. Worker jobs are infrastructure execution, not product tasks; their status never completes a SailorBridge task lifecycle.

sailorbridge login creates a host pairing against https://www.sailorbridge.com. The command displays the one-time code and polls until approval. Deploy-time 429/5xx/network interruptions do not replace an unexpired code, and restarting the command resumes its pending pairing. The stored credential contains a host-scoped relay key (key_scope: "host" plus host_id) and is used as a fallback when SAILORBRIDGE_RELAY_URL or SAILORBRIDGE_TOKEN are not set. Worker data commands reject this host-scoped fallback before any relay request. Raw host keys are written to the credentials file only. Data commands default to --format plain; login's plain success output prints the host id, credential path, and service status. Explicit --format json prints key id and host metadata, not the token.

sailorbridge login installs this service automatically; it exits 3 when pairing succeeded but the install did not, and states that the host remains offline and cannot receive console tasks before naming the command to run by hand. Pass --no-service (or set SAILORBRIDGE_NO_SERVICE=1) to pair only — the opt-out for containers, WSL, and CI images without a service manager. Re-running login refreshes the definition and restarts the daemon so it always runs with the arguments on disk. A markerless legacy definition is taken over only when its parsed executable and arguments identify sailorbridge host run: the old daemon is stopped/unloaded and the definition is renamed to a .sb-legacy-<UTC timestamp> archive before reinstall. Foreign definitions, symlinks, and definitions serving another host remain rejected; move a foreign file aside or pass --service-name to install a scoped sailorbridge-host-<suffix> unit. sailorbridge host service install|uninstall|status manages the long-running host daemon as a per-user service afterwards. Linux uses user systemd with Restart=on-failure; macOS uses a user LaunchAgent with KeepAlive. The service definition points at a credentials file and a binary path; it does not inline tokens. Linux status detects whether linger is enabled. If boot persistence is not active, install and status print the exact sudo loginctl enable-linger <user> command for the user to run, but never run sudo automatically. macOS LaunchAgents have no linger equivalent. Managed and foreground daemons both write the structured, two-generation rotating event log reported as log_path by host service install|status; Linux also captures service stdout/stderr there, so reading host events does not depend on persistent journald configuration. Successful member relaunch/rebuild events also enter the ordinary host report as a bounded retry batch. The relay binds them to this host and exposes the durable cause, exit code, and signal in the captain's member detail; the local log remains the evidence source before a report succeeds.

Ordinary uninstall retains local credentials and state. Use sailorbridge host service uninstall --purge for an explicitly confirmed cleanup of SailorBridge credentials, config, sandboxes, auth state, and logs; non-interactive callers must add --yes. Purge still preserves repositories, member worktrees, worker workdirs, external engine-account directories, and the user's global Claude/Codex configuration.

Relay Contract

This client reimplements the relay HTTP contract in TypeScript. It does not import the Python SailorBridge engine, SailorBridge Site, or SailorAgent code. Key creation uses the observed relay contract roles: string[]; the legacy singular role field is deprecated and intentionally not sent.

Client host commands implement the P5a relay contract from engine/PROTOCOL.md: registering or heartbeating a host, listing captain-scoped hosts, and claiming a single agent_start_request. Host-paired credentials are limited to their bound host id and cannot list sibling hosts. In P5a, sailorbridge host consume requires --once --dry-run-start; it validates and transitions a claimed request but does not launch an engine process. Retirement directives are printed with their exact type and target, left pending without execution or ACK, and point to sail host run for real handling. A stop_worker preview whose relay capabilities are not yet available says that the real daemon will keep it pending until those capabilities appear. sailorbridge host run is the real long-running host daemon: it claims console start requests, redeems an agent credential, prepares the relay-derived workspace, and starts the supervised agent. On daemon restart it asks the product API for every still-active member owned by the host, rotates the agent credential, and rebuilds the exact relay-reported tmux target before it resumes polling new start requests. Workers started by the same daemon are restored from the same durable ledger. Console Relaunch also queues a new host start, so it does not depend on the old supervisor still polling. Host-started Claude and Codex agents get terse-output defaults and Claude output hygiene hooks to reduce routine token spend without lowering reasoning effort or verification requirements. Before launching an agent, the host must already be logged in to that engine: Codex uses $CODEX_HOME/auth.json when set, otherwise ~/.codex/auth.json; Claude uses $CLAUDE_CONFIG_DIR/.credentials.json when set, otherwise ~/.claude/.credentials.json. The per-agent sandbox links to that single host auth file; missing engine auth fails fast instead of starting a CLI that will prompt for credentials. The host daemon also reads the default Claude subscription OAuth credential once per day to list provider model metadata. It sends one GET /v1/models request with a 10-second timeout and no retry, then reports model ids only to the relay. The OAuth token stays in the provider request header and never enters relay payloads, logs, errors, or argv. API-key-only and Codex-only hosts skip this probe; Codex has no machine-readable subscription catalog source.

Worker commands implement both sides of the relay worker contract from engine/PROTOCOL.md: register, heartbeat, pull, stream upload, result posting, session-scoped worker listing, direct bash submission, and terminal result reads. Worker HTTP uses bounded connect/read-idle timeouts for liveness and leaves result uploads without a total timeout cap.

Mailbox claim is a destructive operation on the relay; mailbox peek is the non-destructive command. Add --lease to a claim only to include rows already leased by this client. A supervisor peek leaves workflow-obligation messages queued and claimable after it surfaces the body-free doorbell. The bound agent's first mailbox claim leases them, then the agent completes the required action. Plain claims expose every lease token. HUMAN and MAIL stay leased so reply --in-reply-to can infer the sender and atomically record the reply plus source ACK; untagged peer mail is shown and handled as MAIL. If the exact source cannot be reclaimed, the CLI identifies whether it is missing, settled, addressed to another agent, or a transcript-only row and prints the next action. --no-ack leaves the source leased. Direct workflow requests retain their linked-reply settlement. Task-lane claims with a valid task identity label mailbox ACK as delivery-only and point to the linked durable task transition. An unavailable identity instead prints the ACK-clearable degraded recovery path. ACK lease tokens must stay out of argv: store the surfaced token in a private file and use mailbox ack MESSAGE_ID --lease-token-file PATH, or pipe exactly the token to mailbox ack MESSAGE_ID --lease-token-file -. Empty input, a TTY, duplicate token sources, and piped input unused by a file path are rejected. --lease-token TOKEN is not supported.

Mailbox send is footgun-free by construction: message bodies are read only from piped stdin or --body-file, never from positional argv. Every --body-file and --description-file flag treats - as stdin; task create --description - is the same stdin shorthand, as is positional reply body -. Non-empty piped input that another selected source would discard is rejected instead of silently ignored; commands without a stdin contract reject it before any relay request. mailbox send and reply reject empty, whitespace-only, or BOM-only input before any relay request; there is no implicit empty-message mode. The relay's trusted Console control path separately permits payloadless CLEAR and WORKFLOW-REFRESH envelopes; this public CLI does not expose that exception. This prevents the local shell from expanding $VARS, backticks, or $() before the client starts. A single --to-agents A sends directly to that agent, except reserved captain, which uses the session transcript endpoint. Multiple explicit agents use the session mailbox endpoint. --to-agents is mutually exclusive with --to-roles and --to-host; combine roles and host when filtering registered agents. An interactive TTY with no pipe fails fast instead of waiting for typed input.

Plaintext bearer tokens are never printed by default. Enrollment descriptors contain a plaintext token and are printed only when the caller explicitly runs the enrollment command.

Driver Contract

Phase 1 exposes only the tmux transport for Claude and Codex agents. SDK, headless, and PTY driver code remains in the package for a later release, but the public CLI and host-start surfaces fail fast if callers request those transports or request postponed engines such as opencode, copilot, or a custom CLI id. There is no silent downgrade to tmux or PTY.

The client provides a shared AgentDriver interface. The public Phase 1 path is the tmux backend; all drivers share the same transport-level contract: start, send, on, resume, isAlive, and stop.

The tmux driver creates a dedicated tmux session by default and only destroys sessions it created. Existing-session mode can send or interrupt, but it does not launch a process because environment secrets must not be typed into an interactive pane. Host-started agents use readable session names such as sb-<team>-<role> and append the captain number only when that default already exists on the host.

Tmux launch environment is handed directly to the new pane through a private, one-shot FIFO. The pane imports it immediately before exec; bearer values never enter tmux argv, the tmux environment store, pane input, or a regular credential file.

SDK drivers are optional implementation code and are release-gated from the public Phase 1 CLI. A base install or tmux-only caller does not need to instantiate them. The pinned SDK versions are:

Anthropic's public Claude Code SDK docs still reference @anthropic-ai/claude-code in some places; the implementation is grounded in the @anthropic-ai/[email protected] npm package declarations. Unit tests use injected fake SDK facades to verify our event mapping and option assembly; they do not prove live provider behavior. Real SDK smoke tests exist but are skipped by default. Set SAILORBRIDGE_REAL_SDK_SMOKE=1 to run them.

Productized supervisors default to unattended execution so a BYO agent does not stop on local permission prompts: Claude uses bypassPermissions and Codex uses danger-full-access plus approvalPolicy: "never" / yolo. Explicit --permission-mode, --sandbox-mode, --approval-policy, or --yolo false opt back into a more interactive or constrained launch; the tmux transport translates those flags into first-party CLI argv. An explicit -- COMMAND... is always used verbatim; combining it with safety flags fails fast because those flags would not be applied to the verbatim argv. --yolo true is also mutually exclusive with explicit Codex sandbox or approval-policy constraints.

SDK capabilities, PTY support, opencode/copilot adapters, and custom CLI ids are deferred to the next release. Their implementation files may exist in this package, but Phase 1 help, docs, and validation do not expose them as supported product surface.

Mailbox consumption, CLEAR, and WORKFLOW-REFRESH handling are supervisor responsibilities (see Supervisor Contract). The driver only manages transport lifecycle, liveness inspection, and text delivery.

Supervisor Contract

The TypeScript supervisor owns one local agent process, chooses a transport, leases that agent's mailbox, and reports only real session/profile changes back to the relay.

Ownership is exclusive: do not run this supervisor and the Python SailorBridge listener against the same agent_name. A single agent inbox and runtime control channel must have one owner, otherwise mailbox leases can be stolen. Use sailorbridge supervise only for agents not already driven by another listener/supervisor.

Context auto-checkpoint (tmux transport, claude/codex): the supervisor reads the context percentage from the pane every ~20s and, at or above the agent's configured context_checkpoint_percent (console setting; 0 disables, default 75), posts one self-addressed SAVE-CONTEXT. The mailbox loop then cues the save_ctx skill, waits for SAVE-CONTEXT-DONE, and replays /clear plus the resume_ctx cue. Threshold changes made in the console take effect live through the reload_context_autopilot resolve command. Semantics match the engine listener's autopilot (same thresholds, same armed-below/fire-once water-mark).

On that same ~20s observation cadence, current clients report the percentage actually read from the pane (or the engine's self-describing token source) to the relay's dedicated context-telemetry endpoint. An unavailable reading is reported as null; the conservative +5 fallback bias remains checkpoint-only and is never shown as live usage. This display telemetry does not participate in checkpoint, relaunch, liveness, or scheduling decisions.

Idle pre-save (tmux transport, claude by default): the same loop watches for a member that has gone quiet. Once the pane is visibly idle and the newest real mailbox activity is older than idle_presave_min (console setting; 0 disables, empty takes the engine default — claude 50 minutes, codex off) but still younger than the engine's prompt-cache TTL, the supervisor re-samples pane and activity and then posts one SAVE-CONTEXT carrying source=listener-idle-presave. That checkpoint is ACK-only: the handoff is written, but nothing clears, resumes, or relaunches — a member that merely sat idle keeps its live context. One archive per span of activity, a 30-minute cooldown after each attempt, and every unknown (capture failure, busy pane, missing or future activity timestamps) is a no-op. Tables are pinned against the engine listener by tooling/e2e/idle-presave-parity.test.ts.

Default transport selection:

| CLI | Default transport | Default command | | --- | --- | --- | | claude | tmux | claude | | codex | tmux | codex |

claude/codex default to tmux so the agent runs the real first-party CLI (covered by the captain's Claude/Codex subscription and attachable). When tmux is the resolved transport and no --tmux-session is given, host-started agents use readable names such as sb-<team>-<role>; same-host collisions try the default name, then the captain number, then the agent name, followed by numeric suffixes -1 through -100. If every candidate is occupied, startup fails explicitly. Direct sailorbridge supervise has no team/role context, so it falls back to a readable sb-<agent> name. --tmux-session overrides all derived names.

When tmux is the resolved transport but no tmux binary is found, the client fails fast or asks before installing it:

  • default path (no --transport) → resolves to tmux and fails fast with a platform install hint.
  • explicit --transport tmux on an interactive TTY → prompts: [1] install tmux (macOS brew / Linux apt-get/dnf/pacman, asking for confirmation and showing the exact command — including sudo — before running it), or [2] cancel.
  • explicit --transport tmux without a TTY (e.g. CI) → fails fast with the platform install hint. It never installs silently or downgrades the transport.

--transport tmux launches through the tmux driver, using an explicit --tmux-session when given or otherwise deriving the readable default described above; pass --tmux-session for direct-supervise collision control. It is the attach/mirror option because tmux reports attach: true. Explicit --transport auto is rejected so the public surface stays tmux-only; sdk, headless, and pty fail fast as next-release transports.

Supervisor relaunch decisions are based only on local process liveness: driver.on("exit") or isAlive().processAlive === false. Reply liveness is not reported as relay presence and never triggers relaunch by itself.

CLEAR and WORKFLOW-REFRESH are monitor-handled control messages. They are claimed and acknowledged by the supervisor after successful control cue delivery; they are not normal work prompts. For the current tmux transport, CLEAR sends /clear. Future SDK transports must expose a real session reset or document their best-effort semantics before they become public.

The supervisor owns mailbox leases for its agent and long-polls with wait=true and peek=false. Delivery is serial and control-first. If driver delivery fails, the message is not acknowledged so the relay lease can expire and requeue it. Surface telemetry is observability-only; acknowledgement and driver delivery failures remain real errors. Shutdown aborts the in-flight long poll and never acknowledges a partially delivered message. Persistent relay errors back off instead of spinning. The independent resolve consumer is also a real long poll: the relay holds an empty queue for 30 seconds and notifies it when a command is enqueued; an empty timeout has a 2-second local gap. Both mailbox and resolve keep running with capped backoff across 429/503 responses.

The CLI entrypoint is explicit about ownership:

sailorbridge supervise --cli codex --cwd /repo --i-own-this-agent

--i-own-this-agent is required as a runtime collision guard. For tmux launches, the built-in Claude/Codex default commands include the matching no-prompt flags unless explicit safety flags request a constrained command. Explicit command argv is used verbatim and cannot be combined with safety flags. The runtime fetches the relay monitor record before start/relaunch and uses session_handle only when should_resume is true and the selected driver reports resume: true. After every successful start/rebuild it patches the driver's live handle back to the relay; for tmux this is the final collision-resolved session name consumed by roster tmux_session_name.

The liveness loop no longer patches relay presence. Relaunch is attempted only from driver.on("exit") or isAlive().processAlive === false, with capped exponential backoff and a give-up state after repeated failures. replyAlive never causes a relaunch by itself.