package claudecode import ( "bufio" "context" "encoding/json" "fmt" "io" "sync" "time" "github.com/google/uuid" "github.com/kandev/kandev/internal/common/logger" "go.uber.org/zap" ) // RequestHandler handles incoming control requests from Claude Code CLI. // It receives the request ID and control request, and should call SendControlResponse. type RequestHandler func(requestID string, req *ControlRequest) // CancelHandler handles control_cancel_request messages that cancel a pending request. type CancelHandler func(requestID string) // MessageHandler handles streaming messages from Claude Code CLI. type MessageHandler func(msg *CLIMessage) // pendingRequest tracks a control request waiting for a response. type pendingRequest struct { ch chan *IncomingControlResponse } // Client handles Claude Code CLI communication over stdin/stdout streams. // It reads streaming JSON from stdout and writes control messages to stdin. type Client struct { stdin io.Writer stdout io.Reader logger *logger.Logger // Handlers for incoming messages requestHandler RequestHandler cancelHandler CancelHandler messageHandler MessageHandler closeHandler func() // Called when readLoop exits (EOF or error) // Pending control requests (requests we sent, waiting for responses) pendingRequests map[string]*pendingRequest pendingRequestsMu sync.Mutex // Synchronization mu sync.RWMutex done chan struct{} } // NewClient creates a new Claude Code CLI client. func NewClient(stdin io.Writer, stdout io.Reader, log *logger.Logger) *Client { return &Client{ stdin: stdin, stdout: stdout, logger: log.WithFields(zap.String("component", "claudecode-client")), done: make(chan struct{}), pendingRequests: make(map[string]*pendingRequest), } } // SetRequestHandler sets the handler for incoming control requests. func (c *Client) SetRequestHandler(handler RequestHandler) { c.mu.Lock() defer c.mu.Unlock() c.requestHandler = handler } // SetCancelHandler sets the handler for control_cancel_request messages. func (c *Client) SetCancelHandler(handler CancelHandler) { c.mu.Lock() defer c.mu.Unlock() c.cancelHandler = handler } // SetMessageHandler sets the handler for streaming messages. func (c *Client) SetMessageHandler(handler MessageHandler) { c.mu.Lock() defer c.mu.Unlock() c.messageHandler = handler } // SetCloseHandler sets a handler called when the read loop exits (EOF or error). func (c *Client) SetCloseHandler(handler func()) { c.mu.Lock() defer c.mu.Unlock() c.closeHandler = handler } // Start begins reading from stdout in a goroutine. // Returns a channel that is closed when the read loop is ready. func (c *Client) Start(ctx context.Context) <-chan struct{} { ready := make(chan struct{}) go c.readLoop(ctx, ready) return ready } // Stop stops the client and closes the done channel. func (c *Client) Stop() { c.mu.Lock() defer c.mu.Unlock() select { case <-c.done: // Already closed default: close(c.done) } } // Initialize sends the initialize control request to Claude Code CLI and waits for the response. // This must be called in streaming mode (input-format=stream-json) to get slash commands. // hooks is optional — pass nil for no hooks. func (c *Client) Initialize(ctx context.Context, timeout time.Duration, hooks map[string]any) (*InitializeResponseData, error) { requestID := uuid.New().String() // Create pending request channel pending := &pendingRequest{ ch: make(chan *IncomingControlResponse, 1), } c.pendingRequestsMu.Lock() c.pendingRequests[requestID] = pending c.pendingRequestsMu.Unlock() defer func() { c.pendingRequestsMu.Lock() delete(c.pendingRequests, requestID) c.pendingRequestsMu.Unlock() }() // Send initialize control request req := &SDKControlRequest{ Type: MessageTypeControlRequest, RequestID: requestID, Request: SDKControlRequestBody{ Subtype: SubtypeInitialize, Hooks: hooks, }, } c.logger.Info("sending initialize control request", zap.String("request_id", requestID)) if err := c.send(req); err != nil { return nil, fmt.Errorf("failed to send initialize request: %w", err) } // Wait for response with timeout c.logger.Info("waiting for initialize response", zap.Duration("timeout", timeout)) select { case <-ctx.Done(): c.logger.Warn("initialize cancelled by context") return nil, ctx.Err() case <-time.After(timeout): c.logger.Warn("initialize request timed out", zap.Duration("timeout", timeout)) return nil, fmt.Errorf("initialize request timed out after %v", timeout) case resp := <-pending.ch: if resp.Subtype == "error" { return nil, fmt.Errorf("initialize failed: %s", resp.Error) } c.logger.Info("initialize response received", zap.Int("commands", len(resp.Response.Commands)), zap.Int("agents", len(resp.Response.Agents))) return resp.Response, nil } } // SendControlRequest sends a control request to Claude Code CLI. func (c *Client) SendControlRequest(req *SDKControlRequest) error { return c.send(req) } // SendControlResponse sends a control response to Claude Code CLI. func (c *Client) SendControlResponse(resp *ControlResponseMessage) error { return c.send(resp) } // SendUserMessage sends a user message (prompt) to Claude Code CLI. func (c *Client) SendUserMessage(content string) error { msg := &UserMessage{ Type: MessageTypeUser, Message: UserMessageBody{ Role: "user", Content: content, }, } return c.send(msg) } func (c *Client) send(msg any) error { data, err := json.Marshal(msg) if err != nil { return fmt.Errorf("failed to marshal message: %w", err) } data = append(data, '\n') _, err = c.stdin.Write(data) if err != nil { return fmt.Errorf("failed to write message: %w", err) } c.logger.Debug("claudecode: sent message", zap.String("data", string(data))) return nil } func (c *Client) readLoop(ctx context.Context, ready chan<- struct{}) { scanner := bufio.NewScanner(c.stdout) // Allow for large JSON messages (up to 10MB) buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 10*1024*1024) // Signal that we're ready to read c.logger.Info("claudecode: read loop starting") close(ready) for scanner.Scan() { select { case <-ctx.Done(): return case <-c.done: return default: } line := scanner.Bytes() if len(line) == 0 { continue } c.handleLine(line) } if err := scanner.Err(); err != nil { c.logger.Error("read loop error", zap.Error(err)) } // Notify close handler (e.g., to unblock Prompt on unexpected EOF) c.mu.RLock() handler := c.closeHandler c.mu.RUnlock() if handler != nil { handler() } } func (c *Client) handleLine(line []byte) { // Log all incoming lines for debugging c.logger.Debug("claudecode: received raw line", zap.String("line", string(line))) // First, parse the basic structure to determine message type var msg CLIMessage if err := json.Unmarshal(line, &msg); err != nil { c.logger.Warn("failed to parse message", zap.Error(err), zap.String("line", string(line))) return } c.logger.Debug("claudecode: parsed message", zap.String("type", msg.Type), zap.String("request_id", msg.RequestID), zap.Bool("has_response", msg.Response != nil)) // Handle control requests (from Claude to us, e.g., permission requests) if msg.Type == MessageTypeControlRequest && msg.Request != nil { c.handleControlRequest(msg.RequestID, msg.Request) return } // Handle control_cancel_request (cancels a pending permission/hook request) if msg.Type == MessageTypeControlCancelRequest && msg.RequestID != "" { c.mu.RLock() handler := c.cancelHandler c.mu.RUnlock() if handler != nil { handler(msg.RequestID) } else { c.logger.Warn("received control_cancel_request but no cancel handler registered", zap.String("request_id", msg.RequestID)) } return } // Handle control responses (from Claude back to us, e.g., initialize response) // Note: request_id is inside the response object, not at the message level if msg.Type == MessageTypeControlResponse && msg.Response != nil { c.handleControlResponse(msg.Response) return } // Forward other messages to the message handler c.mu.RLock() handler := c.messageHandler c.mu.RUnlock() if handler != nil { // Store raw line for advanced parsing if needed msg.RawContent = line handler(&msg) } } func (c *Client) handleControlRequest(requestID string, req *ControlRequest) { c.mu.RLock() handler := c.requestHandler c.mu.RUnlock() if handler != nil { handler(requestID, req) } else { c.logger.Warn("received control request but no handler registered", zap.String("request_id", requestID), zap.String("subtype", req.Subtype)) // Auto-deny if no handler if err := c.SendControlResponse(&ControlResponseMessage{ Type: MessageTypeControlResponse, RequestID: requestID, Response: &ControlResponse{ Subtype: "error", Error: "no handler registered", }, }); err != nil { c.logger.Warn("failed to send error response", zap.Error(err)) } } } func (c *Client) handleControlResponse(resp *IncomingControlResponse) { requestID := resp.RequestID c.pendingRequestsMu.Lock() pending, ok := c.pendingRequests[requestID] c.pendingRequestsMu.Unlock() if !ok { c.logger.Warn("received control response for unknown request", zap.String("request_id", requestID), zap.String("subtype", resp.Subtype)) return } c.logger.Info("received control response", zap.String("request_id", requestID), zap.String("subtype", resp.Subtype)) // Send response to waiting goroutine select { case pending.ch <- resp: default: c.logger.Warn("pending request channel full", zap.String("request_id", requestID)) } }