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

@cbreland/coop-cli

v0.1.13

Published

Stream your terminal to your phone. Agent-agnostic, peer-to-peer, zero config.

Downloads

1,638

Readme

coop

Stream your terminal to your phone. Agent-agnostic. Peer-to-peer. Zero config.

npm license node

Run one command on your dev machine, scan a QR with your phone, and your terminal is live on the other device with full fidelity — spinners, colors, ANSI redraws, all of it. Works with Claude Code, Aider, Gemini CLI, plain shells, anything that runs in a terminal. Peer-to-peer over WebRTC — terminal bytes never touch a server.

npm install -g @cbreland/coop-cli
coop

Then scan the QR with the Coop app (or paste the COOP-XXXX-XXXX short code). That's the whole setup.


Node.js CLI daemon that spawns shells in PTYs, pairs with a phone/browser client via a short code, and streams terminal output both ways. Multi-session (multiple terminals per daemon), multi-client (multiple devices simultaneously), persistent identity (same pairing code forever), and Claude Code hook-driven notifications with Web Push fallback for iOS background.

The daemon is fully standalone. It talks only to the WebRTC pairing relay, STUN/TURN, and (when the user opts into Claude Code notifications) open-standard Web Push endpoints. No Firebase, no Google APIs beyond STUN, no auth providers, no telemetry, no update checks. Anything that looks like "Coop's backend" is client-side.

Network boundaries

Every outbound connection the daemon makes, by design:

| Endpoint | Purpose | When | Whose infra | |----------|---------|------|-------------| | wss://coop-production-fdc9.up.railway.app | WebRTC signaling (pairing relay) | Always, while running | Coop (relay only — no user data stored) | | https://coop-production-fdc9.up.railway.app/turn-credentials | Fetch short-lived ICE server list (STUN + TURN) | Once per connect | Coop relay (master TURN key lives server-side, never shipped in the package) | | stun:stun.cloudflare.com:3478, turn:turn.cloudflare.com:…, turns:… | NAT traversal (STUN) and TURN relay when P2P fails | During WebRTC handshake | Cloudflare Realtime TURN; credentials are per-request, TTL ~24h | | stun:stun.l.google.com:19302 | STUN fallback if the credentials fetch fails | During WebRTC handshake (rare) | Google's public STUN, stateless, no auth | | https://fcm.googleapis.com/..., https://web.push.apple.com/..., https://updates.push.services.mozilla.com/... | Web Push delivery | Only when a Claude Code hook fires AND the client is disconnected AND has registered a push subscription | Standard Web Push (RFC 8030); daemon auths with VAPID keys it generates locally in ~/.coop/vapid.json |

Everything else is local-only: PTYs, scrollback, Claude Code hook socket (~/.coop/hook.sock), identity files (~/.coop/identity.json, ~/.coop/vapid.json), and the hook config in ~/.claude/settings.json.

The daemon does not talk to any Coop Firebase project, does not verify ID tokens, does not register users anywhere. User/auth/daemon-list persistence is entirely the client's responsibility.

Robustness contract

  • Token persists. Once the daemon generates a pairing token (COOP-XXXX-XXXX), the same code stays valid forever. Saved to ~/.coop/identity.json. Use --regen-token to rotate.
  • Daemon stays up unless you tell it to stop. Survives all clients disconnecting, signaling reconnects, internet blips, and individual PTY exits. Only shuts down on explicit signals (Ctrl-C / Ctrl-\ / kill).
  • PTY sessions stay alive until their shell exits or a client explicitly closes them. The daemon never auto-kills them.
  • coop always shows the QR + code on startup. No "skip if running" — you can always see how to connect.
  • coop code prints the QR + code on demand without affecting a running daemon. Useful when the daemon's terminal is buried or you forgot the code.

Install

From npm (the normal path)

npm install -g @cbreland/coop-cli

Then coop is on your PATH. Requirements: Node.js ≥ 20 on macOS or Linux. The installer also runs a best-effort check for jq and nc, which the Claude Code hook pipeline uses — if either is missing you'll get a friendly one-line message telling you how to install them (daemon runs fine without).

From this checkout (development)

cd cli
npm install
npm link            # symlinks `coop` to this checkout; edits take effect immediately
coop --help

Unlink with npm unlink -g @cbreland/coop-cli.

From a tarball

cd cli
npm pack                                       # produces cbreland-coop-cli-<version>.tgz
npm install -g ./cbreland-coop-cli-0.1.1.tgz

Useful for handing a build to a teammate without publishing.

Post-install behavior

Two scripts run automatically on install:

  • scripts/fix-pty-helper.js — chmods node-pty's prebuilt spawn-helper binary so posix_spawnp works on first coop run.
  • scripts/check-deps.js — prints a warning (not an error) if jq or nc aren't on PATH. These are OS-level tools used by the Claude Code hook forwarder; both ship with macOS and most Linux distros.

Neither blocks the install.

Run

coop                  # headless: client drives the terminal size (default)
coop -v, --verbose    # print full status stream — signaling state, client join/leave,
                      # resize events, session lifecycle. Useful for debugging.
coop --mirror         # mirror: local terminal drives the size
coop --regen-token    # rotate the pairing token (invalidates the old code)
coop code             # print the current QR + code without starting the daemon
coop --help

By default the daemon is quiet — it prints the QR code, the short code (COOP-XXXX-XXXX), and a single Ready. Scan the QR above to connect. line, then stays silent until you interact. Errors always print to stderr regardless of verbosity. Add --verbose when debugging a connection to see the full event stream; or set COOP_DEBUG=1 for a structured JSONL log at /tmp/coop-debug.log.

