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 ¶
- func CleanAllStaleSockets() error
- func CleanStaleSocket(socketPath string) error
- func CleanupHooksFile(path string) error
- func CleanupStaleHooksFiles() error
- func EncodeMessage(msg *Message) ([]byte, error)
- func GenerateHooksFile(meta *HooksMetadata) (string, error)
- func GenerateSessionName(command string) string
- func InteractiveSessionPicker() (string, error)
- func ListStaplerSquadSessions() ([]string, error)
- func Run(command string, args []string) (int, error)
- func RunAttach(tmuxSession string) (int, error)
- func RunWithName(command string, args []string, sessionName string) (int, error)
- func WriteMessage(w io.Writer, msg *Message) error
- func WriteSessionUserOptions(sessionName, socketPath, cwd, command string, pid int, startTime int64) error
- type AutoDiscovery
- type DiscoveredSession
- type Discovery
- func (d *Discovery) GetClaudeSessions() []*DiscoveredSession
- func (d *Discovery) GetSessions() []*DiscoveredSession
- func (d *Discovery) OnSessionChange(callback func(*DiscoveredSession, bool))
- func (d *Discovery) Scan() ([]*DiscoveredSession, error)
- func (d *Discovery) ScanFromUserOptions() ([]*DiscoveredSession, error)
- func (d *Discovery) StartPolling(ctx context.Context, interval time.Duration) <-chan struct{}
- type HookCommand
- type HookDefinition
- type HookMatcher
- type HooksConfig
- type HooksMetadata
- type Message
- func DecodeMessage(r io.Reader) (*Message, error)
- func NewCloseMessage() *Message
- func NewInputMessage(data []byte) *Message
- func NewMetadataMessage(meta *SessionMetadata) (*Message, error)
- func NewOutputMessage(data []byte) *Message
- func NewPingMessage() *Message
- func NewPongMessage() *Message
- func NewResizeMessage(cols, rows uint16) *Message
- func NewSnapshotReplyMessage(content []byte) *Message
- func NewSnapshotRequestMessage() *Message
- type MessageType
- type Multiplexer
- func (m *Multiplexer) CapturePane() ([]byte, error)
- func (m *Multiplexer) SetWindowSize(cols, rows uint16) error
- func (m *Multiplexer) Shutdown()
- func (m *Multiplexer) SocketPath() string
- func (m *Multiplexer) Start() error
- func (m *Multiplexer) TmuxSessionName() string
- func (m *Multiplexer) Wait() (int, error)
- type MultiplexerOption
- type RegistryEntry
- type ResizeData
- type SessionInfo
- type SessionMetadata
- type SocketRegistry
- func (r *SocketRegistry) Delete(title string)
- func (r *SocketRegistry) Get(title string) (*RegistryEntry, bool)
- func (r *SocketRegistry) Load() error
- func (r *SocketRegistry) PruneStale(maxAge time.Duration)
- func (r *SocketRegistry) Save() error
- func (r *SocketRegistry) Set(title string, entry RegistryEntry)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CleanAllStaleSockets ¶
func CleanAllStaleSockets() error
CleanAllStaleSockets removes all stale claude-mux sockets.
func CleanStaleSocket ¶
CleanStaleSocket removes a socket file if it's no longer connected to a running process.
func CleanupHooksFile ¶
CleanupHooksFile removes the generated hooks configuration file. This should be called when claude-mux exits.
func CleanupStaleHooksFiles ¶
func CleanupStaleHooksFiles() error
CleanupStaleHooksFiles removes any hooks files left behind by crashed claude-mux instances. This should be called on startup to clean up orphaned files.
func EncodeMessage ¶
EncodeMessage encodes a message to wire format.
func GenerateHooksFile ¶
func GenerateHooksFile(meta *HooksMetadata) (string, error)
GenerateHooksFile creates a temporary hooks configuration file for claude-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 ¶
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 ¶
InteractiveSessionPicker displays an interactive picker for session selection
func ListStaplerSquadSessions ¶
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 RunAttach ¶
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 ¶
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 ¶
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 claude-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 claude-mux session.
func ScanByUserOptions ¶
func ScanByUserOptions() ([]*DiscoveredSession, error)
ScanByUserOptions discovers active claude-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 claude-mux sessions).
type Discovery ¶
type Discovery struct {
// contains filtered or unexported fields
}
Discovery scans for and tracks claude-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 claude-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.
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 claude-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 ¶
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 ¶
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 ¶
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 ¶
NewResizeMessage creates a resize message with terminal dimensions.
func NewSnapshotReplyMessage ¶
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 ¶
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.