rdev/internal/adapter/codeagent/claudecode/parser.go
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

164 lines
4.9 KiB
Go

// 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
}