npm.io
0.0.1 • Published 2h ago

@myrialabs/ptykit

Licence
MIT
Version
0.0.1
Deps
2
Size
235 kB
Vulns
0
Weekly
0

PtyKit

PtyKit

PTY sessions over WebSocket for Node & Bun.
Collaborative rooms, serialized-scrollback reattach, and a resilient browser client — one typed API.

Website · npm · API reference · Server · Client · Examples · Issues

npm version License: MIT Node 18+ and Bun


PtyKit runs interactive shells server-side and streams them to the browser over a single WebSocket. Output is kept in a headless terminal so a refresh, a dropped connection, or a second viewer all replay the exact screen — no lost bytes, no double output. The PTY backend is auto-detected (bun-pty on Bun, node-pty on Node); you bring the auth.

// Server
import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit';

const manager = new PtyKit();
const server = createPtyKitServer(manager, {
  path: '/pty',
  authorize: (ctx) => ctx.conn.data.user?.canAccess(ctx.namespace) ?? false,
});

Bun.serve({ port: 3000, fetch: server.fetch, websocket: server.websocket });
// Browser
import { PtyKitClient } from '@myrialabs/ptykit/client';

const client = new PtyKitClient({ url: '/pty', namespace: 'project-42' });
const session = await client.attach('project-42-terminal-1');
session.onData((chunk) => term.write(chunk));
term.onData((data) => session.write(data));

Why PtyKit

  • WebSocket only — one multiplexed control + data channel. No SSE, no transport option, no polling.
  • Collaborative rooms — output broadcasts to a room (default = namespace), so N clients 1 session. Multiple viewers see the same live terminal.
  • Reattach that just works — scrollback lives server-side in a headless xterm and replays as a single serialized frame. Survives refresh, disconnect, and tab switches with zero data loss and no double output.
  • Auto-detected backendbun-pty on Bun (the tested path), node-pty on Node (experimental). A Node consumer never builds bun-pty's rust/ffi, and vice-versa — both are optional, lazily loaded.
  • Resilient client — reconnect with exponential backoff, heal-reconnect for "open but dead" sockets, and idempotency-aware resend, all on by default.
  • Bring your own auth — an authorize hook enforces namespace access with anti-hijack ownership checks. The library ships no auth of its own.
  • Quiet, typed core — no stdout/stderr writes, sideEffects: false, JSDoc on every export, runs on Node 18+ and Bun.

Install

bun add @myrialabs/ptykit          # or: npm i @myrialabs/ptykit / pnpm add @myrialabs/ptykit

The PTY backend is an optional dependency resolved at runtime: bun-pty on Bun, node-pty on Node. For the Node WebSocket server, ws is used (also optional). Browser peers (@xterm/xterm, @xterm/addon-fit) and svelte are optional peer dependencies you already have in a frontend.

Entry points

Import What
@myrialabs/ptykit Core session engine (PtyKit) + WebSocket server (createPtyKitServer).
@myrialabs/ptykit/client Framework-agnostic browser client (mountTerminal, PtyKitClient, attachFit).
@myrialabs/ptykit/svelte Official Svelte component (<PtyTerminal/>).

Quick start

Task API
Create the manager const m = new PtyKit({ scrollback: 5000 })
Mount on Bun Bun.serve({ fetch: server.fetch, websocket: server.websocket })
Mount on Node await server.attach(httpServer)
Terminal (vanilla) await mountTerminal(el, { url: '/pty', namespace, sessionId, create: true })
Attach (client) await client.attach(sessionId)
Create (client) await client.create({ cols, rows })
Stream output session.onData((chunk) => term.write(chunk))
Send keystrokes session.write(data)
Resize attachFit(session, term, fitAddon)
Svelte <PtyTerminal {sessionId} url="/pty" namespace="project-42" />

Server

createPtyKitServer mounts onto the HTTP server you already have.

Bun — wire fetch + websocket into Bun.serve:

Bun.serve({ port: 3000, fetch: server.fetch, websocket: server.websocket });

Node — attach to an http.Server (uses the optional ws package):

import http from 'node:http';
const httpServer = http.createServer(app);
await server.attach(httpServer);
httpServer.listen(3000);

The PtyKit manager owns the sessions and is transport-agnostic:

const manager = new PtyKit({
  scrollback: 5000,         // headless xterm lines
  idleTtl: null,            // sessions live until killed; a number opts into idle reaping
  retainExitedMs: 5 * 60_000, // keep exited sessions this long for reconnect replay
  env: { sanitize: true, inject: { MY_VAR: '1' } }, // strip runtime pollution, inject yours
});

See docs/server.md for the full surface, operations, and events.

Collaborative rooms

Output broadcasts to a room (default = the namespace) and clients filter by sessionId, so any number of clients can attach to the same session and watch the same live terminal. The serialized reattach frame is unicast to the joining client, so existing viewers are never repainted.

const server = createPtyKitServer(manager, {
  room: (ctx) => ctx.namespace, // or group however you like
});

The resilient client

Skip the xterm boilerplate with mountTerminal — the framework-agnostic counterpart to <PtyTerminal/>. Give it a container and a url; it creates the terminal, fits it, opens the session, and wires output⇄input, while staying fully configurable.

import { mountTerminal } from '@myrialabs/ptykit/client';

const { session, terminal, dispose } = await mountTerminal(el, {
  url: '/pty',
  namespace: 'project-42',
  sessionId: 'project-42-terminal-1',
  create: true,
  onStatus: (s) => render(s),
});

Need full control? Drop down to PtyKitClient. Reconnect is on by default — exponential backoff (1s → 30s), heal-reconnect for sockets that are "open but dead", and idempotency-aware resend. On reconnect, every known session is re-attached so the room subscription and scrollback recover.

const client = new PtyKitClient({
  url: '/pty',
  namespace: 'project-42',
  reconnect: { enabled: true, baseDelayMs: 1000, maxDelayMs: 30_000, maxAttempts: 5 },
  persistence: { load, save }, // optional: own the active-session id yourself
});

client.onStatus((s) => render(s)); // 'connected' | 'reconnecting' | 'disconnected'

See docs/client.md for mountTerminal, attachFit, persistence, and the session API.

Svelte

<script>
  import { PtyTerminal } from '@myrialabs/ptykit/svelte';
</script>

<PtyTerminal sessionId="project-42-terminal-1" url="/pty" namespace="project-42" />

The component is fully configurable (theme, font, reconnect, lifecycle callbacks, …). See docs/svelte.md.

Security

The authorize hook defaults to allow-all so the package is friendly to try locally — this is unsafe in production. A network-reachable deployment must provide an authorize implementation that checks the connection's identity (populated by onUpgrade) against the requested namespace. PtyKit also rejects cross-namespace sessionId access (anti-hijack), but it cannot know who your users are — that's your hook's job.

node-pty status

bun-pty is the default, tested backend. The node-pty adapter exists and auto-activates under Node, but is marked experimental until the scale and auto-detect benchmarks gate it. On the benchmark machine (Node 25, macOS arm64), node-pty failed to spawn (posix_spawnp, reproduced with raw node-pty) — see bench-results.md.

Performance

Measured on a dev laptop (Apple M2, Bun 1.3.14). Reproduce with bun bench.ts; full numbers in bench-results.md.

  • Throughput overhead of the wrapped pipeline vs raw bun-pty: ~7% (target <10%) — the cost of persist-to-headless-first + batching.
  • Reattach serialize latency: p50 ~3–9ms, p95 ~10–19ms across 10KB/100KB/1MB buffers; the newest output is always present.
  • Idle footprint: ~0.13 MB of parent-process RSS per session. In-memory scrollback was fine at 100 sessions — no disk spill needed.

Documentation

  • API reference — every export across the three entry points.
  • ServerPtyKit, createPtyKitServer, rooms, authorize.
  • ClientmountTerminal, PtyKitClient, reconnect, persistence, attachFit.
  • Svelte — the <PtyTerminal/> component.
  • Examples — runnable scenarios.

Support

If PtyKit is useful to you, consider supporting its development:

Method Address / Link
Bitcoin (BTC) bc1qd9fyx4r84cce2a9hkjksetah802knadw5msls3
Solana (SOL) Ev3P4KLF1PNC5C9rZYP8M3DdssyBQAQAiNJkvNmPQPVs
Ethereum (ERC-20) 0x61D826e5b666AA5345302EEEd485Acca39b1AFCF
USDT (TRC-20) TLH49i3EoVKhFyLb6u2JUXZWScK7uzksdC
Saweria saweria.co/myrialabs

License

MIT — see LICENSE.

Keywords