mux

package
v1.50.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package mux provides PTY multiplexing functionality for external Claude sessions. It enables bidirectional terminal streaming from external processes (like Claude Code running in IntelliJ) to stapler-squad for monitoring and interaction.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CleanAllStaleSockets

func CleanAllStaleSockets() error

CleanAllStaleSockets removes all stale ssq-mux sockets.

func CleanStaleSocket

func CleanStaleSocket(socketPath string) error

CleanStaleSocket removes a socket file if it's no longer connected to a running process.

func CleanupHooksFile

func CleanupHooksFile(path string) error

CleanupHooksFile removes the generated hooks configuration file. This should be called when ssq-mux exits.

func CleanupStaleHooksFiles

func CleanupStaleHooksFiles() error

CleanupStaleHooksFiles removes any hooks files left behind by crashed ssq-mux instances. This should be called on startup to clean up orphaned files.

func EncodeMessage

func EncodeMessage(msg *Message) ([]byte, error)

EncodeMessage encodes a message to wire format.

func GenerateHooksFile

func GenerateHooksFile(meta *HooksMetadata) (string, error)

GenerateHooksFile creates a temporary hooks configuration file for ssq-mux. The file enables Claude Code hooks to send notifications to stapler-squad with proper session context for correlation and deep linking.

Returns the path to the generated hooks file, which should be set as CLAUDE_CODE_HOOKS_PATH environment variable before starting Claude.

func GenerateSessionName

func GenerateSessionName(command string) string

GenerateSessionName creates a human-readable session name based on the current directory and command. The name follows stapler-squad's naming convention for external sessions. Returns a name like "staplersquad_ext_myproject_claude_1234" where 1234 is the PID. Uses PID instead of timestamp to guarantee uniqueness across concurrent sessions.

func InteractiveSessionPicker

func InteractiveSessionPicker() (string, error)

InteractiveSessionPicker displays an interactive picker for session selection

func ListStaplerSquadSessions

func ListStaplerSquadSessions() ([]string, error)

ListStaplerSquadSessions returns a list of tmux sessions that match the stapler-squad naming convention. These are sessions that can be attached to using RunAttach.

func Run

func Run(command string, args []string) (int, error)

Run is a convenience function that starts the multiplexer, handles signals, and waits.

func RunAttach

func RunAttach(tmuxSession string) (int, error)

RunAttach attaches to an existing tmux session and creates a streaming socket. This is useful for reconnecting to orphaned sessions after a restart. The session is NOT killed when detaching, allowing future reattachment.

func RunWithName

func RunWithName(command string, args []string, sessionName string) (int, error)

RunWithName is like Run but allows specifying a custom session name. If name is empty, a descriptive name is generated based on the current directory.

func WriteMessage

func WriteMessage(w io.Writer, msg *Message) error

WriteMessage writes an encoded message to a writer.

func WriteSessionUserOptions

func WriteSessionUserOptions(sessionName, socketPath, cwd, command string, pid int, startTime int64) error

WriteSessionUserOptions stores session metadata as tmux user options on the named session. This enables the claude-squad server to discover external sessions without probing Unix sockets, and the data survives server restarts.

Callers should treat errors as non-fatal — log and continue. The socket-based discovery path remains a fallback.

Types

type AutoDiscovery

type AutoDiscovery struct {
	*Discovery
	// contains filtered or unexported fields
}

AutoDiscovery provides filesystem watching for immediate session discovery. It watches /tmp/ for ssq-mux-*.sock file creation/deletion and immediately connects to new sessions without polling delay.

func NewAutoDiscovery

func NewAutoDiscovery() (*AutoDiscovery, error)

NewAutoDiscovery creates a new auto-discovery service with filesystem watching.

func NewAutoDiscoveryWithFallback

func NewAutoDiscoveryWithFallback() *AutoDiscovery

NewAutoDiscoveryWithFallback creates an auto-discovery service that falls back to polling if filesystem watching fails.

func (*AutoDiscovery) IsUsingFallback

func (ad *AutoDiscovery) IsUsingFallback() bool

IsUsingFallback returns true if auto-discovery is using polling fallback.

func (*AutoDiscovery) Start

func (ad *AutoDiscovery) Start(ctx context.Context) (<-chan struct{}, error)

Start begins filesystem watching or polling based on availability. Returns a done channel that closes when the auto-discovery stops.

func (*AutoDiscovery) Stop

func (ad *AutoDiscovery) Stop() error

