claude_codes/
io.rs

1//! Core message types for Claude communication.
2//!
3//! This module defines the primary message structures used in the Claude protocol:
4//!
5//! - [`ClaudeInput`] - Messages sent to Claude
6//! - [`ClaudeOutput`] - Messages received from Claude
7//! - [`ContentBlock`] - Different types of content within messages
8//!
9//! # Message Flow
10//!
11//! 1. Create a [`ClaudeInput`] with your query
12//! 2. Send it to Claude via a client
13//! 3. Receive [`ClaudeOutput`] messages in response
14//! 4. Handle different output types (System, Assistant, Result)
15//!
16//! # Example
17//!
18//! ```
19//! use claude_codes::{ClaudeInput, ClaudeOutput};
20//!
21//! // Create an input message
22//! let input = ClaudeInput::user_message("Hello, Claude!", uuid::Uuid::new_v4());
23//!
24//! // Parse an output message
25//! let json = r#"{"type":"assistant","message":{"role":"assistant","content":[]}}"#;
26//! match ClaudeOutput::parse_json(json) {
27//!     Ok(output) => println!("Got: {}", output.message_type()),
28//!     Err(e) => eprintln!("Parse error: {}", e),
29//! }
30//! ```
31
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37/// Serialize an optional UUID as a string
38fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40    S: Serializer,
41{
42    match uuid {
43        Some(id) => serializer.serialize_str(&id.to_string()),
44        None => serializer.serialize_none(),
45    }
46}
47
48/// Deserialize an optional UUID from a string
49fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51    D: Deserializer<'de>,
52{
53    let opt_str: Option<String> = Option::deserialize(deserializer)?;
54    match opt_str {
55        Some(s) => Uuid::parse_str(&s)
56            .map(Some)
57            .map_err(serde::de::Error::custom),
58        None => Ok(None),
59    }
60}
61
62/// Top-level enum for all possible Claude input messages
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66    /// User message input
67    User(UserMessage),
68
69    /// Control request (for initialization handshake)
70    ControlRequest(ControlRequest),
71
72    /// Control response (for tool permission responses)
73    ControlResponse(ControlResponse),
74
75    /// Raw JSON for untyped messages
76    #[serde(untagged)]
77    Raw(Value),
78}
79
80/// Error type for parsing failures that preserves the raw JSON
81#[derive(Debug, Clone)]
82pub struct ParseError {
83    /// The raw JSON value that failed to parse
84    pub raw_json: Value,
85    /// The underlying serde error message
86    pub error_message: String,
87}
88
89impl fmt::Display for ParseError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
92    }
93}
94
95impl std::error::Error for ParseError {}
96
97/// Top-level enum for all possible Claude output messages
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum ClaudeOutput {
101    /// System initialization message
102    System(SystemMessage),
103
104    /// User message echoed back
105    User(UserMessage),
106
107    /// Assistant response
108    Assistant(AssistantMessage),
109
110    /// Result message (completion of a query)
111    Result(ResultMessage),
112
113    /// Control request from CLI (tool permissions, hooks, etc.)
114    ControlRequest(ControlRequest),
115
116    /// Control response from CLI (ack for initialization, etc.)
117    ControlResponse(ControlResponse),
118}
119
120/// User message
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UserMessage {
123    pub message: MessageContent,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    #[serde(
126        serialize_with = "serialize_optional_uuid",
127        deserialize_with = "deserialize_optional_uuid"
128    )]
129    pub session_id: Option<Uuid>,
130}
131
132/// Message content with role
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MessageContent {
135    pub role: String,
136    #[serde(deserialize_with = "deserialize_content_blocks")]
137    pub content: Vec<ContentBlock>,
138}
139
140/// Deserialize content blocks that can be either a string or array
141fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
142where
143    D: Deserializer<'de>,
144{
145    let value: Value = Value::deserialize(deserializer)?;
146    match value {
147        Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
148        Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
149        _ => Err(serde::de::Error::custom(
150            "content must be a string or array",
151        )),
152    }
153}
154
155/// System message with metadata
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SystemMessage {
158    pub subtype: String,
159    #[serde(flatten)]
160    pub data: Value, // Captures all other fields
161}
162
163/// Assistant message
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct AssistantMessage {
166    pub message: AssistantMessageContent,
167    pub session_id: String,
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub uuid: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub parent_tool_use_id: Option<String>,
172}
173
174/// Nested message content for assistant messages
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct AssistantMessageContent {
177    pub id: String,
178    pub role: String,
179    pub model: String,
180    pub content: Vec<ContentBlock>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub stop_reason: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub stop_sequence: Option<String>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub usage: Option<serde_json::Value>,
187}
188
189/// Content blocks for messages
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum ContentBlock {
193    Text(TextBlock),
194    Image(ImageBlock),
195    Thinking(ThinkingBlock),
196    ToolUse(ToolUseBlock),
197    ToolResult(ToolResultBlock),
198}
199
200/// Text content block
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TextBlock {
203    pub text: String,
204}
205
206/// Image content block (follows Anthropic API structure)
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ImageBlock {
209    pub source: ImageSource,
210}
211
212/// Image source information
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ImageSource {
215    #[serde(rename = "type")]
216    pub source_type: String, // "base64"
217    pub media_type: String, // e.g., "image/jpeg", "image/png"
218    pub data: String,       // Base64-encoded image data
219}
220
221/// Thinking content block
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ThinkingBlock {
224    pub thinking: String,
225    pub signature: String,
226}
227
228/// Tool use content block
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolUseBlock {
231    pub id: String,
232    pub name: String,
233    pub input: Value,
234}
235
236/// Tool result content block
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ToolResultBlock {
239    pub tool_use_id: String,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub content: Option<ToolResultContent>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub is_error: Option<bool>,
244}
245
246/// Tool result content type
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(untagged)]
249pub enum ToolResultContent {
250    Text(String),
251    Structured(Vec<Value>),
252}
253
254/// Result message for completed queries
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ResultMessage {
257    pub subtype: ResultSubtype,
258    pub is_error: bool,
259    pub duration_ms: u64,
260    pub duration_api_ms: u64,
261    pub num_turns: i32,
262
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub result: Option<String>,
265
266    pub session_id: String,
267    pub total_cost_usd: f64,
268
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub usage: Option<UsageInfo>,
271
272    #[serde(default)]
273    pub permission_denials: Vec<Value>,
274
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub uuid: Option<String>,
277}
278
279/// Result subtypes
280#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ResultSubtype {
283    Success,
284    ErrorMaxTurns,
285    ErrorDuringExecution,
286}
287
288/// MCP Server configuration types
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(tag = "type", rename_all = "snake_case")]
291pub enum McpServerConfig {
292    Stdio(McpStdioServerConfig),
293    Sse(McpSseServerConfig),
294    Http(McpHttpServerConfig),
295}
296
297/// MCP stdio server configuration
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct McpStdioServerConfig {
300    pub command: String,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub args: Option<Vec<String>>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub env: Option<std::collections::HashMap<String, String>>,
305}
306
307/// MCP SSE server configuration
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct McpSseServerConfig {
310    pub url: String,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub headers: Option<std::collections::HashMap<String, String>>,
313}
314
315/// MCP HTTP server configuration
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct McpHttpServerConfig {
318    pub url: String,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub headers: Option<std::collections::HashMap<String, String>>,
321}
322
323/// Permission mode for Claude operations
324#[derive(Debug, Clone, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub enum PermissionMode {
327    Default,
328    AcceptEdits,
329    BypassPermissions,
330    Plan,
331}
332
333// ============================================================================
334// Control Protocol Types (for bidirectional tool approval)
335// ============================================================================
336
337/// Control request from CLI (tool permission requests, hooks, etc.)
338///
339/// When using `--permission-prompt-tool stdio`, the CLI sends these requests
340/// asking for approval before executing tools. The SDK must respond with a
341/// [`ControlResponse`].
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ControlRequest {
344    /// Unique identifier for this request (used to correlate responses)
345    pub request_id: String,
346    /// The request payload
347    pub request: ControlRequestPayload,
348}
349
350/// Control request payload variants
351#[derive(Debug, Clone, Serialize, Deserialize)]
352#[serde(tag = "subtype", rename_all = "snake_case")]
353pub enum ControlRequestPayload {
354    /// Tool permission request - Claude wants to use a tool
355    CanUseTool(ToolPermissionRequest),
356    /// Hook callback request
357    HookCallback(HookCallbackRequest),
358    /// MCP message request
359    McpMessage(McpMessageRequest),
360    /// Initialize request (sent by SDK to CLI)
361    Initialize(InitializeRequest),
362}
363
364/// Tool permission request details
365///
366/// This is sent when Claude wants to use a tool. The SDK should evaluate
367/// the request and respond with allow/deny using the ergonomic builder methods.
368///
369/// # Example
370///
371/// ```
372/// use claude_codes::{ToolPermissionRequest, ControlResponse};
373/// use serde_json::json;
374///
375/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
376///     // Block dangerous bash commands
377///     if req.tool_name == "Bash" {
378///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
379///             if cmd.contains("rm -rf") {
380///                 return req.deny("Dangerous command blocked", request_id);
381///             }
382///         }
383///     }
384///
385///     // Allow everything else
386///     req.allow(request_id)
387/// }
388/// ```
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ToolPermissionRequest {
391    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
392    pub tool_name: String,
393    /// Input parameters for the tool
394    pub input: Value,
395    /// Suggested permissions (if any)
396    #[serde(default)]
397    pub permission_suggestions: Vec<Value>,
398    /// Path that was blocked (if this is a retry after path-based denial)
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub blocked_path: Option<String>,
401}
402
403impl ToolPermissionRequest {
404    /// Allow the tool to execute with its original input.
405    ///
406    /// # Example
407    /// ```
408    /// # use claude_codes::ToolPermissionRequest;
409    /// # use serde_json::json;
410    /// let req = ToolPermissionRequest {
411    ///     tool_name: "Read".to_string(),
412    ///     input: json!({"file_path": "/tmp/test.txt"}),
413    ///     permission_suggestions: vec![],
414    ///     blocked_path: None,
415    /// };
416    /// let response = req.allow("req-123");
417    /// ```
418    pub fn allow(&self, request_id: &str) -> ControlResponse {
419        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
420    }
421
422    /// Allow the tool to execute with modified input.
423    ///
424    /// Use this to sanitize or redirect tool inputs. For example, redirecting
425    /// file writes to a safe directory.
426    ///
427    /// # Example
428    /// ```
429    /// # use claude_codes::ToolPermissionRequest;
430    /// # use serde_json::json;
431    /// let req = ToolPermissionRequest {
432    ///     tool_name: "Write".to_string(),
433    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
434    ///     permission_suggestions: vec![],
435    ///     blocked_path: None,
436    /// };
437    /// // Redirect to safe location
438    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
439    /// let response = req.allow_with(safe_input, "req-123");
440    /// ```
441    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
442        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
443    }
444
445    /// Allow with updated permissions list.
446    pub fn allow_with_permissions(
447        &self,
448        modified_input: Value,
449        permissions: Vec<Value>,
450        request_id: &str,
451    ) -> ControlResponse {
452        ControlResponse::from_result(
453            request_id,
454            PermissionResult::allow_with_permissions(modified_input, permissions),
455        )
456    }
457
458    /// Deny the tool execution.
459    ///
460    /// The message will be shown to Claude, who may try a different approach.
461    ///
462    /// # Example
463    /// ```
464    /// # use claude_codes::ToolPermissionRequest;
465    /// # use serde_json::json;
466    /// let req = ToolPermissionRequest {
467    ///     tool_name: "Bash".to_string(),
468    ///     input: json!({"command": "sudo rm -rf /"}),
469    ///     permission_suggestions: vec![],
470    ///     blocked_path: None,
471    /// };
472    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
473    /// ```
474    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
475        ControlResponse::from_result(request_id, PermissionResult::deny(message))
476    }
477
478    /// Deny the tool execution and stop the entire session.
479    ///
480    /// Use this for severe policy violations that should halt all processing.
481    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
482        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
483    }
484}
485
486/// Result of a permission decision
487///
488/// This type represents the decision made by the permission callback.
489/// It can be serialized directly into the control response format.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491#[serde(tag = "behavior", rename_all = "snake_case")]
492pub enum PermissionResult {
493    /// Allow the tool to execute
494    Allow {
495        /// The (possibly modified) input to pass to the tool
496        #[serde(rename = "updatedInput")]
497        updated_input: Value,
498        /// Optional updated permissions list
499        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
500        updated_permissions: Option<Vec<Value>>,
501    },
502    /// Deny the tool execution
503    Deny {
504        /// Message explaining why the tool was denied
505        message: String,
506        /// If true, stop the entire session
507        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
508        interrupt: bool,
509    },
510}
511
512impl PermissionResult {
513    /// Create an allow result with the given input
514    pub fn allow(input: Value) -> Self {
515        PermissionResult::Allow {
516            updated_input: input,
517            updated_permissions: None,
518        }
519    }
520
521    /// Create an allow result with permissions
522    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
523        PermissionResult::Allow {
524            updated_input: input,
525            updated_permissions: Some(permissions),
526        }
527    }
528
529    /// Create a deny result
530    pub fn deny(message: impl Into<String>) -> Self {
531        PermissionResult::Deny {
532            message: message.into(),
533            interrupt: false,
534        }
535    }
536
537    /// Create a deny result that also interrupts the session
538    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
539        PermissionResult::Deny {
540            message: message.into(),
541            interrupt: true,
542        }
543    }
544}
545
546/// Hook callback request
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct HookCallbackRequest {
549    pub callback_id: String,
550    pub input: Value,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub tool_use_id: Option<String>,
553}
554
555/// MCP message request
556#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct McpMessageRequest {
558    pub server_name: String,
559    pub message: Value,
560}
561
562/// Initialize request (SDK -> CLI)
563#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct InitializeRequest {
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub hooks: Option<Value>,
567}
568
569/// Control response to CLI
570///
571/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
572/// constructed directly for other control request types.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct ControlResponse {
575    /// The request ID this response corresponds to
576    pub response: ControlResponsePayload,
577}
578
579impl ControlResponse {
580    /// Create a success response from a PermissionResult
581    ///
582    /// This is the preferred way to construct permission responses.
583    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
584        // Serialize the PermissionResult to Value for the response
585        let response_value = serde_json::to_value(&result)
586            .expect("PermissionResult serialization should never fail");
587        ControlResponse {
588            response: ControlResponsePayload::Success {
589                request_id: request_id.to_string(),
590                response: Some(response_value),
591            },
592        }
593    }
594
595    /// Create a success response with the given payload (raw Value)
596    pub fn success(request_id: &str, response_data: Value) -> Self {
597        ControlResponse {
598            response: ControlResponsePayload::Success {
599                request_id: request_id.to_string(),
600                response: Some(response_data),
601            },
602        }
603    }
604
605    /// Create an empty success response (for acks)
606    pub fn success_empty(request_id: &str) -> Self {
607        ControlResponse {
608            response: ControlResponsePayload::Success {
609                request_id: request_id.to_string(),
610                response: None,
611            },
612        }
613    }
614
615    /// Create an error response
616    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
617        ControlResponse {
618            response: ControlResponsePayload::Error {
619                request_id: request_id.to_string(),
620                error: error_message.into(),
621            },
622        }
623    }
624}
625
626/// Control response payload
627#[derive(Debug, Clone, Serialize, Deserialize)]
628#[serde(tag = "subtype", rename_all = "snake_case")]
629pub enum ControlResponsePayload {
630    Success {
631        request_id: String,
632        #[serde(skip_serializing_if = "Option::is_none")]
633        response: Option<Value>,
634    },
635    Error {
636        request_id: String,
637        error: String,
638    },
639}
640
641/// Wrapper for outgoing control responses (includes type tag)
642#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct ControlResponseMessage {
644    #[serde(rename = "type")]
645    pub message_type: String,
646    pub response: ControlResponsePayload,
647}
648
649impl From<ControlResponse> for ControlResponseMessage {
650    fn from(resp: ControlResponse) -> Self {
651        ControlResponseMessage {
652            message_type: "control_response".to_string(),
653            response: resp.response,
654        }
655    }
656}
657
658/// Wrapper for outgoing control requests (includes type tag)
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct ControlRequestMessage {
661    #[serde(rename = "type")]
662    pub message_type: String,
663    pub request_id: String,
664    pub request: ControlRequestPayload,
665}
666
667impl ControlRequestMessage {
668    /// Create an initialization request to send to CLI
669    pub fn initialize(request_id: impl Into<String>) -> Self {
670        ControlRequestMessage {
671            message_type: "control_request".to_string(),
672            request_id: request_id.into(),
673            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
674        }
675    }
676
677    /// Create an initialization request with hooks configuration
678    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
679        ControlRequestMessage {
680            message_type: "control_request".to_string(),
681            request_id: request_id.into(),
682            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
683        }
684    }
685}
686
687/// Usage information for the request
688#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct UsageInfo {
690    pub input_tokens: u32,
691    pub cache_creation_input_tokens: u32,
692    pub cache_read_input_tokens: u32,
693    pub output_tokens: u32,
694    pub server_tool_use: ServerToolUse,
695    pub service_tier: String,
696}
697
698/// Server tool usage information
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ServerToolUse {
701    pub web_search_requests: u32,
702}
703
704impl ClaudeInput {
705    /// Create a simple text user message
706    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
707        ClaudeInput::User(UserMessage {
708            message: MessageContent {
709                role: "user".to_string(),
710                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
711            },
712            session_id: Some(session_id),
713        })
714    }
715
716    /// Create a user message with content blocks
717    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
718        ClaudeInput::User(UserMessage {
719            message: MessageContent {
720                role: "user".to_string(),
721                content: blocks,
722            },
723            session_id: Some(session_id),
724        })
725    }
726
727    /// Create a user message with an image and optional text
728    /// Only supports JPEG, PNG, GIF, and WebP media types
729    pub fn user_message_with_image(
730        image_data: String,
731        media_type: String,
732        text: Option<String>,
733        session_id: Uuid,
734    ) -> Result<Self, String> {
735        // Validate media type
736        let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
737
738        if !valid_types.contains(&media_type.as_str()) {
739            return Err(format!(
740                "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
741                media_type
742            ));
743        }
744
745        let mut blocks = vec![ContentBlock::Image(ImageBlock {
746            source: ImageSource {
747                source_type: "base64".to_string(),
748                media_type,
749                data: image_data,
750            },
751        })];
752
753        if let Some(text_content) = text {
754            blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
755        }
756
757        Ok(Self::user_message_blocks(blocks, session_id))
758    }
759}
760
761impl ClaudeOutput {
762    /// Get the message type as a string
763    pub fn message_type(&self) -> String {
764        match self {
765            ClaudeOutput::System(_) => "system".to_string(),
766            ClaudeOutput::User(_) => "user".to_string(),
767            ClaudeOutput::Assistant(_) => "assistant".to_string(),
768            ClaudeOutput::Result(_) => "result".to_string(),
769            ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
770            ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
771        }
772    }
773
774    /// Check if this is a control request (tool permission request)
775    pub fn is_control_request(&self) -> bool {
776        matches!(self, ClaudeOutput::ControlRequest(_))
777    }
778
779    /// Check if this is a control response
780    pub fn is_control_response(&self) -> bool {
781        matches!(self, ClaudeOutput::ControlResponse(_))
782    }
783
784    /// Get the control request if this is one
785    pub fn as_control_request(&self) -> Option<&ControlRequest> {
786        match self {
787            ClaudeOutput::ControlRequest(req) => Some(req),
788            _ => None,
789        }
790    }
791
792    /// Check if this is a result with error
793    pub fn is_error(&self) -> bool {
794        matches!(self, ClaudeOutput::Result(r) if r.is_error)
795    }
796
797    /// Check if this is an assistant message
798    pub fn is_assistant_message(&self) -> bool {
799        matches!(self, ClaudeOutput::Assistant(_))
800    }
801
802    /// Check if this is a system message
803    pub fn is_system_message(&self) -> bool {
804        matches!(self, ClaudeOutput::System(_))
805    }
806
807    /// Parse a JSON string, handling potential ANSI escape codes and other prefixes
808    /// This method will:
809    /// 1. First try to parse as-is
810    /// 2. If that fails, trim until it finds a '{' and try again
811    pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
812        // First try to parse as-is
813        match Self::parse_json(s) {
814            Ok(output) => Ok(output),
815            Err(first_error) => {
816                // If that fails, look for the first '{' character
817                if let Some(json_start) = s.find('{') {
818                    let trimmed = &s[json_start..];
819                    match Self::parse_json(trimmed) {
820                        Ok(output) => Ok(output),
821                        Err(_) => {
822                            // Return the original error if both attempts fail
823                            Err(first_error)
824                        }
825                    }
826                } else {
827                    Err(first_error)
828                }
829            }
830        }
831    }
832
833    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
834    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
835        // First try to parse as a Value
836        let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
837            raw_json: Value::String(s.to_string()),
838            error_message: format!("Invalid JSON: {}", e),
839        })?;
840
841        // Then try to parse that Value as ClaudeOutput
842        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
843            raw_json: value,
844            error_message: e.to_string(),
845        })
846    }
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    #[test]
854    fn test_serialize_user_message() {
855        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
856        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
857        let json = serde_json::to_string(&input).unwrap();
858        assert!(json.contains("\"type\":\"user\""));
859        assert!(json.contains("\"role\":\"user\""));
860        assert!(json.contains("\"text\":\"Hello, Claude!\""));
861        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
862    }
863
864    #[test]
865    fn test_deserialize_assistant_message() {
866        let json = r#"{
867            "type": "assistant",
868            "message": {
869                "id": "msg_123",
870                "role": "assistant",
871                "model": "claude-3-sonnet",
872                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
873            },
874            "session_id": "123"
875        }"#;
876
877        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
878        assert!(output.is_assistant_message());
879    }
880
881    #[test]
882    fn test_deserialize_result_message() {
883        let json = r#"{
884            "type": "result",
885            "subtype": "success",
886            "is_error": false,
887            "duration_ms": 100,
888            "duration_api_ms": 200,
889            "num_turns": 1,
890            "result": "Done",
891            "session_id": "123",
892            "total_cost_usd": 0.01,
893            "permission_denials": []
894        }"#;
895
896        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
897        assert!(!output.is_error());
898    }
899
900    // ============================================================================
901    // Control Protocol Tests
902    // ============================================================================
903
904    #[test]
905    fn test_deserialize_control_request_can_use_tool() {
906        let json = r#"{
907            "type": "control_request",
908            "request_id": "perm-abc123",
909            "request": {
910                "subtype": "can_use_tool",
911                "tool_name": "Write",
912                "input": {
913                    "file_path": "/home/user/hello.py",
914                    "content": "print('hello')"
915                },
916                "permission_suggestions": [],
917                "blocked_path": null
918            }
919        }"#;
920
921        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
922        assert!(output.is_control_request());
923
924        if let ClaudeOutput::ControlRequest(req) = output {
925            assert_eq!(req.request_id, "perm-abc123");
926            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
927                assert_eq!(perm_req.tool_name, "Write");
928                assert_eq!(
929                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
930                    "/home/user/hello.py"
931                );
932            } else {
933                panic!("Expected CanUseTool payload");
934            }
935        } else {
936            panic!("Expected ControlRequest");
937        }
938    }
939
940    #[test]
941    fn test_tool_permission_request_allow() {
942        let req = ToolPermissionRequest {
943            tool_name: "Read".to_string(),
944            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
945            permission_suggestions: vec![],
946            blocked_path: None,
947        };
948
949        let response = req.allow("req-123");
950        let message: ControlResponseMessage = response.into();
951
952        let json = serde_json::to_string(&message).unwrap();
953        assert!(json.contains("\"type\":\"control_response\""));
954        assert!(json.contains("\"subtype\":\"success\""));
955        assert!(json.contains("\"request_id\":\"req-123\""));
956        assert!(json.contains("\"behavior\":\"allow\""));
957        assert!(json.contains("\"updatedInput\""));
958    }
959
960    #[test]
961    fn test_tool_permission_request_allow_with_modified_input() {
962        let req = ToolPermissionRequest {
963            tool_name: "Write".to_string(),
964            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
965            permission_suggestions: vec![],
966            blocked_path: None,
967        };
968
969        let modified_input = serde_json::json!({
970            "file_path": "/tmp/safe/passwd",
971            "content": "test"
972        });
973        let response = req.allow_with(modified_input, "req-456");
974        let message: ControlResponseMessage = response.into();
975
976        let json = serde_json::to_string(&message).unwrap();
977        assert!(json.contains("/tmp/safe/passwd"));
978        assert!(!json.contains("/etc/passwd"));
979    }
980
981    #[test]
982    fn test_tool_permission_request_deny() {
983        let req = ToolPermissionRequest {
984            tool_name: "Bash".to_string(),
985            input: serde_json::json!({"command": "sudo rm -rf /"}),
986            permission_suggestions: vec![],
987            blocked_path: None,
988        };
989
990        let response = req.deny("Dangerous command blocked", "req-789");
991        let message: ControlResponseMessage = response.into();
992
993        let json = serde_json::to_string(&message).unwrap();
994        assert!(json.contains("\"behavior\":\"deny\""));
995        assert!(json.contains("Dangerous command blocked"));
996        assert!(!json.contains("\"interrupt\":true"));
997    }
998
999    #[test]
1000    fn test_tool_permission_request_deny_and_stop() {
1001        let req = ToolPermissionRequest {
1002            tool_name: "Bash".to_string(),
1003            input: serde_json::json!({"command": "rm -rf /"}),
1004            permission_suggestions: vec![],
1005            blocked_path: None,
1006        };
1007
1008        let response = req.deny_and_stop("Security violation", "req-000");
1009        let message: ControlResponseMessage = response.into();
1010
1011        let json = serde_json::to_string(&message).unwrap();
1012        assert!(json.contains("\"behavior\":\"deny\""));
1013        assert!(json.contains("\"interrupt\":true"));
1014    }
1015
1016    #[test]
1017    fn test_permission_result_serialization() {
1018        // Test allow
1019        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1020        let json = serde_json::to_string(&allow).unwrap();
1021        assert!(json.contains("\"behavior\":\"allow\""));
1022        assert!(json.contains("\"updatedInput\""));
1023
1024        // Test deny
1025        let deny = PermissionResult::deny("Not allowed");
1026        let json = serde_json::to_string(&deny).unwrap();
1027        assert!(json.contains("\"behavior\":\"deny\""));
1028        assert!(json.contains("\"message\":\"Not allowed\""));
1029        assert!(!json.contains("\"interrupt\""));
1030
1031        // Test deny with interrupt
1032        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1033        let json = serde_json::to_string(&deny_stop).unwrap();
1034        assert!(json.contains("\"interrupt\":true"));
1035    }
1036
1037    #[test]
1038    fn test_control_request_message_initialize() {
1039        let init = ControlRequestMessage::initialize("init-1");
1040
1041        let json = serde_json::to_string(&init).unwrap();
1042        assert!(json.contains("\"type\":\"control_request\""));
1043        assert!(json.contains("\"request_id\":\"init-1\""));
1044        assert!(json.contains("\"subtype\":\"initialize\""));
1045    }
1046
1047    #[test]
1048    fn test_control_response_error() {
1049        let response = ControlResponse::error("req-err", "Something went wrong");
1050        let message: ControlResponseMessage = response.into();
1051
1052        let json = serde_json::to_string(&message).unwrap();
1053        assert!(json.contains("\"subtype\":\"error\""));
1054        assert!(json.contains("\"error\":\"Something went wrong\""));
1055    }
1056
1057    #[test]
1058    fn test_roundtrip_control_request() {
1059        // Test that we can serialize and deserialize control requests
1060        let original_json = r#"{
1061            "type": "control_request",
1062            "request_id": "test-123",
1063            "request": {
1064                "subtype": "can_use_tool",
1065                "tool_name": "Bash",
1066                "input": {"command": "ls -la"},
1067                "permission_suggestions": []
1068            }
1069        }"#;
1070
1071        // Parse as ClaudeOutput
1072        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1073
1074        // Serialize back and verify key parts are present
1075        let reserialized = serde_json::to_string(&output).unwrap();
1076        assert!(reserialized.contains("control_request"));
1077        assert!(reserialized.contains("test-123"));
1078        assert!(reserialized.contains("Bash"));
1079    }
1080}