package cmd import ( "bufio" "encoding/json" "fmt" "os" "strings" "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/sessions" "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) func runClientMode(cfg *config.Config, addr, agentName, message, sessionKey string) { wsURL := fmt.Sprintf("ws://%s/ws", addr) conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { fmt.Fprintf(os.Stderr, "WebSocket connect failed: %v\n", err) os.Exit(1) } defer conn.Close() // Authenticate if err := wsConnect(conn, cfg.Gateway.Token); err != nil { fmt.Fprintf(os.Stderr, "Gateway auth failed: %v\n", err) os.Exit(1) } agentCfg := cfg.ResolveAgent(agentName) if message != "" { // One-shot mode resp, err := wsChatSend(conn, agentName, sessionKey, message) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } fmt.Println(resp) return } // Interactive REPL fmt.Fprintf(os.Stderr, "\nGoClaw Interactive Chat (agent: %s, model: %s)\n", agentName, agentCfg.Model) fmt.Fprintf(os.Stderr, "Session: %s\n", sessionKey) fmt.Fprintf(os.Stderr, "Type \"exit\" to quit, \"/new\" for new session\n\n") scanner := bufio.NewScanner(os.Stdin) for { fmt.Fprint(os.Stderr, "You: ") if !scanner.Scan() { break } input := strings.TrimSpace(scanner.Text()) if input == "" { continue } if input == "exit" || input == "quit" { fmt.Fprintln(os.Stderr, "Goodbye!") return } if input == "/new" { sessionKey = sessions.BuildSessionKey(agentName, "cli", sessions.PeerDirect, uuid.NewString()[:8]) fmt.Fprintf(os.Stderr, "New session: %s\n\n", sessionKey) continue } resp, err := wsChatSend(conn, agentName, sessionKey, input) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) continue } fmt.Printf("\n%s\n\n", resp) } } // wsConnect sends the connect RPC and waits for auth response. func wsConnect(conn *websocket.Conn, token string) error { params := map[string]string{} if token != "" { params["token"] = token } paramsJSON, _ := json.Marshal(params) reqFrame := protocol.RequestFrame{ Type: protocol.FrameTypeRequest, ID: "connect-1", Method: protocol.MethodConnect, Params: paramsJSON, } if err := conn.WriteJSON(reqFrame); err != nil { return fmt.Errorf("send connect: %w", err) } var resp protocol.ResponseFrame if err := conn.ReadJSON(&resp); err != nil { return fmt.Errorf("read connect response: %w", err) } if !resp.OK { if resp.Error != nil { return fmt.Errorf("connect rejected: %s", resp.Error.Message) } return fmt.Errorf("connect rejected") } return nil } // wsChatSend sends a chat.send RPC and waits for the response, // displaying events (tool calls, chunks) in real-time. func wsChatSend(conn *websocket.Conn, agentID, sessionKey, message string) (string, error) { reqID := uuid.NewString()[:8] params, _ := json.Marshal(map[string]any{ "message": message, "agentId": agentID, "sessionKey": sessionKey, "stream": true, }) reqFrame := protocol.RequestFrame{ Type: protocol.FrameTypeRequest, ID: reqID, Method: protocol.MethodChatSend, Params: params, } if err := conn.WriteJSON(reqFrame); err != nil { return "", fmt.Errorf("send chat: %w", err) } // Read frames until we get our response var finalContent string for { _, rawMsg, err := conn.ReadMessage() if err != nil { return "", fmt.Errorf("read: %w", err) } frameType, _ := protocol.ParseFrameType(rawMsg) switch frameType { case protocol.FrameTypeResponse: var resp protocol.ResponseFrame if err := json.Unmarshal(rawMsg, &resp); err != nil { continue } if resp.ID != reqID { continue // response for a different request } if !resp.OK { if resp.Error != nil { return "", fmt.Errorf("agent error: %s", resp.Error.Message) } return "", fmt.Errorf("agent error (unknown)") } // Extract content from payload if payload, ok := resp.Payload.(map[string]any); ok { if content, ok := payload["content"].(string); ok && content != "" { finalContent = content } } return finalContent, nil case protocol.FrameTypeEvent: var evt protocol.EventFrame if err := json.Unmarshal(rawMsg, &evt); err != nil { continue } handleCLIEvent(evt) } } } // handleCLIEvent displays agent events in the terminal. func handleCLIEvent(evt protocol.EventFrame) { payload, ok := evt.Payload.(map[string]any) if !ok { return } evtType, _ := payload["type"].(string) switch evt.Event { case protocol.EventAgent: switch evtType { case protocol.AgentEventToolCall: if p, ok := payload["payload"].(map[string]any); ok { name, _ := p["toolName"].(string) if name == "" { name, _ = p["name"].(string) } fmt.Fprintf(os.Stderr, " [tool] %s\n", name) } case protocol.AgentEventToolResult: if p, ok := payload["payload"].(map[string]any); ok { isErr, _ := p["is_error"].(bool) name, _ := p["toolName"].(string) if name == "" { name, _ = p["name"].(string) } if isErr { fmt.Fprintf(os.Stderr, " [tool] %s -> error\n", name) } } } case protocol.EventChat: switch evtType { case protocol.ChatEventChunk: if content, ok := payload["content"].(string); ok { fmt.Print(content) } } } }