Stop gracefully stops the auto-discovery service.

func (*AutoDiscovery) WatcherActive

func (ad *AutoDiscovery) WatcherActive() bool

WatcherActive returns true if filesystem watching is active (not in fallback mode).

type DiscoveredSession

type DiscoveredSession struct {
	SocketPath string
	Metadata   *SessionMetadata
	LastSeen   time.Time
}

DiscoveredSession represents a discovered ssq-mux session.

func ScanByUserOptions

func ScanByUserOptions() ([]*DiscoveredSession, error)

ScanByUserOptions discovers active ssq-mux sessions via a single `tmux list-sessions` call, reading user options written by WriteSessionUserOptions.

This is faster than probing N sockets because:

  • One subprocess call returns all sessions
  • No network I/O (socket connect/read) per session
  • Works immediately after a server restart before sockets are re-probed

Returns an empty slice (not an error) when tmux is not running or has no sessions. Sessions without @cs_socket_path set are silently skipped (not ssq-mux sessions).

type Discovery

type Discovery struct {
	// contains filtered or unexported fields
}

Discovery scans for and tracks ssq-mux sessions.

func NewDiscovery

func NewDiscovery() *Discovery

NewDiscovery creates a new session discovery service.

func (*Discovery) GetClaudeSessions

func (d *Discovery) GetClaudeSessions() []*DiscoveredSession

GetClaudeSessions returns only sessions running Claude.

func (*Discovery) GetSessions

func (d *Discovery) GetSessions() []*DiscoveredSession

GetSessions returns all currently known sessions.

func (*Discovery) OnSessionChange

func (d *Discovery) OnSessionChange(callback func(*DiscoveredSession, bool))

OnSessionChange registers a callback for session discovery/removal events. The callback receives the session and a boolean indicating if it's new (true) or removed (false).

func (*Discovery) Scan

func (d *Discovery) Scan() ([]*DiscoveredSession, error)

Scan searches for active ssq-mux sockets and returns discovered sessions.

func (*Discovery) ScanFromUserOptions

func (d *Discovery) ScanFromUserOptions() ([]*DiscoveredSession, error)

ScanFromUserOptions performs a fast discovery pass using tmux user options written by WriteSessionUserOptions. Unlike Scan(), this issues a single `tmux list-sessions` call instead of probing N sockets, making it ideal for initial discovery on server startup or after a restart.

Results are merged into d.sessions and callbacks are fired for new sessions, matching the behaviour of Scan(). Existing sessions are not removed — call Scan() afterward to reconcile against the live socket set if needed.

func (*Discovery) StartPolling

func (d *Discovery) StartPolling(ctx context.Context, interval time.Duration) <-chan struct{}

StartPolling starts periodic scanning for sessions. Returns a channel that will be closed when polling stops.

type HookCommand

type HookCommand struct {
	// Type is "command" or "http"
	Type string `json:"type"`
	// Command is the shell command to execute (command hooks only)
	Command string `json:"command,omitempty"`
	// URL is the HTTP endpoint to POST to (http hooks only)
	URL string `json:"url,omitempty"`
	// Headers are optional HTTP headers (http hooks only)
	Headers map[string]string `json:"headers,omitempty"`
	// Timeout in seconds for http hooks, milliseconds for command hooks (optional)
	Timeout int `json:"timeout,omitempty"`
}

HookCommand defines a command to execute when a hook triggers. Supports both "command" hooks (shell execution) and "http" hooks (HTTP POST).

type HookDefinition

type HookDefinition struct {
	// Matcher specifies when this hook should trigger
	Matcher HookMatcher `json:"matcher"`
	// Hooks contains the actual hook commands
	Hooks []HookCommand `json:"hooks"`
}

HookDefinition defines a single hook in the hooks configuration.

type HookMatcher

type HookMatcher struct {
	// Event is the hook event type: "Notification", "Stop", "PermissionRequest", "PostToolUse"
	Event string `json:"event"`
}

HookMatcher specifies the conditions for triggering a hook.

type HooksConfig

type HooksConfig struct {
	Hooks []HookDefinition `json:"hooks"`
}

HooksConfig represents the Claude Code hooks configuration file format. When CLAUDE_CODE_HOOKS_PATH is set, Claude Code reads hooks from this file.

type HooksMetadata

type HooksMetadata struct {
	// SocketPath is the path to the mux Unix domain socket
	SocketPath string
	// TmuxSession is the tmux session name for this mux instance
	TmuxSession string
	// PID is the process ID of the ssq-mux wrapper
	PID int
	// Cwd is the current working directory
	Cwd string
	// Command is the command being run (typically "claude")
	Command string
}

