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>
164 lines
4.9 KiB
Go
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
|
|
}
|