Commits
Big release. Highlights:
- Session naming refactor: every session has an immutable `name` (short
random id by default) and an optional mutable `displayName`. `pty
rename` sets/clears the displayName; every CLI ref accepts either.
- Interactive TUI "Create new session..." is now one keystroke.
- Security audit fixes: socket-path length, acquireLock atomicity,
PacketReader size cap, --isolate-env, umask tightening.
- Mouse tracking mode replay, EventFollower new-file offset, and
session_exit-on-SIGTERM race fixes.
- Remote sessions via pty-relay now render with tags + displayName in
pty list --remote.
- pty up removes tags dropped from pty.toml.
- Client API adds extractFilterTags/matchesAllTags, setDisplayName,
launcher override on SpawnDaemonOptions, and
PtyHandle.alternateScreen/kittyKeyboardFlags.
fix: resolve sendData on 'finish' instead of 'close'
sendData was hanging in Linux namespace containers (e.g. Namespace.so
CI runners) because it resolved on the socket's 'close' event, which
requires both sides to half-close. Some container network stacks don't
reliably trigger the server's auto-half-close, so the client waits
forever for a FIN that never comes.
'finish' fires when socket.end() has flushed the writable side to the
kernel — for a fire-and-forget send over a Unix domain socket that's
exactly the right guarantee: the bytes are in the server's recv buffer,
which the kernel will hold until the server reads them regardless of
what happens to our userspace socket.
Only sendData had this bug. The other socket.on('close', ...) sites
(SessionConnection lifecycle, peekScreen safety net) are correctly
waiting for the full close signal.
Ensures a fire-and-forget DATA socket (connect → write → end) gets
the 'finish' event quickly, rather than hanging waiting for the server
to send FIN back ('close' event). This pattern is unreliable in Linux
namespace containers.
Refs #18
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes #18
'close' requires both sides to send FIN (full bidirectional TCP close).
In Linux namespace containers, the server's automatic FIN via
allowHalfOpen: false is unreliable, causing send() to hang until timeout.
'finish' fires when socket.end() has been flushed to the OS kernel,
which is sufficient for Unix domain sockets.
Every session now has an immutable `name` (the stable id) and an
optional, mutable `displayName`. Default `pty run` generates a random
8-char id for `name` and the old human-friendly cwd+command label for
`displayName`. `--no-display-name` opts out of the label. `pty rename`
sets/changes/clears the displayName, either from inside a session
(using PTY_SESSION) or against an explicit ref from outside.
Every command that takes a session reference now accepts either the
name or the displayName. The list and interactive TUI render the
displayName primarily, with the stable id in parens when both exist.
Existing sessions on disk are unchanged (displayName just stays
absent). New sessions created after this release have random ids in
PTY_SESSION, events, and ptyfile.session — this is a behavioral change
for anyone comparing those values as strings.
✻ Cogitated for 13m 45s
- EventFollower.watchFile now starts at offset 0 when the directory
watcher detects a brand-new events.jsonl. session_start is almost
always already in the file by the time the dir event fires, so
starting at EOF was skipping it. Pre-existing files on startup still
start at EOF (no history replay).
- Server.close() now awaits ptyProcess.onExit (bounded at 2s) before
flushing the EventWriter. When the daemon was killed via SIGTERM,
session_exit was queued on the async write chain but the daemon
exited before the append landed. pty kill no longer loses exit
events.
- Reject session names whose Unix-socket path would exceed the 104-byte
sun_path limit. Previously validateName passed them, the daemon's
listen() failed silently inside an error handler, and every caller
awaited server.ready forever. server.ready also now rejects on listen
errors instead of hanging.
- Make acquireLock atomic via openSync(path, "wx"). Two processes racing
to steal a stale lock can no longer both win; the loser gets EEXIST
and returns false.
- Cap inbound packet length at 32 MiB in PacketReader. A malformed
[type, length=0xFFFFFFFF] header previously caused unbounded buffer
growth; oversize now throws PacketTooLargeError and every socket
handler (server, client, connection, builders, testing) destroys the
peer.
- Add pty run --isolate-env (plus isolateEnv/extraEnv on
SpawnDaemonOptions and ServerOptions) to spawn session children with
an env scrubbed down to PATH/HOME/USER/LOGNAME/SHELL/TERM/COLORTERM/
LANG/TZ/PWD/TMPDIR/LC_*/PTY_SESSION_DIR/PTY_SESSION. Opt-in; default
behaviour is unchanged. Intended for pty-relay's remote-spawn path so
operator secrets don't leak to remote clients.
- Tighten umask to 0o077 around the socket listen() to close the brief
window where the socket inode was group/world-readable before the
chmod(0o600) ran.
Hosts embedding PtyHandle (e.g. pty-layout) now have read-only access to
two modes they need to proxy to the outer terminal: whether the child is
in the alternate screen buffer (for scroll-as-arrow-keys fallback), and
the kitty keyboard protocol flag stack (so Shift+Enter and other
modified keys can be forwarded to the focused pane).
alternateScreen reads xterm's buffer.active.type directly. The serialize
addon already captures the alt buffer and emits the mode switch on
replay, so no server-side getModePrefix change is needed.
The server already replayed SGR encoding (1006), cursor visibility, and
kitty keyboard flags via getModePrefix(), but not the tracking modes
themselves. Clients attaching mid-session saw SGR on but tracking off,
so mouse forwarding decisions (e.g. in pty-layout) got it wrong.
Non-Node callers (Bun, Deno) can now route the detached daemon launch
through a Node binary so the PTY server can load its node-pty native
addon. Defaults to process.execPath, so existing Node callers are
unaffected.
Interactive TUI discovers pty-relay on PATH and fetches remote
sessions async via ls --json. Renders local + remote groups using
groupedSelectable. Enter on a remote session spawns pty-relay
connect with pause/resume. pty list --remote appends remote hosts.
Graceful degradation when pty-relay is not installed.
spawnDaemon now kills orphaned daemons on timeout. launchd install
compiles a C wrapper binary for FDA — grant it Full Disk Access
instead of node. Install flow validates FDA via one-shot launchd
job with interactive prompt. Fix events --wait timeout not being
cancelled on match. Fix displayCommand showing duplicated command
for toml sessions. PATH baked into wrapper at compile time.
peek and peek --wait now read saved output from exited sessions
(lastLines increased to 200). --wait accepts multiple patterns
(any match). saveExitMetadata runs twice: immediately in onExit
(for status display) and again in close() (for complete output).
Fix TUI exit race by delaying session list refresh 200ms. Fix
SKILL.md examples to use multiple --wait instead of regex.
Session supervisor watches strategy tags and restarts permanent
sessions with exponential backoff. Runs as a foreground process,
integrates with launchd via esbuild bundling. Mutable tags via
pty tag command. pty kill/down properly handle supervised sessions.
Also: peek --wait blocks until text appears, peek --full shows
scrollback, events --wait blocks for event types, interactive TUI
shows [permanent] markers with color, 10s supervisor scan interval,
supervisor state in its own subdirectory, defensive meta.args
handling, and isolated shell test sessions.
Move PtyServer from ./client to ./server so client-only imports
stay free of native addon dependencies. Add ./keys export for
browser-safe key resolution. The export structure is now cleanly
tiered: browser-safe (./protocol, ./keys), Node-only (./client),
and native addon (./server, ./testing, ./tui).
Sessions can be tagged at creation with key-value pairs via
pty run --tag owner=forge -- command or the tags field in
SpawnDaemonOptions. Tags are persisted, survive process exit,
and appear in pty list --json. Closes #12.
Adds a ./protocol entry to the package.json exports map, pointing at
the existing src/protocol.ts module (built to dist/protocol.js by the
existing tsc build). This module exports PacketReader, MessageType,
Packet, and all encode/decode helpers with zero Node built-in deps
beyond Buffer — making it safe for browser bundlers.
Motivation: consumers like forge maintain a copy of the wire protocol
for browser-side xterm.js packet parsing. With this subpath they can
import from @myobie/pty/protocol instead, keeping the protocol in one
place.
resolveKey("shift+return") previously returned plain "\r" because
shift was silently ignored for non-letter keys. Now:
- CSI sequence keys (arrows, home/end, delete, pageup/down) get the
modifier parameter inserted: e.g. shift+up → \x1b[1;2A
- Control char keys (return, tab, escape, space, backspace) use
Kitty keyboard protocol CSI u encoding: e.g. shift+return → \x1b[13;2u
- shift+tab produces the legacy backtab sequence \x1b[Z
- Combined modifiers (ctrl+shift, alt+shift, ctrl+alt+shift) are
supported using the standard xterm modifier bitmask
Fixes #13
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bump version to 0.5.0, flesh out CHANGELOG with missing entries
(DA1 fix, postinstall fix, plugins, exit name), add Plugins section
to README, and audit all shell completions to cover every command
(stats, rm, gc, wrap, unwrap, test) including proper session-name
completion for pty rm and pty stats.
Fix node-pty spawn-helper chmod for pnpm GVS layouts
The previous postinstall used a relative path that silently no-op'd
under pnpm's global virtual store. Replace with a proper Node.js
script that uses createRequire + require.resolve to find node-pty
regardless of install layout.
Credit: @schickling
Fish 4.x sends a DA1 (Primary Device Attribute) query at startup
and blocks for up to 10s waiting for a response. Add a CSI handler
in PtyServer that intercepts the query and writes back a VT220
response. Add shell integration tests for bash, zsh, and fish.
Closes #5
Based on the approach by @schickling-assistant in #5
New public API covering every CLI feature: SessionConnection for
bidirectional attach without stdin/stdout, sendData/peekScreen as
Promise-based alternatives, events, keys, stats, and protocol types.
Refactor spawnDaemon to options object with optional rows/cols.
Detect when pty run is invoked inside an existing session via
PTY_SESSION env var and exec the command directly (-d bypasses).
Add CPU, memory, and PID reporting to pty stats for both the
child process and daemon.
Big release. Highlights:
- Session naming refactor: every session has an immutable `name` (short
random id by default) and an optional mutable `displayName`. `pty
rename` sets/clears the displayName; every CLI ref accepts either.
- Interactive TUI "Create new session..." is now one keystroke.
- Security audit fixes: socket-path length, acquireLock atomicity,
PacketReader size cap, --isolate-env, umask tightening.
- Mouse tracking mode replay, EventFollower new-file offset, and
session_exit-on-SIGTERM race fixes.
- Remote sessions via pty-relay now render with tags + displayName in
pty list --remote.
- pty up removes tags dropped from pty.toml.
- Client API adds extractFilterTags/matchesAllTags, setDisplayName,
launcher override on SpawnDaemonOptions, and
PtyHandle.alternateScreen/kittyKeyboardFlags.
sendData was hanging in Linux namespace containers (e.g. Namespace.so
CI runners) because it resolved on the socket's 'close' event, which
requires both sides to half-close. Some container network stacks don't
reliably trigger the server's auto-half-close, so the client waits
forever for a FIN that never comes.
'finish' fires when socket.end() has flushed the writable side to the
kernel — for a fire-and-forget send over a Unix domain socket that's
exactly the right guarantee: the bytes are in the server's recv buffer,
which the kernel will hold until the server reads them regardless of
what happens to our userspace socket.
Only sendData had this bug. The other socket.on('close', ...) sites
(SessionConnection lifecycle, peekScreen safety net) are correctly
waiting for the full close signal.
Fixes #18
'close' requires both sides to send FIN (full bidirectional TCP close).
In Linux namespace containers, the server's automatic FIN via
allowHalfOpen: false is unreliable, causing send() to hang until timeout.
'finish' fires when socket.end() has been flushed to the OS kernel,
which is sufficient for Unix domain sockets.
Every session now has an immutable `name` (the stable id) and an
optional, mutable `displayName`. Default `pty run` generates a random
8-char id for `name` and the old human-friendly cwd+command label for
`displayName`. `--no-display-name` opts out of the label. `pty rename`
sets/changes/clears the displayName, either from inside a session
(using PTY_SESSION) or against an explicit ref from outside.
Every command that takes a session reference now accepts either the
name or the displayName. The list and interactive TUI render the
displayName primarily, with the stable id in parens when both exist.
Existing sessions on disk are unchanged (displayName just stays
absent). New sessions created after this release have random ids in
PTY_SESSION, events, and ptyfile.session — this is a behavioral change
for anyone comparing those values as strings.
✻ Cogitated for 13m 45s
- EventFollower.watchFile now starts at offset 0 when the directory
watcher detects a brand-new events.jsonl. session_start is almost
always already in the file by the time the dir event fires, so
starting at EOF was skipping it. Pre-existing files on startup still
start at EOF (no history replay).
- Server.close() now awaits ptyProcess.onExit (bounded at 2s) before
flushing the EventWriter. When the daemon was killed via SIGTERM,
session_exit was queued on the async write chain but the daemon
exited before the append landed. pty kill no longer loses exit
events.
- Reject session names whose Unix-socket path would exceed the 104-byte
sun_path limit. Previously validateName passed them, the daemon's
listen() failed silently inside an error handler, and every caller
awaited server.ready forever. server.ready also now rejects on listen
errors instead of hanging.
- Make acquireLock atomic via openSync(path, "wx"). Two processes racing
to steal a stale lock can no longer both win; the loser gets EEXIST
and returns false.
- Cap inbound packet length at 32 MiB in PacketReader. A malformed
[type, length=0xFFFFFFFF] header previously caused unbounded buffer
growth; oversize now throws PacketTooLargeError and every socket
handler (server, client, connection, builders, testing) destroys the
peer.
- Add pty run --isolate-env (plus isolateEnv/extraEnv on
SpawnDaemonOptions and ServerOptions) to spawn session children with
an env scrubbed down to PATH/HOME/USER/LOGNAME/SHELL/TERM/COLORTERM/
LANG/TZ/PWD/TMPDIR/LC_*/PTY_SESSION_DIR/PTY_SESSION. Opt-in; default
behaviour is unchanged. Intended for pty-relay's remote-spawn path so
operator secrets don't leak to remote clients.
- Tighten umask to 0o077 around the socket listen() to close the brief
window where the socket inode was group/world-readable before the
chmod(0o600) ran.
Hosts embedding PtyHandle (e.g. pty-layout) now have read-only access to
two modes they need to proxy to the outer terminal: whether the child is
in the alternate screen buffer (for scroll-as-arrow-keys fallback), and
the kitty keyboard protocol flag stack (so Shift+Enter and other
modified keys can be forwarded to the focused pane).
alternateScreen reads xterm's buffer.active.type directly. The serialize
addon already captures the alt buffer and emits the mode switch on
replay, so no server-side getModePrefix change is needed.
Interactive TUI discovers pty-relay on PATH and fetches remote
sessions async via ls --json. Renders local + remote groups using
groupedSelectable. Enter on a remote session spawns pty-relay
connect with pause/resume. pty list --remote appends remote hosts.
Graceful degradation when pty-relay is not installed.
spawnDaemon now kills orphaned daemons on timeout. launchd install
compiles a C wrapper binary for FDA — grant it Full Disk Access
instead of node. Install flow validates FDA via one-shot launchd
job with interactive prompt. Fix events --wait timeout not being
cancelled on match. Fix displayCommand showing duplicated command
for toml sessions. PATH baked into wrapper at compile time.
peek and peek --wait now read saved output from exited sessions
(lastLines increased to 200). --wait accepts multiple patterns
(any match). saveExitMetadata runs twice: immediately in onExit
(for status display) and again in close() (for complete output).
Fix TUI exit race by delaying session list refresh 200ms. Fix
SKILL.md examples to use multiple --wait instead of regex.
Session supervisor watches strategy tags and restarts permanent
sessions with exponential backoff. Runs as a foreground process,
integrates with launchd via esbuild bundling. Mutable tags via
pty tag command. pty kill/down properly handle supervised sessions.
Also: peek --wait blocks until text appears, peek --full shows
scrollback, events --wait blocks for event types, interactive TUI
shows [permanent] markers with color, 10s supervisor scan interval,
supervisor state in its own subdirectory, defensive meta.args
handling, and isolated shell test sessions.
Adds a ./protocol entry to the package.json exports map, pointing at
the existing src/protocol.ts module (built to dist/protocol.js by the
existing tsc build). This module exports PacketReader, MessageType,
Packet, and all encode/decode helpers with zero Node built-in deps
beyond Buffer — making it safe for browser bundlers.
Motivation: consumers like forge maintain a copy of the wire protocol
for browser-side xterm.js packet parsing. With this subpath they can
import from @myobie/pty/protocol instead, keeping the protocol in one
place.
resolveKey("shift+return") previously returned plain "\r" because
shift was silently ignored for non-letter keys. Now:
- CSI sequence keys (arrows, home/end, delete, pageup/down) get the
modifier parameter inserted: e.g. shift+up → \x1b[1;2A
- Control char keys (return, tab, escape, space, backspace) use
Kitty keyboard protocol CSI u encoding: e.g. shift+return → \x1b[13;2u
- shift+tab produces the legacy backtab sequence \x1b[Z
- Combined modifiers (ctrl+shift, alt+shift, ctrl+alt+shift) are
supported using the standard xterm modifier bitmask
Fixes #13
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fish 4.x sends a DA1 (Primary Device Attribute) query at startup
and blocks for up to 10s waiting for a response. Add a CSI handler
in PtyServer that intercepts the query and writes back a VT220
response. Add shell integration tests for bash, zsh, and fish.
Closes #5
Based on the approach by @schickling-assistant in #5