package main import ( "bufio" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net" "os" "path/filepath" "strings" "time" ) type relayAuthState struct { RelayID string `json:"relay_id"` RelayToken string `json:"relay_token"` } // protocolVersion indicates whether a command uses the v1 text or v2 JSON-RPC protocol. type protocolVersion int const ( protoV1 protocolVersion = iota protoV2 ) // commandSpec describes a single CLI command and how to relay it. type commandSpec struct { name string // CLI command name (e.g. "ping", "new-window") proto protocolVersion // v1 text or v2 JSON-RPC v1Cmd string // v1: literal command string sent over the socket v2Method string // v2: JSON-RPC method name // flagKeys lists parameter keys this command accepts. // They are extracted from --key flags and added to params. flagKeys []string // noParams means the command takes no parameters at all. noParams bool // paramKeyOverrides remaps specific flags for compatibility aliases. paramKeyOverrides map[string]string // defaultParams are applied before flags/env fallbacks. defaultParams map[string]any } var commands = []commandSpec{ // V1 text protocol commands {name: "ping", proto: protoV1, v1Cmd: "ping", noParams: true}, {name: "new-window", proto: protoV1, v1Cmd: "new_window", noParams: true}, {name: "current-window", proto: protoV1, v1Cmd: "current_window", noParams: true}, {name: "close-window", proto: protoV1, v1Cmd: "close_window", flagKeys: []string{"window"}}, {name: "focus-window", proto: protoV1, v1Cmd: "focus_window", flagKeys: []string{"window"}}, {name: "list-windows", proto: protoV1, v1Cmd: "list_windows", noParams: true}, // V2 JSON-RPC commands {name: "capabilities", proto: protoV2, v2Method: "system.capabilities", noParams: true}, {name: "list-workspaces", proto: protoV2, v2Method: "workspace.list", noParams: true}, {name: "new-workspace", proto: protoV2, v2Method: "workspace.create", flagKeys: []string{"command", "working-directory", "name"}}, {name: "close-workspace", proto: protoV2, v2Method: "workspace.close", flagKeys: []string{"workspace"}}, {name: "select-workspace", proto: protoV2, v2Method: "workspace.select", flagKeys: []string{"workspace"}}, {name: "current-workspace", proto: protoV2, v2Method: "workspace.current", noParams: true}, {name: "list-panels", proto: protoV2, v2Method: "surface.list", flagKeys: []string{"workspace"}}, {name: "focus-panel", proto: protoV2, v2Method: "surface.focus", flagKeys: []string{"panel", "workspace"}, paramKeyOverrides: map[string]string{"panel": "surface_id"}}, {name: "list-panes", proto: protoV2, v2Method: "pane.list", flagKeys: []string{"workspace"}}, {name: "list-pane-surfaces", proto: protoV2, v2Method: "pane.surfaces", flagKeys: []string{"pane"}}, {name: "new-pane", proto: protoV2, v2Method: "pane.create", flagKeys: []string{"workspace", "direction", "type", "url"}, defaultParams: map[string]any{"direction": "right"}}, {name: "new-surface", proto: protoV2, v2Method: "surface.create", flagKeys: []string{"workspace", "pane", "type", "url"}}, {name: "new-split", proto: protoV2, v2Method: "surface.split", flagKeys: []string{"surface", "direction"}}, {name: "close-surface", proto: protoV2, v2Method: "surface.close", flagKeys: []string{"surface"}}, {name: "send", proto: protoV2, v2Method: "surface.send_text", flagKeys: []string{"surface", "text"}}, {name: "send-key", proto: protoV2, v2Method: "surface.send_key", flagKeys: []string{"surface", "key"}}, {name: "notify", proto: protoV2, v2Method: "notification.create", flagKeys: []string{"title", "body", "workspace"}}, {name: "refresh-surfaces", proto: protoV2, v2Method: "surface.refresh", noParams: true}, } var commandIndex map[string]*commandSpec func init() { commandIndex = make(map[string]*commandSpec, len(commands)) for i := range commands { commandIndex[commands[i].name] = &commands[i] } } // runCLI is the entry point for the "cli" subcommand (or busybox "cmux" invocation). func runCLI(args []string) int { socketPath := os.Getenv("CMUX_SOCKET_PATH") // Parse global flags var jsonOutput bool var remaining []string for i := 0; i < len(args); i++ { switch args[i] { case "--socket": if i+1 >= len(args) { fmt.Fprintln(os.Stderr, "cmux: --socket requires a path") return 2 } socketPath = args[i+1] i++ case "--json": jsonOutput = true case "--help", "-h": cliUsage() return 0 default: remaining = append(remaining, args[i:]...) goto doneFlags } } doneFlags: if len(remaining) == 0 { cliUsage() return 2 } cmdName := remaining[0] cmdArgs := remaining[1:] if cmdName == "help" { cliUsage() return 0 } // refreshAddr is set when the address came from socket_addr file (not env/flag), // allowing one stale-address refresh if another workspace has replaced socket_addr. var refreshAddr func() string if socketPath == "" { socketPath = readSocketAddrFile() refreshAddr = readSocketAddrFile } if socketPath == "" { fmt.Fprintln(os.Stderr, "cmux: CMUX_SOCKET_PATH not set and --socket not provided") return 1 } // Special case: "rpc" passthrough if cmdName == "rpc" { return runRPC(socketPath, cmdArgs, jsonOutput, refreshAddr) } // Browser subcommand delegation if cmdName == "browser" { return runBrowserRelay(socketPath, cmdArgs, jsonOutput, refreshAddr) } // Agent launch commands if cmdName == "claude-teams" { return runClaudeTeamsRelay(socketPath, cmdArgs, refreshAddr) } if cmdName == "omo" { return runOMORelay(socketPath, cmdArgs, refreshAddr) } // Tmux compatibility layer (used by agent shims) if cmdName == "__tmux-compat" { return runTmuxCompat(socketPath, cmdArgs, refreshAddr) } spec, ok := commandIndex[cmdName] if !ok { fmt.Fprintf(os.Stderr, "cmux: unknown command %q\n", cmdName) return 2 } switch spec.proto { case protoV1: return execV1(socketPath, spec, cmdArgs, refreshAddr) case protoV2: return execV2(socketPath, spec, cmdArgs, jsonOutput, refreshAddr) default: fmt.Fprintf(os.Stderr, "cmux: internal error: unknown protocol for %q\n", cmdName) return 1 } } // execV1 sends a v1 text command over the socket. func execV1(socketPath string, spec *commandSpec, args []string, refreshAddr func() string) int { cmd := spec.v1Cmd if !spec.noParams { parsed, err := parseFlags(args, spec.flagKeys) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 2 } for _, key := range spec.flagKeys { if val, ok := parsed.flags[key]; ok { cmd += " " + val } } } resp, err := socketRoundTrip(socketPath, cmd, refreshAddr) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 1 } fmt.Print(resp) if !strings.HasSuffix(resp, "\n") { fmt.Println() } return 0 } // execV2 sends a v2 JSON-RPC request over the socket. func execV2(socketPath string, spec *commandSpec, args []string, jsonOutput bool, refreshAddr func() string) int { params := make(map[string]any, len(spec.defaultParams)) for key, value := range spec.defaultParams { params[key] = value } if !spec.noParams { parsed, err := parseFlags(args, spec.flagKeys) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 2 } // Map flag keys to JSON param keys (e.g. "workspace" → "workspace_id" where appropriate) for _, key := range spec.flagKeys { if val, ok := parsed.flags[key]; ok { paramKey := flagToParamKey(key) if override, ok := spec.paramKeyOverrides[key]; ok { paramKey = override } params[paramKey] = val } } // First positional arg is used as initial_command if --command wasn't given if _, ok := params["initial_command"]; !ok && len(parsed.positional) > 0 { params["initial_command"] = parsed.positional[0] } applyWorkspaceEnvFallback(params) applySurfaceEnvFallback(params) } resp, err := socketRoundTripV2(socketPath, spec.v2Method, params, refreshAddr) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 1 } if jsonOutput { fmt.Println(resp) } else { fmt.Println(defaultRelayOutput(resp)) } return 0 } // runRPC sends an arbitrary JSON-RPC method with optional JSON params. func runRPC(socketPath string, args []string, jsonOutput bool, refreshAddr func() string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "cmux rpc: requires a method name") return 2 } method := args[0] var params map[string]any if len(args) > 1 { if err := json.Unmarshal([]byte(args[1]), ¶ms); err != nil { fmt.Fprintf(os.Stderr, "cmux rpc: invalid JSON params: %v\n", err) return 2 } } resp, err := socketRoundTripV2(socketPath, method, params, refreshAddr) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 1 } fmt.Println(resp) return 0 } // runBrowserRelay handles "cmux browser " by mapping to browser.* v2 methods. func runBrowserRelay(socketPath string, args []string, jsonOutput bool, refreshAddr func() string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "cmux browser: requires a subcommand (open, navigate, back, forward, reload, get-url)") return 2 } sub := args[0] subArgs := args[1:] var method string var flagKeys []string var allowPositionalURL bool var useWorkspaceEnv bool var useSurfaceEnv bool switch sub { case "open", "open-split", "new": method = "browser.open_split" flagKeys = []string{"url", "workspace", "surface"} allowPositionalURL = true useWorkspaceEnv = true case "navigate": method = "browser.navigate" flagKeys = []string{"url", "surface"} allowPositionalURL = true useSurfaceEnv = true case "back": method = "browser.back" flagKeys = []string{"surface"} useSurfaceEnv = true case "forward": method = "browser.forward" flagKeys = []string{"surface"} useSurfaceEnv = true case "reload": method = "browser.reload" flagKeys = []string{"surface"} useSurfaceEnv = true case "get-url": method = "browser.url.get" flagKeys = []string{"surface"} useSurfaceEnv = true default: fmt.Fprintf(os.Stderr, "cmux browser: unknown subcommand %q\n", sub) return 2 } params := make(map[string]any) parsed, err := parseFlags(subArgs, flagKeys) if err != nil { fmt.Fprintf(os.Stderr, "cmux browser: %v\n", err) return 2 } for _, key := range flagKeys { if val, ok := parsed.flags[key]; ok { paramKey := flagToParamKey(key) params[paramKey] = val } } if allowPositionalURL { if _, ok := params["url"]; !ok && len(parsed.positional) > 0 { params["url"] = strings.Join(parsed.positional, " ") } } if useWorkspaceEnv { applyWorkspaceEnvFallback(params) } if useSurfaceEnv { applySurfaceEnvFallback(params) } resp, err := socketRoundTripV2(socketPath, method, params, refreshAddr) if err != nil { fmt.Fprintf(os.Stderr, "cmux: %v\n", err) return 1 } if jsonOutput { fmt.Println(resp) } else { fmt.Println(defaultRelayOutput(resp)) } return 0 } func applyWorkspaceEnvFallback(params map[string]any) { if _, ok := params["workspace_id"]; ok { return } if envWs := os.Getenv("CMUX_WORKSPACE_ID"); envWs != "" { params["workspace_id"] = envWs } } func applySurfaceEnvFallback(params map[string]any) { if _, ok := params["surface_id"]; ok { return } if envSf := os.Getenv("CMUX_SURFACE_ID"); envSf != "" { params["surface_id"] = envSf } } func defaultRelayOutput(resp string) string { var result any if err := json.Unmarshal([]byte(resp), &result); err != nil { trimmed := strings.TrimSpace(resp) if trimmed == "" { return "OK" } return trimmed } if relayResultIsEmpty(result) { return "OK" } switch typed := result.(type) { case string: return typed default: encoded, err := json.MarshalIndent(typed, "", " ") if err != nil { return "OK" } return string(encoded) } } func relayResultIsEmpty(result any) bool { switch typed := result.(type) { case nil: return true case map[string]any: return len(typed) == 0 case []any: return len(typed) == 0 case string: return typed == "" default: return false } } // flagToParamKey maps a CLI flag name to its JSON-RPC param key. func flagToParamKey(key string) string { switch key { case "workspace": return "workspace_id" case "surface": return "surface_id" case "panel": return "panel_id" case "pane": return "pane_id" case "window": return "window_id" case "command": return "initial_command" case "name": return "title" case "working-directory": return "working_directory" default: return key } } // parsedFlags holds the results of flag parsing. type parsedFlags struct { flags map[string]string // --key value pairs positional []string // non-flag arguments } // parseFlags extracts --key value pairs from args for the given allowed keys. // Non-flag arguments are collected in positional. func parseFlags(args []string, keys []string) (parsedFlags, error) { allowed := make(map[string]bool, len(keys)) for _, k := range keys { allowed[k] = true } result := parsedFlags{flags: make(map[string]string)} for i := 0; i < len(args); i++ { if args[i] == "--" { result.positional = append(result.positional, args[i+1:]...) break } if !strings.HasPrefix(args[i], "--") { result.positional = append(result.positional, args[i]) continue } key := strings.TrimPrefix(args[i], "--") if !allowed[key] { return parsedFlags{}, fmt.Errorf("unknown flag --%s", key) } if i+1 < len(args) { result.flags[key] = args[i+1] i++ } } return result, nil } // readSocketAddrFile reads the socket address from ~/.cmux/socket_addr as a fallback // when CMUX_SOCKET_PATH is not set. Written by the cmux app after the relay establishes. func readSocketAddrFile() string { home, err := os.UserHomeDir() if err != nil { return "" } data, err := os.ReadFile(filepath.Join(home, ".cmux", "socket_addr")) if err != nil { return "" } return strings.TrimSpace(string(data)) } func readRelayAuthFile(socketPath string) *relayAuthState { if strings.Contains(socketPath, ":") && !strings.HasPrefix(socketPath, "/") { _, port, err := net.SplitHostPort(socketPath) if err != nil || port == "" { return nil } home, err := os.UserHomeDir() if err != nil { return nil } data, err := os.ReadFile(filepath.Join(home, ".cmux", "relay", port+".auth")) if err != nil { return nil } var state relayAuthState if err := json.Unmarshal(data, &state); err != nil { return nil } if state.RelayID == "" || state.RelayToken == "" { return nil } return &state } return nil } func currentRelayAuth(socketPath string) *relayAuthState { relayID := strings.TrimSpace(os.Getenv("CMUX_RELAY_ID")) relayToken := strings.TrimSpace(os.Getenv("CMUX_RELAY_TOKEN")) if relayID != "" && relayToken != "" { return &relayAuthState{RelayID: relayID, RelayToken: relayToken} } return readRelayAuthFile(socketPath) } // dialSocket connects to the cmux socket. If addr contains a colon and doesn't // start with '/', it's treated as a TCP address (host:port); otherwise Unix socket. // For TCP connections, refreshAddr is used only to recover from a stale socket_addr // rewrite, not to poll for relay readiness. func dialSocket(addr string, refreshAddr func() string) (net.Conn, error) { if strings.Contains(addr, ":") && !strings.HasPrefix(addr, "/") { conn, connectedAddr, err := dialTCP(addr) if err != nil && refreshAddr != nil && isConnectionRefused(err) { if refreshedAddr := strings.TrimSpace(refreshAddr()); refreshedAddr != "" && refreshedAddr != addr { addr = refreshedAddr conn, connectedAddr, err = dialTCP(addr) } } if err != nil { return nil, err } if auth := currentRelayAuth(connectedAddr); auth != nil { if err := authenticateRelayConn(conn, auth); err != nil { conn.Close() return nil, err } } return conn, nil } return net.Dial("unix", addr) } func dialTCP(addr string) (net.Conn, string, error) { conn, err := net.DialTimeout("tcp", addr, 2*time.Second) if err != nil { return nil, addr, err } setTCPNoDelay(conn) return conn, addr, nil } func isConnectionRefused(err error) bool { if opErr, ok := err.(*net.OpError); ok { return strings.Contains(opErr.Err.Error(), "connection refused") } return strings.Contains(err.Error(), "connection refused") } func authenticateRelayConn(conn net.Conn, auth *relayAuthState) error { reader := bufio.NewReader(conn) _ = conn.SetDeadline(time.Now().Add(5 * time.Second)) var challenge struct { Protocol string `json:"protocol"` Version int `json:"version"` RelayID string `json:"relay_id"` Nonce string `json:"nonce"` } line, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read relay auth challenge: %w", err) } if err := json.Unmarshal([]byte(line), &challenge); err != nil { return fmt.Errorf("invalid relay auth challenge") } if challenge.Protocol != "cmux-relay-auth" || challenge.Version != 1 || challenge.RelayID != auth.RelayID || challenge.Nonce == "" { return fmt.Errorf("relay auth challenge mismatch") } tokenBytes, err := hex.DecodeString(auth.RelayToken) if err != nil { return fmt.Errorf("invalid relay auth token") } mac := computeRelayMAC(tokenBytes, auth.RelayID, challenge.Nonce, challenge.Version) payload, err := json.Marshal(map[string]any{ "relay_id": auth.RelayID, "mac": hex.EncodeToString(mac), }) if err != nil { return fmt.Errorf("failed to encode relay auth response: %w", err) } if _, err := conn.Write(append(payload, '\n')); err != nil { return fmt.Errorf("failed to send relay auth response: %w", err) } line, err = reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read relay auth result: %w", err) } var result struct { OK bool `json:"ok"` } if err := json.Unmarshal([]byte(line), &result); err != nil { return fmt.Errorf("invalid relay auth result") } if !result.OK { return fmt.Errorf("relay auth rejected") } _ = conn.SetDeadline(time.Time{}) return nil } func computeRelayMAC(token []byte, relayID, nonce string, version int) []byte { mac := hmac.New(sha256.New, token) _, _ = io.WriteString(mac, fmt.Sprintf("relay_id=%s\nnonce=%s\nversion=%d", relayID, nonce, version)) return mac.Sum(nil) } // socketRoundTrip sends a raw text line and reads a raw text response (v1). func socketRoundTrip(socketPath, command string, refreshAddr func() string) (string, error) { conn, err := dialSocket(socketPath, refreshAddr) if err != nil { return "", fmt.Errorf("failed to connect to %s: %w", socketPath, err) } defer conn.Close() if _, err := fmt.Fprintf(conn, "%s\n", command); err != nil { return "", fmt.Errorf("failed to send command: %w", err) } // V1 handlers may return multiple lines (e.g. list_windows). Read until // the stream goes idle briefly after seeing at least one newline. reader := bufio.NewReader(conn) var response strings.Builder sawNewline := false for { readTimeout := 15 * time.Second if sawNewline { readTimeout = 120 * time.Millisecond } _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) chunk, err := reader.ReadString('\n') if chunk != "" { response.WriteString(chunk) if strings.Contains(chunk, "\n") { sawNewline = true } } if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if sawNewline { break } return "", fmt.Errorf("failed to read response: timeout waiting for response") } if errors.Is(err, io.EOF) { break } return "", fmt.Errorf("failed to read response: %w", err) } } return strings.TrimRight(response.String(), "\n"), nil } // socketRoundTripV2 sends a JSON-RPC request and returns the result JSON. func socketRoundTripV2(socketPath, method string, params map[string]any, refreshAddr func() string) (string, error) { conn, err := dialSocket(socketPath, refreshAddr) if err != nil { return "", fmt.Errorf("failed to connect to %s: %w", socketPath, err) } defer conn.Close() id := randomHex(8) req := map[string]any{ "id": id, "method": method, } if params != nil { req["params"] = params } else { req["params"] = map[string]any{} } payload, err := json.Marshal(req) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } if _, err := conn.Write(append(payload, '\n')); err != nil { return "", fmt.Errorf("failed to send request: %w", err) } _ = conn.SetReadDeadline(time.Now().Add(15 * time.Second)) reader := bufio.NewReader(conn) line, err := reader.ReadString('\n') if err != nil { return "", fmt.Errorf("failed to read response: %w", err) } // Parse the response to check for errors var resp map[string]any if err := json.Unmarshal([]byte(line), &resp); err != nil { return strings.TrimRight(line, "\n"), nil } if ok, _ := resp["ok"].(bool); !ok { if errObj, _ := resp["error"].(map[string]any); errObj != nil { code, _ := errObj["code"].(string) msg, _ := errObj["message"].(string) return "", fmt.Errorf("server error [%s]: %s", code, msg) } return "", fmt.Errorf("server returned error response") } // Return the result portion as JSON if result, ok := resp["result"]; ok { resultJSON, err := json.Marshal(result) if err != nil { return "", fmt.Errorf("failed to marshal result: %w", err) } return string(resultJSON), nil } return "{}", nil } func randomHex(n int) string { b := make([]byte, n) _, _ = rand.Read(b) return hex.EncodeToString(b) } func cliUsage() { fmt.Fprintln(os.Stderr, "Usage: cmux [--socket ] [--json] [args...]") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Commands:") fmt.Fprintln(os.Stderr, " ping Check connectivity") fmt.Fprintln(os.Stderr, " capabilities List server capabilities") fmt.Fprintln(os.Stderr, " list-workspaces List all workspaces") fmt.Fprintln(os.Stderr, " new-window Create a new window") fmt.Fprintln(os.Stderr, " new-workspace Create a new workspace") fmt.Fprintln(os.Stderr, " new-surface Create a new surface") fmt.Fprintln(os.Stderr, " new-split Split an existing surface") fmt.Fprintln(os.Stderr, " close-surface Close a surface") fmt.Fprintln(os.Stderr, " close-workspace Close a workspace") fmt.Fprintln(os.Stderr, " select-workspace Select a workspace") fmt.Fprintln(os.Stderr, " send Send text to a surface") fmt.Fprintln(os.Stderr, " send-key Send a key to a surface") fmt.Fprintln(os.Stderr, " notify Create a notification") fmt.Fprintln(os.Stderr, " browser Browser commands (open, navigate, back, forward, reload, get-url)") fmt.Fprintln(os.Stderr, " claude-teams [args...] Launch Claude Code in teammate mode") fmt.Fprintln(os.Stderr, " omo [args...] Launch OpenCode with cmux integration") fmt.Fprintln(os.Stderr, " rpc [json-params] Send arbitrary JSON-RPC") }