HooksMetadata contains context to be injected into hook environment variables.

type Message

type Message struct {
	Type MessageType
	Data []byte
}

Message represents a single message in the mux protocol. Wire format: [1 byte: type] [4 bytes: length (big-endian)] [N bytes: data]

func DecodeMessage

func DecodeMessage(r io.Reader) (*Message, error)

DecodeMessage reads and decodes a message from a reader.

func NewCloseMessage

func NewCloseMessage() *Message

NewCloseMessage creates a close message to signal session end.

func NewInputMessage

func NewInputMessage(data []byte) *Message

NewInputMessage creates an input message to send to the PTY.

func NewMetadataMessage

func NewMetadataMessage(meta *SessionMetadata) (*Message, error)

NewMetadataMessage creates a metadata message with session information.

func NewOutputMessage

func NewOutputMessage(data []byte) *Message

NewOutputMessage creates an output message from terminal data.

func NewPingMessage

func NewPingMessage() *Message

NewPingMessage creates a ping keepalive message.

func NewPongMessage

func NewPongMessage() *Message

NewPongMessage creates a pong keepalive response.

func NewResizeMessage

func NewResizeMessage(cols, rows uint16) *Message

NewResizeMessage creates a resize message with terminal dimensions.

func NewSnapshotReplyMessage

func NewSnapshotReplyMessage(content []byte) *Message

NewSnapshotReplyMessage creates a snapshot reply with the captured content.

func NewSnapshotRequestMessage

func NewSnapshotRequestMessage() *Message

NewSnapshotRequestMessage creates a snapshot request message. This requests a clean screen capture (tmux capture-pane) from the multiplexer.

type MessageType

type MessageType byte

MessageType represents the type of message in the mux protocol.

const (
	// MessageTypeOutput is terminal output from the PTY (claude -> clients)
	MessageTypeOutput MessageType = 0x01
	// MessageTypeInput is terminal input to the PTY (clients -> claude)
	MessageTypeInput MessageType = 0x02
	// MessageTypeResize is a terminal resize event (SIGWINCH)
	MessageTypeResize MessageType = 0x03
	// MessageTypeMetadata is session metadata (command, pid, cwd, env)
	MessageTypeMetadata MessageType = 0x04
	// MessageTypePing is a keepalive ping
	MessageTypePing MessageType = 0x05
	// MessageTypePong is a keepalive pong response
	MessageTypePong MessageType = 0x06
	// MessageTypeClose signals graceful session close
	MessageTypeClose MessageType = 0x07
	// MessageTypeSnapshot requests a clean screen snapshot (capture-pane)
	MessageTypeSnapshot MessageType = 0x08
	// MessageTypeSnapshotReply contains the clean screen snapshot
	MessageTypeSnapshotReply MessageType = 0x09
)

type Multiplexer

type Multiplexer struct {
	// contains filtered or unexported fields
}

Multiplexer handles PTY multiplexing for external Claude sessions. It creates a tmux session with stapler-squad's naming convention, allowing stapler-squad to discover and control the session (including killing it). Multiple clients can connect via Unix domain socket for bidirectional terminal access.

func NewMultiplexer

func NewMultiplexer(command string, args []string) *Multiplexer

NewMultiplexer creates a new PTY multiplexer for the given command.

func NewMultiplexerAttach

func NewMultiplexerAttach(tmuxSession string) *Multiplexer

NewMultiplexerAttach creates a multiplexer that attaches to an existing tmux session instead of creating a new one. This is useful for reconnecting to orphaned sessions after a restart.

func NewMultiplexerWithName

func NewMultiplexerWithName(command string, args []string, sessionName string) *Multiplexer

NewMultiplexerWithName creates a new PTY multiplexer with a custom session name.

func (*Multiplexer) CapturePane

func (m *Multiplexer) CapturePane() ([]byte, error)

CapturePane captures clean terminal content from the tmux session. This provides a snapshot without ANSI escape sequences for reliable pattern matching.

func (*Multiplexer) SetWindowSize

func (m *Multiplexer) SetWindowSize(cols, rows uint16) error

SetWindowSize sets the PTY window size.

func (*Multiplexer) Shutdown

func (m *Multiplexer) Shutdown()

Shutdown gracefully shuts down the multiplexer.

func (*Multiplexer) SocketPath