The token is persisted across daemon restarts; the same code stays valid forever until you --regen-token or delete ~/.coop/identity.json.

Modes

Headless (default)

coop

The client is the authoritative terminal. The PTY starts at 120×40 and resizes to whatever the client reports. The local terminal stays untouched — it just shows [coop] status lines. Ctrl-C shuts down the daemon cleanly.

Use this when the phone/browser is the primary surface.

Mirror (opt-in, --mirror or -m)

coop --mirror
# or: COOP_MIRROR=1 coop

tmux-style shared attachment. The daemon hands the local terminal over to the PTY after the QR prints, so whatever you type runs in the shell and the client mirrors it byte-for-byte. Requires a real TTY on both stdin and stdout; falls back to headless with a warning otherwise.

  • Local resize re-fits the PTY and notifies the client.
  • Client resize requests are ignored — the local terminal is the source of truth.
  • Status lines go to stderr so they don't corrupt shell output.
  • Quit key: Ctrl-\ (Ctrl-backslash). Cleanly kills the PTYs, disconnects, restores cooked mode, and exits.
  • Status flash: Ctrl-] (Ctrl-rightbracket). Prints a one-line session summary to stderr — [coop] 3 sessions: 0(zsh) 1(zsh) 2(bash) — and immediately returns to pass-through. No command mode.
  • Ctrl-C passes through to the shell, so claude / vim / etc. get their interrupt.

Mirror mode pins the client's grid for the primary session (id 0) to the local terminal's width. Background sessions opened from the client are independently sized and don't appear locally.

Multi-session

Up to 256 PTY sessions concurrently, multiplexed over the connection. The client opens, closes, and routes input per session; each session has its own scrollback ring buffer (1 MB cap each). Session id 0 is the initial session; new sessions get monotonically-increasing ids (1, 2, 3, …).

Multi-client

The daemon accepts N concurrent clients at the same pairing code. Every connected device sees the same PTY output, can send input (interleaved at the PTY — your phone, your tablet, your laptop browser all live at once). One client at a time is "focused" — its viewport size drives the PTY size. Clients connect, disconnect, and switch focus independently; one device leaving doesn't affect the others.

The daemon broadcasts a central event feed (snapshot + delta events) over the data channel so the frontend can render reactively. See client/docs/multi-client-protocol.md for the full event catalog and CONTROL action set.

Identity in the snapshot event

Every snapshot event carries daemonId (stable across restarts, from ~/.coop/identity.json) and machineName (from os.hostname()). Clients typically persist { daemonId, token, machineName } to localStorage when a pairing succeeds so they can auto-reconnect on page reload — entirely their own bookkeeping, no daemon-side registry involved.

Notifications

Coop can push real-time notifications to the client when Claude Code finishes a task, needs permission, idles, or errors out. First-run setup is automatic:

  1. A Unix domain socket is created at ~/.coop/hook.sock (mode 600).
  2. Claude Code hooks are merged into ~/.claude/settings.json for Notification, Stop, StopFailure, SessionStart, SessionEnd. Existing user hooks are preserved; only coop-hook entries are added.
  3. Each PTY is spawned with a unique COOP_SESSION_ID env var so hook events can be routed back to the right session tab.
  4. coop-hook (shipped in this package) tags each hook payload with that id and forwards it to the socket via nc -U.

The daemon rate-limits duplicate notification types (one per type per session per 2 s) and debounces Stop events by 500 ms so Claude's mid-task tool-use chains don't fire spurious "task complete" notifications.

Dependencies. The coop-hook shim uses jq and nc. Both ship with macOS and most Linux distros. If a hook fails for any reason, Claude Code keeps running — Coop outages are never allowed to break the user's session.

Uninstall. To strip Coop's hooks from ~/.claude/settings.json without touching your own, run: node -e "require('./cli/src/hook-config').uninstall()" (from the repo checkout, or a global install).

Reconnection

If the client drops, the daemon prints a new pairing code and waits again — every session stays alive and its scrollback is replayed to the next client so they don't see black tabs.

Configuration

| Env var | Default | Meaning | | -------------------- | ---------------------- | ------------------------------------------------ | | COOP_SIGNALING_URL | production endpoint | Override the pairing service URL. | | COOP_MIRROR | unset | Set to 1 for mirror mode (same as --mirror). | | COOP_DEBUG | unset | Set to 1 to enable JSONL diagnostic logging. | | COOP_DEBUG_PATH | /tmp/coop-debug.log | Where the debug log is written (append mode). |

Debugging

Every run prints a transport summary on exit:

[coop] diag: sent N frames / M bytes, recv K frames / L bytes

A fast sanity check for whether bytes reached the client.

For deeper tracing, run with COOP_DEBUG=1. A JSONL log is appended to /tmp/coop-debug.log containing the full session lifecycle and per-frame metadata. Tail it with:

tail -f /tmp/coop-debug.log | jq .

Tests

npm test

Uses node:test. All suites run locally without network.

Shutdown

Signals that kill the daemon cleanly:

  • Ctrl-\ — mirror-mode quit key.
  • Ctrl-C — headless quit (in mirror mode, Ctrl-C flows to the PTY).
  • SIGTERM / SIGHUP — external kill and terminal close both work.

All paths funnel through a single shutdown routine with a 500 ms grace window, then a forced exit. No stragglers.

Pairing code format

8 characters from a reduced alphabet (no ambiguous I/L/O/U), displayed as COOP-XXXX-XXXX. Typed or scanned, both produce the same canonical token. See src/token.js.