// Package claudecode provides a CodeAgent implementation for Anthropic's Claude Code CLI. package claudecode import ( "encoding/json" "time" "github.com/orchard9/rdev/internal/domain" ) // StreamMessageType identifies the type of stream-json message from Claude Code CLI. type StreamMessageType string const ( // StreamMessageInit is emitted when a session starts. StreamMessageInit StreamMessageType = "init" // StreamMessageMessage contains assistant or user text. StreamMessageMessage StreamMessageType = "message" // StreamMessageToolUse indicates a tool is being invoked. StreamMessageToolUse StreamMessageType = "tool_use" // StreamMessageToolResult contains the output of a tool invocation. StreamMessageToolResult StreamMessageType = "tool_result" // StreamMessageResult is the final message indicating completion. StreamMessageResult StreamMessageType = "result" ) // StreamMessage represents a single NDJSON message from Claude Code's stream-json output. type StreamMessage struct { Type StreamMessageType `json:"type"` Subtype string `json:"subtype,omitempty"` // "success" or "error" (for result type) IsError bool `json:"is_error,omitempty"` // true if result is an error Timestamp string `json:"timestamp,omitempty"` SessionID string `json:"session_id,omitempty"` Role string `json:"role,omitempty"` // "assistant" or "user" Content []ContentBlock `json:"content,omitempty"` Name string `json:"name,omitempty"` // Tool name for tool_use Input json.RawMessage `json:"input,omitempty"` // Tool input for tool_use Output string `json:"output,omitempty"` DurationMs int64 `json:"duration_ms,omitempty"` Error string `json:"error,omitempty"` } // ContentBlock represents a content item within a message. type ContentBlock struct { Type string `json:"type"` // "text" or "tool_use" Text string `json:"text,omitempty"` Name string `json:"name,omitempty"` Input json.RawMessage `json:"input,omitempty"` } // ParseStreamMessage parses a single NDJSON line into a StreamMessage. func ParseStreamMessage(line []byte) (*StreamMessage, error) { var msg StreamMessage if err := json.Unmarshal(line, &msg); err != nil { return nil, err } return &msg, nil } // ToAgentEvent converts a StreamMessage to a domain.AgentEvent. func (m *StreamMessage) ToAgentEvent() domain.AgentEvent { event := domain.AgentEvent{ Timestamp: parseTimestamp(m.Timestamp), Metadata: make(map[string]any), } switch m.Type { case StreamMessageInit: event.Type = domain.AgentEventOutput event.Content = "Session started" if m.SessionID != "" { event.Metadata["session_id"] = m.SessionID } case StreamMessageMessage: event.Type = domain.AgentEventOutput event.Content = extractTextContent(m.Content) if m.Role != "" { event.Metadata["role"] = m.Role } case StreamMessageToolUse: event.Type = domain.AgentEventToolUse event.ToolName = m.Name if len(m.Name) == 0 { // Check content blocks for tool_use for _, block := range m.Content { if block.Type == "tool_use" { event.ToolName = block.Name if len(block.Input) > 0 { var input map[string]any if err := json.Unmarshal(block.Input, &input); err == nil { event.ToolInput = input } } break } } } else if len(m.Input) > 0 { var input map[string]any if err := json.Unmarshal(m.Input, &input); err == nil { event.ToolInput = input } } event.Content = event.ToolName case StreamMessageToolResult: event.Type = domain.AgentEventToolResult event.Content = m.Output case StreamMessageResult: event.Type = domain.AgentEventComplete if m.Subtype == "error" || m.IsError { event.Type = domain.AgentEventError event.Content = m.Error } if m.DurationMs > 0 { event.Metadata["duration_ms"] = m.DurationMs } if m.Subtype != "" { event.Metadata["status"] = m.Subtype } default: // Unknown message type, treat as output event.Type = domain.AgentEventOutput event.Content = extractTextContent(m.Content) } return event } // extractTextContent extracts text from content blocks. func extractTextContent(blocks []ContentBlock) string { for _, block := range blocks { if block.Type == "text" && block.Text != "" { return block.Text } } return "" } // parseTimestamp parses an ISO8601 timestamp string. func parseTimestamp(s string) time.Time { if s == "" { return time.Now() } t, err := time.Parse(time.RFC3339, s) if err != nil { return time.Now() } return t } // IsTerminal returns true if this message indicates execution is complete. func (m *StreamMessage) IsTerminal() bool { return m.Type == StreamMessageResult } // IsSuccess returns true if this is a successful result message. func (m *StreamMessage) IsSuccess() bool { return m.Type == StreamMessageResult && m.Subtype == "success" && !m.IsError }