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

osc-terminal-bridge

v0.3.0

Published

Run interactive CLI agents (claude / codex / copilot / a shell) on this machine and drive them from the osc app — a token-authed sidecar over node-pty + tmux. Add a remote machine to your bridges with one command.

Readme

osc-terminal-bridge

Run an interactive CLI agent — Claude Code / Codex / Copilot / Cursor — from the chatbot UI (or a plain browser tab). Keystrokes flow in, the live TUI streams back. Add this machine to the app with one command:

npx osc-terminal-bridge --token <your-secret>
browser (xterm.js) ──WebSocket──► node-pty ──► tmux session ──► claude / codex / copilot
        ▲ keystrokes / resize                 durable + multi-attach
        └──────────────── ANSI bytes ◄────────────────────────────┘

Two front-ends, one sidecar:

  • In the chatbot UI — the Chat · Terminal switch in a chat swaps the body to a live terminal: one shell session per chat, opened in that chat's workspace (the same dir the chat's bash/readFile tools use, uploads included), with one-click launch chips for Claude / Codex / Copilot / Cursor. Config comes from /api/terminal/config.
  • Standalone pagehttp://<host>:7681/ serves a self-contained xterm page with per-agent buttons (handy for quick tests or phone access).

It's a sidecar, not a Next route: Next handlers can't hold a long-lived WebSocket or own a PTY. Local backend only — there's no Vercel-prod version (no long-lived WS, no PTY), the same boundary the app uses for background/bash.

Prerequisites

  • Node — uses node-pty (ships prebuilt; only needs a C/C++ toolchain if it falls back to building — see Troubleshooting).
  • tmux — required for session resume / durability. Without it every connect spawns a fresh agent and disconnect kills it. brew install tmux (macOS) or apt install tmux (Linux). The bridge detects tmux once at startup, so install it before starting (or restart the bridge after).

Quick start — add this machine to your bridges

1. Run the bridge (one line)

npx osc-terminal-bridge --token <your-secret>              # local URL (tunnel it yourself)
npx osc-terminal-bridge --token <your-secret> --tunnel     # ALSO opens a public https:// URL

It installs, listens on port 7681, and prints the URL + Token to add to the app. Re-running on the same box reuses the token (it's remembered in ~/.osc-terminal-bridge.token).

  • --tunnel — if the app is hosted / not on this box, this opens a public https:// URL automatically (via cloudflared, else ngrok) and prints that — no separate tunnel step. The URL is ephemeral (changes each run), so it's for quick connects; a permanent box wants a stable URL (a named cloudflared tunnel / reserved ngrok domain). Keep the process running. Needs cloudflared (brew install cloudflared) or ngrok installed.

Requirements: Node ≥ 18, plus tmux for durable/resumable sessions (brew install tmux / apt install tmux — install it before starting). Options: --port <p>, --host <h> (default 127.0.0.1; use 0.0.0.0 for LAN), --tunnel, --help. To run from a checkout instead: cd terminal-bridge && npm install && npm start.

2. Add it in the app's /bridges page

Open the app → Bridges (sidebar) → Add bridge, and paste the URL and Token the command printed. The app stores the token encrypted, shows the box online, and new chats can run on it (or set it as your ★ default). No app restart, no .env.local editing.

| How the app reaches this box | URL to add | Start the bridge with | |---|---|---| | App on the same machine | http://localhost:7681 | default (127.0.0.1) | | Another device on your LAN | http://<lan-ip>:7681 | --host 0.0.0.0 | | A hosted app (over https) | the tunnel's https://… URL | default + tunnel it (step 3) |

A hosted/TLS app can only reach a public https/wss bridge, so it must be tunneled (step 3); a raw http://localhost only works when the app runs on the same box or LAN.

The legacy TERMINAL_BRIDGE_URL / TERMINAL_BRIDGE_TOKEN env vars still work as an optional bootstrap, but the /bridges registry above is the primary way to add boxes now.

3. ngrok — remote access (optional)

Tunnel the bridge so a remote browser can reach it, picking the region nearest you. Each keystroke is a round-trip, so a far region adds 200–400 ms of typing lag:

ngrok http 7681 --region ap        # ap / eu / us / au / jp / in / sa — choose closest
# -> https://abc123.ngrok-free.app

Then either:

  • Standalone page: open https://abc123.ngrok-free.app/?token=<TB_TOKEN>.
  • In-chat terminal from a remote browser: set TERMINAL_BRIDGE_URL=wss://abc123.ngrok-free.app (+ matching token) in .env.local and restart pnpm dev.

Free ngrok domains change every restart — update the env line and restart pnpm dev each session, or reserve a domain. Add --basic-auth "user:pass" for a second lock.


Resume / durability (tmux)

With tmux active, the per-chat session is named tb-<chatId> and uses tmux new-session -A (attach-or-create):

  • Refresh / switch chats / re-open Terminal → re-attaches the same live session — scrollback intact, and whatever agent you launched still running.
  • Disconnecting only detaches the viewer — the session keeps running in the background.
  • Watch/drive from your own terminal: tmux attach -t tb-<chatId>.
  • /quit-ing an agent drops the pane back to the shell — hit a launch chip (or type the command) to start another.
  • Hide tmux's status bar if you don't want it: tmux set -g status off.

Live attach to any session — /ws?attach= (0.3.0)

Besides the per-chat tb-<chatId> flow, the WS can attach to an existing session by exact name — any tmux session on the box (not just tb-*) or a GNU screen session. This backs the app's Live fleet view, which lists every running session/agent on your boxes and opens any of them live. Attach-only: it never creates a session and never changes its options.

/ws?attach=tmux:<session-name>       # tmux attach-session -t =<name>  (exact match)
/ws?attach=screen:<pid>.<name>       # screen -x <id> || screen -r <id>
  • tmux needs no extra software. screen attach needs screen installed (caps.screen). A missing target closes with 4404, a bad target 4400, screen-unavailable 4501 — so the client shows a reason instead of retrying.
  • Disconnecting detaches the viewer; the session (tmux server / screen backend) keeps running, same as the tb- flow.

Feature detection. GET /control/health advertises what this bridge supports so the app can gate its UI (and never send attach= to an old bridge that would ignore it):

{ "ok": true, "tmux": true, "dataDir": "…", "hostname": "…",
  "version": "0.3.0",
  "caps": { "rawAttach": true, "screen": true, "listPanes": true } }

A bridge with no caps field is ≤ 0.2.0 — treat raw attach as unavailable and update it (npm i -g osc-terminal-bridge@latest, or just re-run npx osc-terminal-bridge@latest). Existing tb-* sessions still attach on old bridges via the per-chat flow.


Config reference

Bridge env vars (passed to npm start)

| Var | Default | Purpose | |---|---|---| | PORT / HOST | 7681 / 127.0.0.1 | where the sidecar listens (HOST=0.0.0.0 for LAN) | | TB_TOKEN | (none) | shared secret; set before exposing. Must equal the app's TERMINAL_BRIDGE_TOKEN | | TB_CWD | $HOME | working dir the agent starts in | | TB_CMD_CLAUDE / TB_CMD_CODEX / TB_CMD_COPILOT / TB_CMD_CURSOR | claude / codex / copilot / agent | per-agent command (e.g. gh copilot; Cursor CLI is agent, override → cursor-agent) | | TB_USE_TMUX | 1 | 0 bypasses tmux (no durability) | | TB_SESSION_PREFIX | tb- | tmux session name prefix | | TB_DATA_DIR | $HOME/.osc-sandboxes | per-chat workspace root for remote-spawned workers; the control API remaps a new-session start-dir here by chatId |

Chatbot app env vars (project-root .env.local)

| Var | Purpose | |---|---| | TERMINAL_BRIDGE_URL | WS base the browser connects to (table above). Unset → the app guesses ws://<app-host>:7681 | | TERMINAL_BRIDGE_TOKEN | Must equal the bridge's TB_TOKEN. Handed only to signed-in users via /api/terminal/config, never shipped in the bundle | | REMOTE_NODE_URL / REMOTE_NODE_TOKEN | Point the orchestrator's control plane at this box's /control/* API (the https:// origin + TB_TOKEN). Set both → chats spawn/read/tell/stop here. See below + docs/remote-nodes.md |


Control API — remote orchestration (v1a + v1b)

Besides the live-view WebSocket (/ws), the bridge exposes a small token-authed HTTP surface so the app can drive an orchestrator's worker agents on THIS box (not the app's host) — the "node agent" half of the remote-nodes design (see docs/remote-nodes.md). Every request needs Authorization: Bearer <TB_TOKEN> (when TB_TOKEN is set).

| Method + path | Body | Purpose | |---|---|---| | GET /control/health | — | { ok, tmux, dataDir, hostname, version, caps } — liveness + the box's TB_DATA_DIR, its hostname, this bridge's version, and caps (feature flags, see below) | | POST /control/tmux | { "args": string[] } | run ONE tmux command → { stdout, ok } (the terminal seam) | | GET /control/envs | ?dir= (optional) | this box's conda envs → { environments: [{name,value}] } — backs the app's env picker | | POST /control/exec | { cmd, args?, cwd?, timeoutMs?, detached?, logPath? } | run a command → { exitCode, stdout, stderr }, or detached → { pid } (the sandbox runCommand) | | POST /control/fs | { op: read\|readBuffer\|write\|mkdir\|exists\|stat\|rm\|readdir\|realpath, path, … } | filesystem ops on the box; bytes are base64 (the sandbox fs.* — uploads, skills, file-browser) |

/control/tmux is the single seam: the app builds the tmux args (spawn / capture / send-keys / kill / list) and this runs them. It accepts only the verbs the app uses (new-session, has-session, capture-pane, send-keys, kill-session, list-sessions, list-panes, set-option) and remaps a new-session start-dir under TB_DATA_DIR. Point the app at it with REMOTE_NODE_URL (the https:// origin) + REMOTE_NODE_TOKEN (= TB_TOKEN).

/control/exec + /control/fs back the RemoteNodeSandbox (v1b): the chat's own bash/readFile/writeFile and uploaded files run on this box too, sharing one workspace (TB_DATA_DIR/<chatId>) with the spawned workers.

Smoke test:

curl -s -X POST "$URL/control/tmux" -H "authorization: Bearer $TB_TOKEN" \
  -d '{"args":["new-session","-d","-s","tb-smoke","--","bash","-lc","echo hi; exec bash -l"]}'
curl -s -X POST "$URL/control/tmux" -H "authorization: Bearer $TB_TOKEN" \
  -d '{"args":["capture-pane","-t","tb-smoke","-p"]}'   # → shows "hi"

Security ⚠️

A full interactive shell on the host, plus agents that run arbitrary code (RCE by design):

  • Never expose without TB_TOKEN. Prefer keeping it on 127.0.0.1 and reaching it through the authenticated app, or via an SSH tunnel.
  • Treat the ngrok URL + token as secrets; add --basic-auth on ngrok.
  • The /control/* API is the same RCE surface over HTTP (it runs LLM-driven tmux/shell on this box). Same rule: strong TB_TOKEN, TLS only, a box you trust.
  • Local backend only — no Vercel-prod version (isLocalBackend() boundary).

Troubleshooting

  • Every click starts a NEW session / nothing resumes — tmux isn't active. Install tmux and restart the bridge (detected only at startup); confirm tmux=true in the log.
  • In-chat: "Couldn't reach the terminal bridge at ws://…" — wrong/missing TERMINAL_BRIDGE_URL (the browser can't reach that address) or it fell back to a guess. Set it per the URL table and restart pnpm dev. If it instead connects then immediately drops, the token doesn't match (TB_TOKENTERMINAL_BRIDGE_TOKEN).
  • Typing lag (200–400 ms) — a network round-trip per keystroke. Use a nearer ngrok --region, or stay local (ws://localhost). TCP_NODELAY is already enabled in the bridge.
  • posix_spawnp failed on macOS — node-pty's prebuilt spawn-helper lost its +x bit; npm run postinstall (auto-run on install) restores it.
  • EACCES … ~/.npm/_cacache — borked global npm cache (often a past sudo npm). Install with a scratch cache: npm install --cache /tmp/tb-npm-cache.

If node-pty won't build

Needs Python 3 + a C/C++ toolchain (Xcode CLT on macOS, build-essential on Linux). Or use the prebuilt drop-in node-pty-prebuilt-multiarch and change the import in server.mjs.


Files

Sidecar (terminal-bridge/):

  • server.mjs — HTTP + WS sidecar; spawns agents via node-pty (+ tmux).
  • public/index.html — standalone xterm page.
  • smoke.mjs — headless PTY round-trip test (npm run smoke, needs a running bridge).
  • durtest.mjs — headless resume test (npm run durtest, needs a running bridge + tmux).
  • scripts/fix-pty-perms.mjs — postinstall chmod for node-pty's spawn-helper.

In the app:

  • components/chat/terminal-view.tsx — xterm client (connects to the sidecar).
  • components/chat/chat-mode-selector.tsx — the Chat · Terminal switch.
  • lib/terminal-bridge.ts — shared mode type, AGENT_LAUNCHERS + URL/session helpers.
  • app/(app)/(chat)/api/terminal/config/route.ts — auth-gated { url, token, cwd } (cwd = the chat's workspace dir).
  • components/chat/shell.tsx — holds the per-chat mode and swaps body ⇄ terminal.