func (m *Multiplexer) SocketPath() string

SocketPath returns the path to the Unix domain socket.

func (*Multiplexer) Start

func (m *Multiplexer) Start() error

Start launches the command in a tmux session and starts the socket server. The tmux session uses stapler-squad's naming convention (staplersquad_ext_<PID>) so stapler-squad can discover and control it.

func (*Multiplexer) TmuxSessionName

func (m *Multiplexer) TmuxSessionName() string

TmuxSessionName returns the tmux session name for this multiplexer. This allows stapler-squad to adopt and control the session.

func (*Multiplexer) Wait

func (m *Multiplexer) Wait() (int, error)

Wait waits for the multiplexer to finish and returns the exit code.

type MultiplexerOption added in v1.18.0

type MultiplexerOption func(*Multiplexer)

MultiplexerOption is a functional option for configuring a Multiplexer.

func WithPaneExitSubscriber added in v1.18.0

func WithPaneExitSubscriber(s tmux.PaneExitSubscriber) MultiplexerOption

WithPaneExitSubscriber injects a PaneExitSubscriber for test isolation. When set, startSessionMonitor uses the channel-based path instead of polling.

type RegistryEntry

type RegistryEntry struct {
	SocketPath  string    `json:"socket_path"`
	SessionName string    `json:"session_name"`
	LastSeen    time.Time `json:"last_seen"`
}

RegistryEntry holds the metadata for one socket in the registry.

type ResizeData

type ResizeData struct {
	Cols uint16 `json:"cols"`
	Rows uint16 `json:"rows"`
}

ResizeData represents terminal resize dimensions.

func ParseResizeMessage

func ParseResizeMessage(msg *Message) (*ResizeData, error)

ParseResizeMessage extracts dimensions from a resize message.

type SessionInfo

type SessionInfo struct {
	Name         string
	CreatedAt    time.Time
	LastActivity time.Time
	Path         string
	Windows      int
	Attached     bool
}

SessionInfo contains metadata about a tmux session

func ListStaplerSquadSessionsWithInfo

func ListStaplerSquadSessionsWithInfo() ([]SessionInfo, error)

ListStaplerSquadSessionsWithInfo returns sessions with full metadata.

type SessionMetadata

type SessionMetadata struct {
	Command     string            `json:"command"`      // The command being run (e.g., "claude")
	Args        []string          `json:"args"`         // Command arguments
	PID         int               `json:"pid"`          // Process ID of the child
	Cwd         string            `json:"cwd"`          // Current working directory
	Env         map[string]string `json:"env"`          // Selected environment variables
	SocketPath  string            `json:"socket_path"`  // Path to the Unix socket
	StartTime   int64             `json:"start_time"`   // Unix timestamp when session started
	TmuxSession string            `json:"tmux_session"` // Tmux session name (for stapler-squad adoption)
}

SessionMetadata contains information about the multiplexed session.

func ParseMetadataMessage

func ParseMetadataMessage(msg *Message) (*SessionMetadata, error)

ParseMetadataMessage extracts session metadata from a message.

type SocketRegistry

type SocketRegistry struct {
	// contains filtered or unexported fields
}

SocketRegistry is a file-backed map of session title → socket path for fast reconnection after a restart. It is safe for concurrent use.

The backing file is located at {configDir}/mux-registry.json.

func NewSocketRegistry

func NewSocketRegistry(configDir string) *SocketRegistry

NewSocketRegistry creates (but does not load) a SocketRegistry backed by {configDir}/mux-registry.json.

func (*SocketRegistry) Delete

func (r *SocketRegistry) Delete(title string)

Delete removes an entry and immediately persists the registry.

func (*SocketRegistry) Get

func (r *SocketRegistry) Get(title string) (*RegistryEntry, bool)

Get returns the entry for the given title.

func (*SocketRegistry) Load

func (r *SocketRegistry) Load() error

Load reads the registry from disk. If the file does not exist, the registry is left empty (not an error).

func (*SocketRegistry) PruneStale

func (r *SocketRegistry) PruneStale(maxAge time.Duration)

PruneStale removes entries whose LastSeen is older than maxAge AND whose socket file no longer exists on disk. Persists after pruning.

func (*SocketRegistry) Save

func (r *SocketRegistry) Save() error

Save writes the current in-memory registry to disk atomically.

func (*SocketRegistry) Set

func (r *SocketRegistry) Set(title string, entry RegistryEntry)

Set stores an entry and immediately persists the registry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL