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

@pty-server/ptys

v0.3.0

Published

Run terminal apps as long-lived server-side sessions. Attach/detach from anywhere.

Readme

ptys

Run terminal apps as long-lived server-side sessions; attach and detach from anywhere.

Install

npm i -g @pty-server/ptys

ptys is currently in prerelease, so that command finds nothing yet. Until the first stable version, install the prerelease explicitly:

npm i -g @pty-server/ptys@next

Requirements

Node.js 24 or newer, on Linux.

This first release is developed and tested on Linux only. ptys is expected to work on macOS, and on Windows apart from daemon mode (ptys server start), but neither is covered by CI yet - node-pty prebuilds, signals, terminal I/O, and shell selection are all platform-sensitive. Coverage for other platforms is planned.

node-pty is a native addon, pinned to a version that ships prebuilt binaries for linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64 and win32-arm64. Those load without running any install script, so npm versions that block dependency lifecycle scripts by default install ptys unchanged.

Where no prebuilt binary matches, node-pty falls back to compiling itself, which needs both a C++ toolchain (python3, make, a C compiler) and permission to run that install script:

npm i -g @pty-server/ptys@next --allow-scripts=node-pty

Quickstart

# Start a background server. It binds nothing to the network: local commands
# reach it over a private Unix socket, and need no token.
ptys server start

# Only if a browser or a remote machine has to reach it: bind an address.
# That, and only that, generates the token, printed once (see Security).
ptys server start --instance browser --listen 127.0.0.1:7801

# Create a detached session, or create and attach immediately.
ptys start --name shell bash
ptys run --name editor vim

# Inspect, attach, and stop a session.
ptys list
ptys attach <id>
ptys kill <id>

# Stop the background server.
ptys server stop

Commands

| Command | Notable flags | | --- | --- | | ptys server / ptys server run | Run the server in the foreground. --instance, --listen (repeatable), --token, --no-auth, --allow-origin, --browse-root, --shell, --scrollback, --max-closed-sessions, --disable-exec | | ptys server start | Start a background daemon (not supported on Windows). Same server flags as server run. | | ptys server stop | Stop a daemon. --instance, --all | | ptys server status | Show daemon status. --instance, --json | | ptys server restart | Restart a daemon. --instance | | ptys start [cmd] [args...] | Create a detached session. Omitting cmd starts the server user's default shell. --instance, --server, --name, --workspace, --cwd, --env K=V, --size WxH, --follow-size, --token | | ptys run [cmd] [args...] | Create a session and attach to it. Omitting cmd starts the server user's default shell. --instance, --server, --name, --workspace, --cwd, --env K=V, --size WxH, --follow-size, --lossy, --token | | ptys list | List sessions. --instance, --server, --token, --json | | ptys kill <id> | Stop a session by ID, ID prefix, or name. --instance, --server, --token, --signal | | ptys rename <id> <name> | Change a session name. --instance, --server, --token | | ptys event <json> | Emit an event from an application running in a session. --request, --timeout | | ptys event-listener | Print global session events as NDJSON. --instance, --server, --token | | ptys config origin list | List origins currently allowed to make browser requests. --instance, --server, --token, --json | | ptys config origin allow/remove <origin> | Change the browser Origin allowlist immediately. --instance, --server, --token, --json | | ptys config listen list | List the addresses a running server is bound to. Control socket only: --instance, --json | | ptys config listen add/remove <addr> | Bind or unbind a TCP address on a running server. Control socket only: --instance, --json | | ptys attach <id> | Attach by ID, ID prefix, or name. --instance, --server, --read-only, --lossy, --token | | ptys bridge | Pipe stdin and stdout to a local server's control socket, byte for byte. A transport for clients such as choux, not an interactive command; see Bridge. Control socket only: --instance |

Over the control socket, a session starts in the directory ptys start or ptys run is run from, and a relative --cwd resolves against it. Over --server, a session starts at its workspace root, and a relative --cwd resolves against that root.

Without --workspace, sessions are grouped under the default runner workspace, rooted at the server user's home directory. --workspace takes a workspace id, name or id prefix; it only groups the session and never changes its directory. start and run need a server at protocol 1.1 or newer; after upgrading ptys, restart a daemon that is still running the old version.

Instances and addresses

A server is identified by its instance name, not by an address. --instance work names the pidfile, log and control socket under ~/.ptys/run (work.pid, work.log, work.sock); the default name is default. Several instances can run side by side, and a socket-only one binds nothing at all.

--listen host:port is separate, and purely about binding. It is repeatable, so one server can answer on several addresses, and passing it at all is what creates the token:

ptys server start                                                  # control socket only
ptys server start --listen 127.0.0.1:7801                          # + a loopback listener
ptys server start --instance lan --listen 127.0.0.1:7801 --listen 0.0.0.0:8080

Client commands mirror the split. --instance <name> (or PTYS_INSTANCE) reaches a local server over its control socket, with no credential. --server <addr> (or PTYS_SERVER) reaches an address over TCP, with a token - that is the path for remote servers and for Windows, which has no control socket. Passing both is an error. Every command that takes --server also takes --insecure; see Security.

Bridge

ptys bridge is a transport for clients such as choux, not an interactive command. It connects to the control socket of the instance named by --instance (or PTYS_INSTANCE) and copies stdin to the socket and the socket to stdout as raw bytes, so a client that can only spawn a process - wsl.exe --exec ..., ssh host ... - can speak HTTP/1.1 and WebSocket to the server over the child's pipes. Only protocol bytes ever reach stdout; diagnostics go to stderr.

It finds the socket exactly as other local commands do, so it reaches a foreground ptys server run as well as a daemon, and it never falls back to TCP: --server, or PTYS_SERVER without --instance, is refused. Whoever can run it holds the control socket's authority, with no token.

When the server closes the connection, including the idle close of a keep-alive connection, the bridge flushes stdout and exits. When stdin closes, it half-closes the connection; the server still answers the requests it has already received, and the bridge copies those responses until the server closes. From the moment stdin closes, or the bridge sees the connection end or fail, it has 30 seconds to finish, plus up to one second to write its final stderr line; output the reader has not taken by then is dropped. Before that point there is no deadline: a client that stops reading stdout while stdin is open stalls the bridge, which cannot tell a stopped reader from a slow one, and backpressure can keep it from even seeing the server close. A client must keep reading stdout for as long as stdin is open.

| Exit code | Meaning | | --- | --- | | 0 | The connection closed normally | | 1 | stdin, stdout or the connection failed, the socket could not be opened for another reason (such as EACCES), or shutdown did not finish within 30 seconds | | 2 | Invalid usage: --server, PTYS_SERVER without --instance, unexpected arguments, an invalid instance name, or a platform without a control socket | | 3 | No usable control socket for the instance: none exists, or only untrusted ones | | 4 | The control socket refused the connection or is gone: its server is not running |

Server configuration

~/.ptys.json is an optional JSON file containing defaults for ptys server and ptys server start. Explicit command-line flags override these defaults; repeated --allow-origin, --browse-root and --listen flags replace their corresponding arrays. server stop and server restart also use the configured instance when that flag is omitted, while server status lists every daemon unless --instance names one.

{
  "instance": "default",
  "listen": ["127.0.0.1:7801"],
  "noAuth": false,
  "allowOrigins": ["https://app.example"],
  "browseRoots": ["/absolute/path/to/workspaces"],
  "shell": "/bin/zsh",
  "scrollback": 5000,
  "maxClosedSessions": 100,
  "disableExec": false
}

All fields are optional. instance starts with a letter or digit and holds only letters, digits, dots, dashes and underscores (at most 64 characters); listen is an array of host:port strings, with IPv6 bracketed as [::1]:7801, and an omitted or empty one means no TCP listener at all; noAuth and disableExec are booleans; allowOrigins is an array of absolute http(s) origins; browseRoots is an array of existing directories; shell is a non-empty command run by sessions that name none, and defaults to this user's passwd shell rather than the daemon's inherited SHELL, which froze when the daemon detached; and scrollback and maxClosedSessions are non-negative integers. Unknown fields are rejected. Tokens are deliberately not accepted in this file: use --token, PTYS_TOKEN, or the existing ~/.ptys/token store.

The same rules apply to the equivalent command-line flags, and they are checked before anything is started, so a rejected value never leaves a listener or a pidfile behind.

maxClosedSessions controls how many exited sessions remain available for ptys list and late-attach final-screen viewing. The default is 100; set it to 0 to remove sessions as soon as they exit. A session no client has attached to yet is held for 30 seconds regardless, so ptys run still collects the output and exit status of a command that finishes before it attaches.

JSON HTTP request bodies are limited to 64 KiB.

--allow-origin seeds an in-memory browser Origin allowlist at startup. You can change it without a restart using ptys config origin allow https://app.example or ptys config origin remove https://app.example; dynamic changes are lost when the server stops.

--listen works the same way. A running server can be opened up, or closed again, without a restart:

ptys config listen add 127.0.0.1:7801     # prints the token if this is the first listener
ptys config listen list
ptys config listen remove 127.0.0.1:7801

These commands are accepted only over the control socket, so they take --instance, never --server: opening a network listener is exactly the privilege a network caller must not have, and the reply carries the token. Like runtime origins, runtime listeners are lost when the server stops - ptys server restart replays the --listen addresses the daemon was started with, and nothing else. ptys server status shows what it is bound to now.

--no-auth is for trusted local use only. It refuses any non-loopback --listen address, accepts API requests only when their Host header exactly names one of the bound addresses, and requires Content-Type: application/json for JSON API requests. Use normal token authentication for any less-trusted setup, and read Security before exposing a server beyond loopback.

Session introspection and exec

Every session on the wire carries pid, cwd and, while it is running, process - the name of whatever is in the foreground right now. POST /v1/sessions/{id}/exec complements them by running one buffered command in that session's directory and environment:

curl -H "authorization: Bearer $PTYS_TOKEN" -H 'content-type: application/json' \
  -d '{"cmd":"tmux","args":["list-windows"],"cwd":"live"}' \
  http://127.0.0.1:7801/v1/sessions/$ID/exec

cwd selects "session" (the directory it was started in, the default) or "live" (where the pty is now, so it follows cd), and the response says which one it actually used. cmd and args are passed as discrete arguments - there is no shell, so nothing in them is expanded. A command that cannot be run at all, such as a missing binary, comes back as 200 with code: null and the reason in stderr, so a client can tell a missing tool from a broken server. Output is capped at 1 MiB per stream, and a command that reaches the cap is stopped rather than drained, so truncated: true comes back with code: null and the signal that stopped it, not the exit status the command would have had. Commands time out after 5 seconds by default and 60 at most (timedOut), and a session may have 4 commands in flight at once, 16 across the server, before further requests get 429. A stopped command - timed out or truncated - is sent SIGTERM and then SIGKILL two seconds later, so a request always answers and its slot always comes back, however the command behaves.

This is on by default and grants any holder of the token the ability to run commands as the server's user, which the session API already allowed less conveniently. --disable-exec (or "disableExec": true) removes the route entirely, so it answers 404. GET /v1/info reports capabilities: ["exec"] when it is available and [] when it is not.

While attached with ptys attach or ptys run, press Ctrl-P then Ctrl-Q to detach without stopping the session. To send Ctrl-P to the attached program, press Ctrl-P twice; a lone Ctrl-P is held until the next keypress.

Attach does not reconnect. A deliberate detach exits 0 and a finished session exits with the program's own status, but a dropped connection - including the server dropping a client that cannot keep up - exits nonzero with the reason on stderr, so scripts never mistake lost output for success. Reattach with ptys attach <id>; the session is untouched.

Security

A ptys token is shell access: anyone holding it can spawn processes as the server's user. Token authentication proves who is calling; it does not encrypt anything.

Nothing is bound to the network unless --listen asks for it, and a server with no listener never creates a token at all - there is no credential to leak, and no port for anyone to reach.

Local commands do not use the token in any case. The server accepts requests on a Unix socket in a private (0700) directory - ~/.ptys/run/<instance>.sock, or $XDG_RUNTIME_DIR/ptys / /tmp/ptys-<uid> when the home path is too long for a socket address. Only the server's own user can connect to it, so no credential is presented and none is asked for, and the CLI never sends a stored token to a loopback TCP port - on a shared machine, another user can occupy that port while the server is stopped. Set PTYS_TOKEN or pass --token for remote servers, for browser clients, and on Windows, which has no equivalent socket and therefore requires --listen. Over plaintext http://, both the bearer token and the full terminal stream are readable - and the token is replayable - by anyone on the network path.

ptys does not terminate TLS itself. For any non-loopback setup, run it behind a TLS reverse proxy and point clients at the https:// address; attach and event streams follow to wss:// automatically. Minimal nginx example (the Upgrade/Connection headers are required - attach and events are WebSocket):

server {
  listen 443 ssl;
  server_name ptys.example;
  ssl_certificate     /etc/ssl/ptys.example.crt;
  ssl_certificate_key /etc/ssl/ptys.example.key;

  location / {
    proxy_pass http://127.0.0.1:7801;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 1d;
  }
}

Keep the ptys server itself on a loopback --listen address so the proxy is the only way in.

Client commands refuse a plaintext http:// address whose host is not loopback:

$ ptys list --server ptys.example:7801
ptys: refusing plaintext http:// to non-loopback host ptys.example; use https:// (terminate TLS in front of ptys) or pass --insecure / PTYS_INSECURE=1

--insecure, or PTYS_INSECURE=1, lifts that refusal for setups where the network is already trusted (for example an established VPN or SSH tunnel). It disables a safety check, not a feature - the traffic is still plaintext.

Events

Applications running inside a session can emit live events for global listeners:

ptys event '{"type":"notification","data":{"message":"job done"}}'
ptys event --request --timeout 10 '{"type":"confirmation","data":{"ask":"continue?"}}'
ptys event-listener

Every listener receives NDJSON envelopes containing sessionId, type, and data. Built-in events include session.created (the public session record), session.title, session.updated, and session.exited (with { code, signal?, at }). Request events print only the first reply's data; --timeout 0 waits indefinitely after a listener is connected.

The companion GUI/native client project is called choux.

API documentation

The generated OpenAPI HTTP contract and AsyncAPI WebSocket contract live in docs/. Load the OpenAPI file in a Swagger UI; use an AsyncAPI-compatible viewer for the terminal stream.

Versioning

@pty-server/ptys and @pty-server/protocol are versioned and released in lockstep: both carry the same version and are published together from a single vX.Y.Z tag, which the release workflow refuses unless it matches both package.json versions exactly. A prerelease version such as 0.1.0-next.0 is published under the next dist tag, never latest, so it reaches only installs that ask for it by tag or by exact version. The wire protocol carries its own integer version, reported in /v1/info and in the attach handshake; clients require an exact match and refuse to talk to a server advertising anything else.

License

MIT; see LICENSE.