Terminals in the browser.
Reattached.
PtyKit serves interactive shell sessions over a single WebSocket — with collaborative rooms, serialized-scrollback reattach that loses zero bytes, and a resilient client. Bring your own auth.
import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit'; const manager = new PtyKit(); const server = createPtyKitServer(manager, { path: '/pty', }); Bun.serve({ port: 3000, fetch: server.fetch, websocket: server.websocket, });
import { mountTerminal } from '@myrialabs/ptykit/client'; const url = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/pty`; await mountTerminal( document.getElementById('term'), { url, namespace: 'demo', sessionId: 'terminal-1', create: true, onStatus: (s) => { document.getElementById('status').textContent = s; }, }, );
Everything a terminal needs
over the wire.
The hard parts of streaming a real PTY to the browser — solved and typed.
authorize hook enforces namespace access with anti-hijack ownership checks. The library ships no auth of its own.From zero to a live terminal,
a few lines at a time
One call, a full terminal
mountTerminal skips the xterm boilerplate entirely. Give it a
container and a url — it creates the terminal, fits it, opens the session,
and wires output ⟶ input. The server mounts createPtyKitServer
onto any HTTP server.
- ✓ Creates
Terminal+FitAddoninternally — zero setup - ✓ Server builds the browser bundle on the fly with
Bun.build - ✓
onStatusdrives a connection-state indicator in your UI - ✓
fontSize,fitDebounceMs, and more — all configurable
import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit'; const manager = new PtyKit({ scrollback: 5000 }); const server = createPtyKitServer(manager, { path: '/pty', }); Bun.serve({ port: 8781, fetch: server.fetch, websocket: server.websocket, });
import { mountTerminal } from '@myrialabs/ptykit/client'; const wsUrl = `ws://${location.host}/pty`; await mountTerminal( document.getElementById('screen'), { url: wsUrl, namespace: 'demo', sessionId: 'demo-terminal-1', create: true, fontSize: 13, onStatus: (s) => { document.getElementById('status').textContent = s; }, }, );
Manual control with PtyKitClient
When mountTerminal is too high-level, drop down to
PtyKitClient directly. Create sessions, attach to
existing ones, pipe data programmatically — ideal for tests,
automation, or custom UIs that need full control over the
connection lifecycle.
- ✓
.create()spawns a shell session server-side - ✓
.attach()replays scrollback with zero data loss - ✓
.disconnect()/write()/onData()— full lifecycle - ✓ Works in Node, Bun, or the browser
import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit'; const manager = new PtyKit(); const server = createPtyKitServer(manager, { path: '/pty', }); const bun = Bun.serve({ port: 0, fetch: server.fetch, websocket: server.websocket, });
import { PtyKitClient } from '@myrialabs/ptykit/client'; const url = `ws://localhost:${port}/pty`; // First client: create session, run command, then disconnect. const first = new PtyKitClient({ url, namespace: 'demo', }); const s1 = await first.create({ sessionId: 'demo-1', }); s1.write('echo "ran BEFORE refresh"\r'); first.disconnect(); // Second client: attach to SAME session — scrollback replays. const second = new PtyKitClient({ url, namespace: 'demo', }); const s2 = await second.attach('demo-1'); s2.onData((c) => process.stdout.write(c)); s2.write('echo "ran AFTER reattaching"\r');
mountTerminal × 2 = collaboration
Two mountTerminal calls targeting the same session are all
it takes. Output broadcasts to a room (default = the namespace) so both
viewers see every keystroke and its result — one shell, N screens.
- ✓ Same
sessionId= same shell, shared output - ✓
mountTerminalhandles xterm, fit, and reconnect - ✓ Serialized reattach is unicast; existing viewers aren't repainted
- ✓ Type in either terminal — everyone sees it
import { PtyKit, createPtyKitServer } from '@myrialabs/ptykit'; const manager = new PtyKit(); const server = createPtyKitServer(manager, { path: '/pty', }); const bun = Bun.serve({ port: 0, fetch: server.fetch, websocket: server.websocket, });
import { mountTerminal } from '@myrialabs/ptykit/client'; const wsUrl = `ws://${location.host}/pty`; // Alice's terminal — creates the session mountTerminal( document.getElementById('alice-term'), { url: wsUrl, namespace: 'team', sessionId: 'team-terminal-1', create: true, }, ); // Bob joins — same sessionId = same shell mountTerminal( document.getElementById('bob-term'), { url: wsUrl, namespace: 'team', sessionId: 'team-terminal-1', create: true, }, );
Custom shell, cwd & environment
Configure which shell to launch, where to start, and what the child
process sees. env.sanitize strips runtime pollution
(Bun/npm/Vite variables), env.inject adds your own.
The manager is transport-agnostic — use it with or without a server.
- ✓
shell— force/bin/shinstead of$SHELL - ✓
cwd— start the session in any directory - ✓
env.sanitizeremoves Bun/npm/Vite pollution - ✓
env.injectadds your custom variables
import { PtyKit } from '@myrialabs/ptykit'; const manager = new PtyKit({ env: { sanitize: true, inject: { MY_VAR: 'hello' }, }, }); const session = await manager.createSession({ sessionId: 'env-1', namespace: 'local', shell: '/bin/sh', cwd: '/tmp', cols: 100, rows: 30, }); session.addDataListener((chunk) => process.stdout.write(chunk)); session.write('pwd\r'); // → /tmp session.write('echo "$MY_VAR"\r'); // → hello session.write('echo "${npm_cache:-stripped}"\r');
Up and running in a minute.
One package, three entry points. The PTY backend is an optional dependency, resolved at runtime.
A small, typed surface.
Wire up your terminal
in a few lines.
Open source, MIT licensed, and ready for production on Bun.