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
163impl SystemMessage {
164    /// Check if this is an init message
165    pub fn is_init(&self) -> bool {
166        self.subtype == "init"
167    }
168
169    /// Check if this is a status message
170    pub fn is_status(&self) -> bool {
171        self.subtype == "status"
172    }
173
174    /// Check if this is a compact_boundary message
175    pub fn is_compact_boundary(&self) -> bool {
176        self.subtype == "compact_boundary"
177    }
178
179    /// Try to parse as an init message
180    pub fn as_init(&self) -> Option<InitMessage> {
181        if self.subtype != "init" {
182            return None;
183        }
184        serde_json::from_value(self.data.clone()).ok()
185    }
186
187    /// Try to parse as a status message
188    pub fn as_status(&self) -> Option<StatusMessage> {
189        if self.subtype != "status" {
190            return None;
191        }
192        serde_json::from_value(self.data.clone()).ok()
193    }
194
195    /// Try to parse as a compact_boundary message
196    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
197        if self.subtype != "compact_boundary" {
198            return None;
199        }
200        serde_json::from_value(self.data.clone()).ok()
201    }
202}
203
204/// Init system message data - sent at session start
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InitMessage {
207    /// Session identifier
208    pub session_id: String,
209    /// Current working directory
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub cwd: Option<String>,
212    /// Model being used
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub model: Option<String>,
215    /// List of available tools
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub tools: Vec<String>,
218    /// MCP servers configured
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub mcp_servers: Vec<Value>,
221}
222
223/// Status system message - sent during operations like context compaction
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StatusMessage {
226    /// Session identifier
227    pub session_id: String,
228    /// Current status (e.g., "compacting") or null when complete
229    pub status: Option<String>,
230    /// Unique identifier for this message
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub uuid: Option<String>,
233}
234
235/// Compact boundary message - marks where context compaction occurred
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct CompactBoundaryMessage {
238    /// Session identifier
239    pub session_id: String,
240    /// Metadata about the compaction
241    pub compact_metadata: CompactMetadata,
242    /// Unique identifier for this message
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub uuid: Option<String>,
245}
246
247/// Metadata about context compaction
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactMetadata {
250    /// Number of tokens before compaction
251    pub pre_tokens: u64,
252    /// What triggered the compaction ("auto" or "manual")
253    pub trigger: String,
254}
255
256/// Assistant message
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct AssistantMessage {
259    pub message: AssistantMessageContent,
260    pub session_id: String,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub uuid: Option<String>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub parent_tool_use_id: Option<String>,
265}
266
267/// Nested message content for assistant messages
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AssistantMessageContent {
270    pub id: String,
271    pub role: String,
272    pub model: String,
273    pub content: Vec<ContentBlock>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub stop_reason: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub stop_sequence: Option<String>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub usage: Option<AssistantUsage>,
280}
281
282/// Usage information for assistant messages
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AssistantUsage {
285    /// Number of input tokens
286    #[serde(default)]
287    pub input_tokens: u32,
288
289    /// Number of output tokens
290    #[serde(default)]
291    pub output_tokens: u32,
292
293    /// Tokens used to create cache
294    #[serde(default)]
295    pub cache_creation_input_tokens: u32,
296
297    /// Tokens read from cache
298    #[serde(default)]
299    pub cache_read_input_tokens: u32,
300
301    /// Service tier used (e.g., "standard")
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub service_tier: Option<String>,
304
305    /// Detailed cache creation breakdown
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub cache_creation: Option<CacheCreationDetails>,
308}
309
310/// Detailed cache creation information
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct CacheCreationDetails {
313    /// Ephemeral 1-hour input tokens
314    #[serde(default)]
315    pub ephemeral_1h_input_tokens: u32,
316
317    /// Ephemeral 5-minute input tokens
318    #[serde(default)]
319    pub ephemeral_5m_input_tokens: u32,
320}
321
322/// Content blocks for messages
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "type", rename_all = "snake_case")]
325pub enum ContentBlock {
326    Text(TextBlock),
327    Image(ImageBlock),
328    Thinking(ThinkingBlock),
329    ToolUse(ToolUseBlock),
330    ToolResult(ToolResultBlock),
331}
332
333/// Text content block
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TextBlock {
336    pub text: String,
337}
338
339/// Image content block (follows Anthropic API structure)
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ImageBlock {
342    pub source: ImageSource,
343}
344
345/// Image source information
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ImageSource {
348    #[serde(rename = "type")]
349    pub source_type: String, // "base64"
350    pub media_type: String, // e.g., "image/jpeg", "image/png"
351    pub data: String,       // Base64-encoded image data
352}
353
354/// Thinking content block
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ThinkingBlock {
357    pub thinking: String,
358    pub signature: String,
359}
360
361/// Tool use content block
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ToolUseBlock {
364    pub id: String,
365    pub name: String,
366    pub input: Value,
367}
368
369impl ToolUseBlock {
370    /// Try to parse the input as a typed ToolInput.
371    ///
372    /// This attempts to deserialize the raw JSON input into a strongly-typed
373    /// `ToolInput` enum variant. Returns `None` if parsing fails.
374    ///
375    /// # Example
376    ///
377    /// ```
378    /// use claude_codes::{ToolUseBlock, ToolInput};
379    /// use serde_json::json;
380    ///
381    /// let block = ToolUseBlock {
382    ///     id: "toolu_123".to_string(),
383    ///     name: "Bash".to_string(),
384    ///     input: json!({"command": "ls -la"}),
385    /// };
386    ///
387    /// if let Some(ToolInput::Bash(bash)) = block.typed_input() {
388    ///     assert_eq!(bash.command, "ls -la");
389    /// }
390    /// ```
391    pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
392        serde_json::from_value(self.input.clone()).ok()
393    }
394
395    /// Parse the input as a typed ToolInput, returning an error on failure.
396    ///
397    /// Unlike `typed_input()`, this method returns the parsing error for debugging.
398    pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
399        serde_json::from_value(self.input.clone())
400    }
401}
402
403/// Tool result content block
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ToolResultBlock {
406    pub tool_use_id: String,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub content: Option<ToolResultContent>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub is_error: Option<bool>,
411}
412
413/// Tool result content type
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(untagged)]
416pub enum ToolResultContent {
417    Text(String),
418    Structured(Vec<Value>),
419}
420
421/// Result message for completed queries
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ResultMessage {
424    pub subtype: ResultSubtype,
425    pub is_error: bool,
426    pub duration_ms: u64,
427    pub duration_api_ms: u64,
428    pub num_turns: i32,
429
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub result: Option<String>,
432
433    pub session_id: String,
434    pub total_cost_usd: f64,
435
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub usage: Option<UsageInfo>,
438
439    /// Tools that were blocked due to permission denials during the session
440    #[serde(default)]
441    pub permission_denials: Vec<PermissionDenial>,
442
443    /// Error messages when `is_error` is true.
444    ///
445    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
446    /// This allows typed access to error conditions without needing to serialize to JSON and search.
447    #[serde(default)]
448    pub errors: Vec<String>,
449
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub uuid: Option<String>,
452}
453
454/// A record of a tool permission that was denied during the session.
455///
456/// This is included in `ResultMessage.permission_denials` to provide a summary
457/// of all permission denials that occurred.
458#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
459pub struct PermissionDenial {
460    /// The name of the tool that was blocked (e.g., "Bash", "Write")
461    pub tool_name: String,
462
463    /// The input that was passed to the tool
464    pub tool_input: Value,
465
466    /// The unique identifier for this tool use request
467    pub tool_use_id: String,
468}
469
470/// Result subtypes
471#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum ResultSubtype {
474    Success,
475    ErrorMaxTurns,
476    ErrorDuringExecution,
477}
478
479/// MCP Server configuration types
480#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum McpServerConfig {
483    Stdio(McpStdioServerConfig),
484    Sse(McpSseServerConfig),
485    Http(McpHttpServerConfig),
486}
487
488/// MCP stdio server configuration
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct McpStdioServerConfig {
491    pub command: String,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub args: Option<Vec<String>>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub env: Option<std::collections::HashMap<String, String>>,
496}
497
498/// MCP SSE server configuration
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct McpSseServerConfig {
501    pub url: String,
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub headers: Option<std::collections::HashMap<String, String>>,
504}
505
506/// MCP HTTP server configuration
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct McpHttpServerConfig {
509    pub url: String,
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub headers: Option<std::collections::HashMap<String, String>>,
512}
513
514/// Permission mode for Claude operations
515#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub enum PermissionMode {
518    Default,
519    AcceptEdits,
520    BypassPermissions,
521    Plan,
522}
523
524// ============================================================================
525// Control Protocol Types (for bidirectional tool approval)
526// ============================================================================
527
528/// Control request from CLI (tool permission requests, hooks, etc.)
529///
530/// When using `--permission-prompt-tool stdio`, the CLI sends these requests
531/// asking for approval before executing tools. The SDK must respond with a
532/// [`ControlResponse`].
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct ControlRequest {
535    /// Unique identifier for this request (used to correlate responses)
536    pub request_id: String,
537    /// The request payload
538    pub request: ControlRequestPayload,
539}
540
541/// Control request payload variants
542#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(tag = "subtype", rename_all = "snake_case")]
544pub enum ControlRequestPayload {
545    /// Tool permission request - Claude wants to use a tool
546    CanUseTool(ToolPermissionRequest),
547    /// Hook callback request
548    HookCallback(HookCallbackRequest),
549    /// MCP message request
550    McpMessage(McpMessageRequest),
551    /// Initialize request (sent by SDK to CLI)
552    Initialize(InitializeRequest),
553}
554
555/// A suggested permission for tool approval.
556///
557/// When Claude requests tool permission, it may include suggestions for
558/// permissions that could be granted to avoid repeated prompts for similar
559/// actions.
560#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
561pub struct PermissionSuggestion {
562    /// The tool this permission applies to (e.g., "Bash")
563    pub tool: String,
564    /// Semantic description of the action (e.g., "run tests")
565    pub prompt: String,
566}
567
568/// Tool permission request details
569///
570/// This is sent when Claude wants to use a tool. The SDK should evaluate
571/// the request and respond with allow/deny using the ergonomic builder methods.
572///
573/// # Example
574///
575/// ```
576/// use claude_codes::{ToolPermissionRequest, ControlResponse};
577/// use serde_json::json;
578///
579/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
580///     // Block dangerous bash commands
581///     if req.tool_name == "Bash" {
582///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
583///             if cmd.contains("rm -rf") {
584///                 return req.deny("Dangerous command blocked", request_id);
585///             }
586///         }
587///     }
588///
589///     // Allow everything else
590///     req.allow(request_id)
591/// }
592/// ```
593#[derive(Debug, Clone, Serialize, Deserialize)]
594pub struct ToolPermissionRequest {
595    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
596    pub tool_name: String,
597    /// Input parameters for the tool
598    pub input: Value,
599    /// Suggested permissions that could be granted to avoid repeated prompts
600    #[serde(default)]
601    pub permission_suggestions: Vec<PermissionSuggestion>,
602    /// Path that was blocked (if this is a retry after path-based denial)
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub blocked_path: Option<String>,
605}
606
607impl ToolPermissionRequest {
608    /// Allow the tool to execute with its original input.
609    ///
610    /// # Example
611    /// ```
612    /// # use claude_codes::ToolPermissionRequest;
613    /// # use serde_json::json;
614    /// let req = ToolPermissionRequest {
615    ///     tool_name: "Read".to_string(),
616    ///     input: json!({"file_path": "/tmp/test.txt"}),
617    ///     permission_suggestions: vec![],
618    ///     blocked_path: None,
619    /// };
620    /// let response = req.allow("req-123");
621    /// ```
622    pub fn allow(&self, request_id: &str) -> ControlResponse {
623        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
624    }
625
626    /// Allow the tool to execute with modified input.
627    ///
628    /// Use this to sanitize or redirect tool inputs. For example, redirecting
629    /// file writes to a safe directory.
630    ///
631    /// # Example
632    /// ```
633    /// # use claude_codes::ToolPermissionRequest;
634    /// # use serde_json::json;
635    /// let req = ToolPermissionRequest {
636    ///     tool_name: "Write".to_string(),
637    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
638    ///     permission_suggestions: vec![],
639    ///     blocked_path: None,
640    /// };
641    /// // Redirect to safe location
642    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
643    /// let response = req.allow_with(safe_input, "req-123");
644    /// ```
645    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
646        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
647    }
648
649    /// Allow with updated permissions list.
650    pub fn allow_with_permissions(
651        &self,
652        modified_input: Value,
653        permissions: Vec<Value>,
654        request_id: &str,
655    ) -> ControlResponse {
656        ControlResponse::from_result(
657            request_id,
658            PermissionResult::allow_with_permissions(modified_input, permissions),
659        )
660    }
661
662    /// Deny the tool execution.
663    ///
664    /// The message will be shown to Claude, who may try a different approach.
665    ///
666    /// # Example
667    /// ```
668    /// # use claude_codes::ToolPermissionRequest;
669    /// # use serde_json::json;
670    /// let req = ToolPermissionRequest {
671    ///     tool_name: "Bash".to_string(),
672    ///     input: json!({"command": "sudo rm -rf /"}),
673    ///     permission_suggestions: vec![],
674    ///     blocked_path: None,
675    /// };
676    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
677    /// ```
678    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
679        ControlResponse::from_result(request_id, PermissionResult::deny(message))
680    }
681
682    /// Deny the tool execution and stop the entire session.
683    ///
684    /// Use this for severe policy violations that should halt all processing.
685    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
686        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
687    }
688}
689
690/// Result of a permission decision
691///
692/// This type represents the decision made by the permission callback.
693/// It can be serialized directly into the control response format.
694#[derive(Debug, Clone, Serialize, Deserialize)]
695#[serde(tag = "behavior", rename_all = "snake_case")]
696pub enum PermissionResult {
697    /// Allow the tool to execute
698    Allow {
699        /// The (possibly modified) input to pass to the tool
700        #[serde(rename = "updatedInput")]
701        updated_input: Value,
702        /// Optional updated permissions list
703        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
704        updated_permissions: Option<Vec<Value>>,
705    },
706    /// Deny the tool execution
707    Deny {
708        /// Message explaining why the tool was denied
709        message: String,
710        /// If true, stop the entire session
711        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
712        interrupt: bool,
713    },
714}
715
716impl PermissionResult {
717    /// Create an allow result with the given input
718    pub fn allow(input: Value) -> Self {
719        PermissionResult::Allow {
720            updated_input: input,
721            updated_permissions: None,
722        }
723    }
724
725    /// Create an allow result with permissions
726    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
727        PermissionResult::Allow {
728            updated_input: input,
729            updated_permissions: Some(permissions),
730        }
731    }
732
733    /// Create a deny result
734    pub fn deny(message: impl Into<String>) -> Self {
735        PermissionResult::Deny {
736            message: message.into(),
737            interrupt: false,
738        }
739    }
740
741    /// Create a deny result that also interrupts the session
742    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
743        PermissionResult::Deny {
744            message: message.into(),
745            interrupt: true,
746        }
747    }
748}
749
750/// Hook callback request
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct HookCallbackRequest {
753    pub callback_id: String,
754    pub input: Value,
755    #[serde(skip_serializing_if = "Option::is_none")]
756    pub tool_use_id: Option<String>,
757}
758
759/// MCP message request
760#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct McpMessageRequest {
762    pub server_name: String,
763    pub message: Value,
764}
765
766/// Initialize request (SDK -> CLI)
767#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct InitializeRequest {
769    #[serde(skip_serializing_if = "Option::is_none")]
770    pub hooks: Option<Value>,
771}
772
773/// Control response to CLI
774///
775/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
776/// constructed directly for other control request types.
777#[derive(Debug, Clone, Serialize, Deserialize)]
778pub struct ControlResponse {
779    /// The request ID this response corresponds to
780    pub response: ControlResponsePayload,
781}
782
783impl ControlResponse {
784    /// Create a success response from a PermissionResult
785    ///
786    /// This is the preferred way to construct permission responses.
787    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
788        // Serialize the PermissionResult to Value for the response
789        let response_value = serde_json::to_value(&result)
790            .expect("PermissionResult serialization should never fail");
791        ControlResponse {
792            response: ControlResponsePayload::Success {
793                request_id: request_id.to_string(),
794                response: Some(response_value),
795            },
796        }
797    }
798
799    /// Create a success response with the given payload (raw Value)
800    pub fn success(request_id: &str, response_data: Value) -> Self {
801        ControlResponse {
802            response: ControlResponsePayload::Success {
803                request_id: request_id.to_string(),
804                response: Some(response_data),
805            },
806        }
807    }
808
809    /// Create an empty success response (for acks)
810    pub fn success_empty(request_id: &str) -> Self {
811        ControlResponse {
812            response: ControlResponsePayload::Success {
813                request_id: request_id.to_string(),
814                response: None,
815            },
816        }
817    }
818
819    /// Create an error response
820    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
821        ControlResponse {
822            response: ControlResponsePayload::Error {
823                request_id: request_id.to_string(),
824                error: error_message.into(),
825            },
826        }
827    }
828}
829
830/// Control response payload
831#[derive(Debug, Clone, Serialize, Deserialize)]
832#[serde(tag = "subtype", rename_all = "snake_case")]
833pub enum ControlResponsePayload {
834    Success {
835        request_id: String,
836        #[serde(skip_serializing_if = "Option::is_none")]
837        response: Option<Value>,
838    },
839    Error {
840        request_id: String,
841        error: String,
842    },
843}
844
845/// Wrapper for outgoing control responses (includes type tag)
846#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct ControlResponseMessage {
848    #[serde(rename = "type")]
849    pub message_type: String,
850    pub response: ControlResponsePayload,
851}
852
853impl From<ControlResponse> for ControlResponseMessage {
854    fn from(resp: ControlResponse) -> Self {
855        ControlResponseMessage {
856            message_type: "control_response".to_string(),
857            response: resp.response,
858        }
859    }
860}
861
862/// Wrapper for outgoing control requests (includes type tag)
863#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct ControlRequestMessage {
865    #[serde(rename = "type")]
866    pub message_type: String,
867    pub request_id: String,
868    pub request: ControlRequestPayload,
869}
870
871impl ControlRequestMessage {
872    /// Create an initialization request to send to CLI
873    pub fn initialize(request_id: impl Into<String>) -> Self {
874        ControlRequestMessage {
875            message_type: "control_request".to_string(),
876            request_id: request_id.into(),
877            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
878        }
879    }
880
881    /// Create an initialization request with hooks configuration
882    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
883        ControlRequestMessage {
884            message_type: "control_request".to_string(),
885            request_id: request_id.into(),
886            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
887        }
888    }
889}
890
891/// Usage information for the request
892#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct UsageInfo {
894    pub input_tokens: u32,
895    pub cache_creation_input_tokens: u32,
896    pub cache_read_input_tokens: u32,
897    pub output_tokens: u32,
898    pub server_tool_use: ServerToolUse,
899    pub service_tier: String,
900}
901
902/// Server tool usage information
903#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct ServerToolUse {
905    pub web_search_requests: u32,
906}
907
908impl ClaudeInput {
909    /// Create a simple text user message
910    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
911        ClaudeInput::User(UserMessage {
912            message: MessageContent {
913                role: "user".to_string(),
914                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
915            },
916            session_id: Some(session_id),
917        })
918    }
919
920    /// Create a user message with content blocks
921    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
922        ClaudeInput::User(UserMessage {
923            message: MessageContent {
924                role: "user".to_string(),
925                content: blocks,
926            },
927            session_id: Some(session_id),
928        })
929    }
930
931    /// Create a user message with an image and optional text
932    /// Only supports JPEG, PNG, GIF, and WebP media types
933    pub fn user_message_with_image(
934        image_data: String,
935        media_type: String,
936        text: Option<String>,
937        session_id: Uuid,
938    ) -> Result<Self, String> {
939        // Validate media type
940        let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
941
942        if !valid_types.contains(&media_type.as_str()) {
943            return Err(format!(
944                "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
945                media_type
946            ));
947        }
948
949        let mut blocks = vec![ContentBlock::Image(ImageBlock {
950            source: ImageSource {
951                source_type: "base64".to_string(),
952                media_type,
953                data: image_data,
954            },
955        })];
956
957        if let Some(text_content) = text {
958            blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
959        }
960
961        Ok(Self::user_message_blocks(blocks, session_id))
962    }
963}
964
965impl ClaudeOutput {
966    /// Get the message type as a string
967    pub fn message_type(&self) -> String {
968        match self {
969            ClaudeOutput::System(_) => "system".to_string(),
970            ClaudeOutput::User(_) => "user".to_string(),
971            ClaudeOutput::Assistant(_) => "assistant".to_string(),
972            ClaudeOutput::Result(_) => "result".to_string(),
973            ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
974            ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
975        }
976    }
977
978    /// Check if this is a control request (tool permission request)
979    pub fn is_control_request(&self) -> bool {
980        matches!(self, ClaudeOutput::ControlRequest(_))
981    }
982
983    /// Check if this is a control response
984    pub fn is_control_response(&self) -> bool {
985        matches!(self, ClaudeOutput::ControlResponse(_))
986    }
987
988    /// Get the control request if this is one
989    pub fn as_control_request(&self) -> Option<&ControlRequest> {
990        match self {
991            ClaudeOutput::ControlRequest(req) => Some(req),
992            _ => None,
993        }
994    }
995
996    /// Check if this is a result with error
997    pub fn is_error(&self) -> bool {
998        matches!(self, ClaudeOutput::Result(r) if r.is_error)
999    }
1000
1001    /// Check if this is an assistant message
1002    pub fn is_assistant_message(&self) -> bool {
1003        matches!(self, ClaudeOutput::Assistant(_))
1004    }
1005
1006    /// Check if this is a system message
1007    pub fn is_system_message(&self) -> bool {
1008        matches!(self, ClaudeOutput::System(_))
1009    }
1010
1011    /// Check if this is a system init message
1012    ///
1013    /// # Example
1014    /// ```
1015    /// use claude_codes::ClaudeOutput;
1016    ///
1017    /// let json = r#"{"type":"system","subtype":"init","session_id":"abc"}"#;
1018    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1019    /// assert!(output.is_system_init());
1020    /// ```
1021    pub fn is_system_init(&self) -> bool {
1022        matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1023    }
1024
1025    /// Get the session ID from any message type that has one.
1026    ///
1027    /// Returns the session ID from System, Assistant, or Result messages.
1028    /// Returns `None` for User, ControlRequest, and ControlResponse messages.
1029    ///
1030    /// # Example
1031    /// ```
1032    /// use claude_codes::ClaudeOutput;
1033    ///
1034    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1035    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1036    ///     "session_id":"my-session","total_cost_usd":0.01}"#;
1037    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1038    /// assert_eq!(output.session_id(), Some("my-session"));
1039    /// ```
1040    pub fn session_id(&self) -> Option<&str> {
1041        match self {
1042            ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1043            ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1044            ClaudeOutput::Result(res) => Some(&res.session_id),
1045            ClaudeOutput::User(_) => None,
1046            ClaudeOutput::ControlRequest(_) => None,
1047            ClaudeOutput::ControlResponse(_) => None,
1048        }
1049    }
1050
1051    /// Get a specific tool use by name from an assistant message.
1052    ///
1053    /// Returns the first `ToolUseBlock` with the given name, or `None` if this
1054    /// is not an assistant message or doesn't contain the specified tool.
1055    ///
1056    /// # Example
1057    /// ```
1058    /// use claude_codes::ClaudeOutput;
1059    ///
1060    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1061    ///     "model":"claude-3","content":[{"type":"tool_use","id":"tu_1",
1062    ///     "name":"Bash","input":{"command":"ls"}}]},"session_id":"abc"}"#;
1063    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1064    ///
1065    /// if let Some(bash) = output.as_tool_use("Bash") {
1066    ///     assert_eq!(bash.name, "Bash");
1067    /// }
1068    /// ```
1069    pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1070        match self {
1071            ClaudeOutput::Assistant(ass) => {
1072                ass.message.content.iter().find_map(|block| match block {
1073                    ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1074                    _ => None,
1075                })
1076            }
1077            _ => None,
1078        }
1079    }
1080
1081    /// Get all tool uses from an assistant message.
1082    ///
1083    /// Returns an iterator over all `ToolUseBlock`s in the message, or an empty
1084    /// iterator if this is not an assistant message.
1085    ///
1086    /// # Example
1087    /// ```
1088    /// use claude_codes::ClaudeOutput;
1089    ///
1090    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1091    ///     "model":"claude-3","content":[
1092    ///         {"type":"tool_use","id":"tu_1","name":"Read","input":{"file_path":"/tmp/a"}},
1093    ///         {"type":"tool_use","id":"tu_2","name":"Write","input":{"file_path":"/tmp/b","content":"x"}}
1094    ///     ]},"session_id":"abc"}"#;
1095    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1096    ///
1097    /// let tools: Vec<_> = output.tool_uses().collect();
1098    /// assert_eq!(tools.len(), 2);
1099    /// ```
1100    pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1101        let content = match self {
1102            ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1103            _ => None,
1104        };
1105
1106        content
1107            .into_iter()
1108            .flat_map(|c| c.iter())
1109            .filter_map(|block| match block {
1110                ContentBlock::ToolUse(tu) => Some(tu),
1111                _ => None,
1112            })
1113    }
1114
1115    /// Get text content from an assistant message.
1116    ///
1117    /// Returns the concatenated text from all text blocks in the message,
1118    /// or `None` if this is not an assistant message or has no text content.
1119    ///
1120    /// # Example
1121    /// ```
1122    /// use claude_codes::ClaudeOutput;
1123    ///
1124    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1125    ///     "model":"claude-3","content":[{"type":"text","text":"Hello, world!"}]},
1126    ///     "session_id":"abc"}"#;
1127    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1128    /// assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1129    /// ```
1130    pub fn text_content(&self) -> Option<String> {
1131        match self {
1132            ClaudeOutput::Assistant(ass) => {
1133                let texts: Vec<&str> = ass
1134                    .message
1135                    .content
1136                    .iter()
1137                    .filter_map(|block| match block {
1138                        ContentBlock::Text(t) => Some(t.text.as_str()),
1139                        _ => None,
1140                    })
1141                    .collect();
1142
1143                if texts.is_empty() {
1144                    None
1145                } else {
1146                    Some(texts.join(""))
1147                }
1148            }
1149            _ => None,
1150        }
1151    }
1152
1153    /// Get the assistant message if this is one.
1154    ///
1155    /// # Example
1156    /// ```
1157    /// use claude_codes::ClaudeOutput;
1158    ///
1159    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1160    ///     "model":"claude-3","content":[]},"session_id":"abc"}"#;
1161    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1162    ///
1163    /// if let Some(assistant) = output.as_assistant() {
1164    ///     assert_eq!(assistant.message.model, "claude-3");
1165    /// }
1166    /// ```
1167    pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1168        match self {
1169            ClaudeOutput::Assistant(ass) => Some(ass),
1170            _ => None,
1171        }
1172    }
1173
1174    /// Get the result message if this is one.
1175    ///
1176    /// # Example
1177    /// ```
1178    /// use claude_codes::ClaudeOutput;
1179    ///
1180    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1181    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1182    ///     "session_id":"abc","total_cost_usd":0.01}"#;
1183    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1184    ///
1185    /// if let Some(result) = output.as_result() {
1186    ///     assert!(!result.is_error);
1187    /// }
1188    /// ```
1189    pub fn as_result(&self) -> Option<&ResultMessage> {
1190        match self {
1191            ClaudeOutput::Result(res) => Some(res),
1192            _ => None,
1193        }
1194    }
1195
1196    /// Get the system message if this is one.
1197    pub fn as_system(&self) -> Option<&SystemMessage> {
1198        match self {
1199            ClaudeOutput::System(sys) => Some(sys),
1200            _ => None,
1201        }
1202    }
1203
1204    /// Parse a JSON string, handling potential ANSI escape codes and other prefixes
1205    /// This method will:
1206    /// 1. First try to parse as-is
1207    /// 2. If that fails, trim until it finds a '{' and try again
1208    pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1209        // First try to parse as-is
1210        match Self::parse_json(s) {
1211            Ok(output) => Ok(output),
1212            Err(first_error) => {
1213                // If that fails, look for the first '{' character
1214                if let Some(json_start) = s.find('{') {
1215                    let trimmed = &s[json_start..];
1216                    match Self::parse_json(trimmed) {
1217                        Ok(output) => Ok(output),
1218                        Err(_) => {
1219                            // Return the original error if both attempts fail
1220                            Err(first_error)
1221                        }
1222                    }
1223                } else {
1224                    Err(first_error)
1225                }
1226            }
1227        }
1228    }
1229
1230    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
1231    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1232        // First try to parse as a Value
1233        let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1234            raw_json: Value::String(s.to_string()),
1235            error_message: format!("Invalid JSON: {}", e),
1236        })?;
1237
1238        // Then try to parse that Value as ClaudeOutput
1239        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1240            raw_json: value,
1241            error_message: e.to_string(),
1242        })
1243    }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248    use super::*;
1249
1250    #[test]
1251    fn test_serialize_user_message() {
1252        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1253        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1254        let json = serde_json::to_string(&input).unwrap();
1255        assert!(json.contains("\"type\":\"user\""));
1256        assert!(json.contains("\"role\":\"user\""));
1257        assert!(json.contains("\"text\":\"Hello, Claude!\""));
1258        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1259    }
1260
1261    #[test]
1262    fn test_deserialize_assistant_message() {
1263        let json = r#"{
1264            "type": "assistant",
1265            "message": {
1266                "id": "msg_123",
1267                "role": "assistant",
1268                "model": "claude-3-sonnet",
1269                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1270            },
1271            "session_id": "123"
1272        }"#;
1273
1274        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1275        assert!(output.is_assistant_message());
1276    }
1277
1278    #[test]
1279    fn test_deserialize_result_message() {
1280        let json = r#"{
1281            "type": "result",
1282            "subtype": "success",
1283            "is_error": false,
1284            "duration_ms": 100,
1285            "duration_api_ms": 200,
1286            "num_turns": 1,
1287            "result": "Done",
1288            "session_id": "123",
1289            "total_cost_usd": 0.01,
1290            "permission_denials": []
1291        }"#;
1292
1293        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1294        assert!(!output.is_error());
1295    }
1296
1297    #[test]
1298    fn test_deserialize_result_with_permission_denials() {
1299        let json = r#"{
1300            "type": "result",
1301            "subtype": "success",
1302            "is_error": false,
1303            "duration_ms": 100,
1304            "duration_api_ms": 200,
1305            "num_turns": 2,
1306            "result": "Done",
1307            "session_id": "123",
1308            "total_cost_usd": 0.01,
1309            "permission_denials": [
1310                {
1311                    "tool_name": "Bash",
1312                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1313                    "tool_use_id": "toolu_123"
1314                }
1315            ]
1316        }"#;
1317
1318        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1319        if let ClaudeOutput::Result(result) = output {
1320            assert_eq!(result.permission_denials.len(), 1);
1321            assert_eq!(result.permission_denials[0].tool_name, "Bash");
1322            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1323            assert_eq!(
1324                result.permission_denials[0]
1325                    .tool_input
1326                    .get("command")
1327                    .unwrap(),
1328                "rm -rf /"
1329            );
1330        } else {
1331            panic!("Expected Result");
1332        }
1333    }
1334
1335    #[test]
1336    fn test_permission_denial_roundtrip() {
1337        let denial = PermissionDenial {
1338            tool_name: "Write".to_string(),
1339            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1340            tool_use_id: "toolu_456".to_string(),
1341        };
1342
1343        let json = serde_json::to_string(&denial).unwrap();
1344        assert!(json.contains("\"tool_name\":\"Write\""));
1345        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1346        assert!(json.contains("/etc/passwd"));
1347
1348        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1349        assert_eq!(parsed, denial);
1350    }
1351
1352    // ============================================================================
1353    // Control Protocol Tests
1354    // ============================================================================
1355
1356    #[test]
1357    fn test_deserialize_control_request_can_use_tool() {
1358        let json = r#"{
1359            "type": "control_request",
1360            "request_id": "perm-abc123",
1361            "request": {
1362                "subtype": "can_use_tool",
1363                "tool_name": "Write",
1364                "input": {
1365                    "file_path": "/home/user/hello.py",
1366                    "content": "print('hello')"
1367                },
1368                "permission_suggestions": [],
1369                "blocked_path": null
1370            }
1371        }"#;
1372
1373        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1374        assert!(output.is_control_request());
1375
1376        if let ClaudeOutput::ControlRequest(req) = output {
1377            assert_eq!(req.request_id, "perm-abc123");
1378            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1379                assert_eq!(perm_req.tool_name, "Write");
1380                assert_eq!(
1381                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1382                    "/home/user/hello.py"
1383                );
1384            } else {
1385                panic!("Expected CanUseTool payload");
1386            }
1387        } else {
1388            panic!("Expected ControlRequest");
1389        }
1390    }
1391
1392    #[test]
1393    fn test_tool_permission_request_allow() {
1394        let req = ToolPermissionRequest {
1395            tool_name: "Read".to_string(),
1396            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1397            permission_suggestions: vec![],
1398            blocked_path: None,
1399        };
1400
1401        let response = req.allow("req-123");
1402        let message: ControlResponseMessage = response.into();
1403
1404        let json = serde_json::to_string(&message).unwrap();
1405        assert!(json.contains("\"type\":\"control_response\""));
1406        assert!(json.contains("\"subtype\":\"success\""));
1407        assert!(json.contains("\"request_id\":\"req-123\""));
1408        assert!(json.contains("\"behavior\":\"allow\""));
1409        assert!(json.contains("\"updatedInput\""));
1410    }
1411
1412    #[test]
1413    fn test_tool_permission_request_allow_with_modified_input() {
1414        let req = ToolPermissionRequest {
1415            tool_name: "Write".to_string(),
1416            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1417            permission_suggestions: vec![],
1418            blocked_path: None,
1419        };
1420
1421        let modified_input = serde_json::json!({
1422            "file_path": "/tmp/safe/passwd",
1423            "content": "test"
1424        });
1425        let response = req.allow_with(modified_input, "req-456");
1426        let message: ControlResponseMessage = response.into();
1427
1428        let json = serde_json::to_string(&message).unwrap();
1429        assert!(json.contains("/tmp/safe/passwd"));
1430        assert!(!json.contains("/etc/passwd"));
1431    }
1432
1433    #[test]
1434    fn test_tool_permission_request_deny() {
1435        let req = ToolPermissionRequest {
1436            tool_name: "Bash".to_string(),
1437            input: serde_json::json!({"command": "sudo rm -rf /"}),
1438            permission_suggestions: vec![],
1439            blocked_path: None,
1440        };
1441
1442        let response = req.deny("Dangerous command blocked", "req-789");
1443        let message: ControlResponseMessage = response.into();
1444
1445        let json = serde_json::to_string(&message).unwrap();
1446        assert!(json.contains("\"behavior\":\"deny\""));
1447        assert!(json.contains("Dangerous command blocked"));
1448        assert!(!json.contains("\"interrupt\":true"));
1449    }
1450
1451    #[test]
1452    fn test_tool_permission_request_deny_and_stop() {
1453        let req = ToolPermissionRequest {
1454            tool_name: "Bash".to_string(),
1455            input: serde_json::json!({"command": "rm -rf /"}),
1456            permission_suggestions: vec![],
1457            blocked_path: None,
1458        };
1459
1460        let response = req.deny_and_stop("Security violation", "req-000");
1461        let message: ControlResponseMessage = response.into();
1462
1463        let json = serde_json::to_string(&message).unwrap();
1464        assert!(json.contains("\"behavior\":\"deny\""));
1465        assert!(json.contains("\"interrupt\":true"));
1466    }
1467
1468    #[test]
1469    fn test_permission_result_serialization() {
1470        // Test allow
1471        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1472        let json = serde_json::to_string(&allow).unwrap();
1473        assert!(json.contains("\"behavior\":\"allow\""));
1474        assert!(json.contains("\"updatedInput\""));
1475
1476        // Test deny
1477        let deny = PermissionResult::deny("Not allowed");
1478        let json = serde_json::to_string(&deny).unwrap();
1479        assert!(json.contains("\"behavior\":\"deny\""));
1480        assert!(json.contains("\"message\":\"Not allowed\""));
1481        assert!(!json.contains("\"interrupt\""));
1482
1483        // Test deny with interrupt
1484        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1485        let json = serde_json::to_string(&deny_stop).unwrap();
1486        assert!(json.contains("\"interrupt\":true"));
1487    }
1488
1489    #[test]
1490    fn test_control_request_message_initialize() {
1491        let init = ControlRequestMessage::initialize("init-1");
1492
1493        let json = serde_json::to_string(&init).unwrap();
1494        assert!(json.contains("\"type\":\"control_request\""));
1495        assert!(json.contains("\"request_id\":\"init-1\""));
1496        assert!(json.contains("\"subtype\":\"initialize\""));
1497    }
1498
1499    #[test]
1500    fn test_control_response_error() {
1501        let response = ControlResponse::error("req-err", "Something went wrong");
1502        let message: ControlResponseMessage = response.into();
1503
1504        let json = serde_json::to_string(&message).unwrap();
1505        assert!(json.contains("\"subtype\":\"error\""));
1506        assert!(json.contains("\"error\":\"Something went wrong\""));
1507    }
1508
1509    #[test]
1510    fn test_roundtrip_control_request() {
1511        // Test that we can serialize and deserialize control requests
1512        let original_json = r#"{
1513            "type": "control_request",
1514            "request_id": "test-123",
1515            "request": {
1516                "subtype": "can_use_tool",
1517                "tool_name": "Bash",
1518                "input": {"command": "ls -la"},
1519                "permission_suggestions": []
1520            }
1521        }"#;
1522
1523        // Parse as ClaudeOutput
1524        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1525
1526        // Serialize back and verify key parts are present
1527        let reserialized = serde_json::to_string(&output).unwrap();
1528        assert!(reserialized.contains("control_request"));
1529        assert!(reserialized.contains("test-123"));
1530        assert!(reserialized.contains("Bash"));
1531    }
1532
1533    #[test]
1534    fn test_permission_suggestions_parsing() {
1535        // Test that permission_suggestions deserialize correctly
1536        let json = r#"{
1537            "type": "control_request",
1538            "request_id": "perm-456",
1539            "request": {
1540                "subtype": "can_use_tool",
1541                "tool_name": "Bash",
1542                "input": {"command": "npm test"},
1543                "permission_suggestions": [
1544                    {"tool": "Bash", "prompt": "run tests"},
1545                    {"tool": "Bash", "prompt": "install dependencies"}
1546                ]
1547            }
1548        }"#;
1549
1550        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1551        if let ClaudeOutput::ControlRequest(req) = output {
1552            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1553                assert_eq!(perm_req.permission_suggestions.len(), 2);
1554                assert_eq!(perm_req.permission_suggestions[0].tool, "Bash");
1555                assert_eq!(perm_req.permission_suggestions[0].prompt, "run tests");
1556                assert_eq!(perm_req.permission_suggestions[1].tool, "Bash");
1557                assert_eq!(
1558                    perm_req.permission_suggestions[1].prompt,
1559                    "install dependencies"
1560                );
1561            } else {
1562                panic!("Expected CanUseTool payload");
1563            }
1564        } else {
1565            panic!("Expected ControlRequest");
1566        }
1567    }
1568
1569    #[test]
1570    fn test_permission_suggestion_roundtrip() {
1571        let suggestion = PermissionSuggestion {
1572            tool: "Bash".to_string(),
1573            prompt: "run tests".to_string(),
1574        };
1575
1576        let json = serde_json::to_string(&suggestion).unwrap();
1577        assert!(json.contains("\"tool\":\"Bash\""));
1578        assert!(json.contains("\"prompt\":\"run tests\""));
1579
1580        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1581        assert_eq!(parsed, suggestion);
1582    }
1583
1584    // ============================================================================
1585    // System Message Subtype Tests
1586    // ============================================================================
1587
1588    #[test]
1589    fn test_system_message_init() {
1590        let json = r#"{
1591            "type": "system",
1592            "subtype": "init",
1593            "session_id": "test-session-123",
1594            "cwd": "/home/user/project",
1595            "model": "claude-sonnet-4",
1596            "tools": ["Bash", "Read", "Write"],
1597            "mcp_servers": []
1598        }"#;
1599
1600        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1601        if let ClaudeOutput::System(sys) = output {
1602            assert!(sys.is_init());
1603            assert!(!sys.is_status());
1604            assert!(!sys.is_compact_boundary());
1605
1606            let init = sys.as_init().expect("Should parse as init");
1607            assert_eq!(init.session_id, "test-session-123");
1608            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1609            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1610            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1611        } else {
1612            panic!("Expected System message");
1613        }
1614    }
1615
1616    #[test]
1617    fn test_system_message_status() {
1618        let json = r#"{
1619            "type": "system",
1620            "subtype": "status",
1621            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1622            "status": "compacting",
1623            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1624        }"#;
1625
1626        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1627        if let ClaudeOutput::System(sys) = output {
1628            assert!(sys.is_status());
1629            assert!(!sys.is_init());
1630
1631            let status = sys.as_status().expect("Should parse as status");
1632            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1633            assert_eq!(status.status, Some("compacting".to_string()));
1634            assert_eq!(
1635                status.uuid,
1636                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1637            );
1638        } else {
1639            panic!("Expected System message");
1640        }
1641    }
1642
1643    #[test]
1644    fn test_system_message_status_null() {
1645        let json = r#"{
1646            "type": "system",
1647            "subtype": "status",
1648            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1649            "status": null,
1650            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1651        }"#;
1652
1653        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1654        if let ClaudeOutput::System(sys) = output {
1655            let status = sys.as_status().expect("Should parse as status");
1656            assert_eq!(status.status, None);
1657        } else {
1658            panic!("Expected System message");
1659        }
1660    }
1661
1662    #[test]
1663    fn test_system_message_compact_boundary() {
1664        let json = r#"{
1665            "type": "system",
1666            "subtype": "compact_boundary",
1667            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1668            "compact_metadata": {
1669                "pre_tokens": 155285,
1670                "trigger": "auto"
1671            },
1672            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1673        }"#;
1674
1675        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1676        if let ClaudeOutput::System(sys) = output {
1677            assert!(sys.is_compact_boundary());
1678            assert!(!sys.is_init());
1679            assert!(!sys.is_status());
1680
1681            let compact = sys
1682                .as_compact_boundary()
1683                .expect("Should parse as compact_boundary");
1684            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1685            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1686            assert_eq!(compact.compact_metadata.trigger, "auto");
1687        } else {
1688            panic!("Expected System message");
1689        }
1690    }
1691
1692    // ============================================================================
1693    // Helper Method Tests
1694    // ============================================================================
1695
1696    #[test]
1697    fn test_is_system_init() {
1698        let init_json = r#"{
1699            "type": "system",
1700            "subtype": "init",
1701            "session_id": "test-session"
1702        }"#;
1703        let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1704        assert!(output.is_system_init());
1705
1706        let status_json = r#"{
1707            "type": "system",
1708            "subtype": "status",
1709            "session_id": "test-session"
1710        }"#;
1711        let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1712        assert!(!output.is_system_init());
1713    }
1714
1715    #[test]
1716    fn test_session_id() {
1717        // Result message
1718        let result_json = r#"{
1719            "type": "result",
1720            "subtype": "success",
1721            "is_error": false,
1722            "duration_ms": 100,
1723            "duration_api_ms": 200,
1724            "num_turns": 1,
1725            "session_id": "result-session",
1726            "total_cost_usd": 0.01
1727        }"#;
1728        let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1729        assert_eq!(output.session_id(), Some("result-session"));
1730
1731        // Assistant message
1732        let assistant_json = r#"{
1733            "type": "assistant",
1734            "message": {
1735                "id": "msg_1",
1736                "role": "assistant",
1737                "model": "claude-3",
1738                "content": []
1739            },
1740            "session_id": "assistant-session"
1741        }"#;
1742        let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1743        assert_eq!(output.session_id(), Some("assistant-session"));
1744
1745        // System message
1746        let system_json = r#"{
1747            "type": "system",
1748            "subtype": "init",
1749            "session_id": "system-session"
1750        }"#;
1751        let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1752        assert_eq!(output.session_id(), Some("system-session"));
1753    }
1754
1755    #[test]
1756    fn test_as_tool_use() {
1757        let json = r#"{
1758            "type": "assistant",
1759            "message": {
1760                "id": "msg_1",
1761                "role": "assistant",
1762                "model": "claude-3",
1763                "content": [
1764                    {"type": "text", "text": "Let me run that command."},
1765                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1766                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1767                ]
1768            },
1769            "session_id": "abc"
1770        }"#;
1771        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1772
1773        // Find Bash tool
1774        let bash = output.as_tool_use("Bash");
1775        assert!(bash.is_some());
1776        assert_eq!(bash.unwrap().id, "tu_1");
1777
1778        // Find Read tool
1779        let read = output.as_tool_use("Read");
1780        assert!(read.is_some());
1781        assert_eq!(read.unwrap().id, "tu_2");
1782
1783        // Non-existent tool
1784        assert!(output.as_tool_use("Write").is_none());
1785
1786        // Not an assistant message
1787        let result_json = r#"{
1788            "type": "result",
1789            "subtype": "success",
1790            "is_error": false,
1791            "duration_ms": 100,
1792            "duration_api_ms": 200,
1793            "num_turns": 1,
1794            "session_id": "abc",
1795            "total_cost_usd": 0.01
1796        }"#;
1797        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1798        assert!(result.as_tool_use("Bash").is_none());
1799    }
1800
1801    #[test]
1802    fn test_tool_uses() {
1803        let json = r#"{
1804            "type": "assistant",
1805            "message": {
1806                "id": "msg_1",
1807                "role": "assistant",
1808                "model": "claude-3",
1809                "content": [
1810                    {"type": "text", "text": "Running commands..."},
1811                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1812                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1813                    {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1814                ]
1815            },
1816            "session_id": "abc"
1817        }"#;
1818        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1819
1820        let tools: Vec<_> = output.tool_uses().collect();
1821        assert_eq!(tools.len(), 3);
1822        assert_eq!(tools[0].name, "Bash");
1823        assert_eq!(tools[1].name, "Read");
1824        assert_eq!(tools[2].name, "Write");
1825    }
1826
1827    #[test]
1828    fn test_text_content() {
1829        // Single text block
1830        let json = r#"{
1831            "type": "assistant",
1832            "message": {
1833                "id": "msg_1",
1834                "role": "assistant",
1835                "model": "claude-3",
1836                "content": [{"type": "text", "text": "Hello, world!"}]
1837            },
1838            "session_id": "abc"
1839        }"#;
1840        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1841        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1842
1843        // Multiple text blocks
1844        let json = r#"{
1845            "type": "assistant",
1846            "message": {
1847                "id": "msg_1",
1848                "role": "assistant",
1849                "model": "claude-3",
1850                "content": [
1851                    {"type": "text", "text": "Hello, "},
1852                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1853                    {"type": "text", "text": "world!"}
1854                ]
1855            },
1856            "session_id": "abc"
1857        }"#;
1858        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1859        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1860
1861        // No text blocks
1862        let json = r#"{
1863            "type": "assistant",
1864            "message": {
1865                "id": "msg_1",
1866                "role": "assistant",
1867                "model": "claude-3",
1868                "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1869            },
1870            "session_id": "abc"
1871        }"#;
1872        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1873        assert_eq!(output.text_content(), None);
1874
1875        // Not an assistant message
1876        let json = r#"{
1877            "type": "result",
1878            "subtype": "success",
1879            "is_error": false,
1880            "duration_ms": 100,
1881            "duration_api_ms": 200,
1882            "num_turns": 1,
1883            "session_id": "abc",
1884            "total_cost_usd": 0.01
1885        }"#;
1886        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1887        assert_eq!(output.text_content(), None);
1888    }
1889
1890    #[test]
1891    fn test_as_assistant() {
1892        let json = r#"{
1893            "type": "assistant",
1894            "message": {
1895                "id": "msg_1",
1896                "role": "assistant",
1897                "model": "claude-sonnet-4",
1898                "content": []
1899            },
1900            "session_id": "abc"
1901        }"#;
1902        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1903
1904        let assistant = output.as_assistant();
1905        assert!(assistant.is_some());
1906        assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
1907
1908        // Not an assistant
1909        let result_json = r#"{
1910            "type": "result",
1911            "subtype": "success",
1912            "is_error": false,
1913            "duration_ms": 100,
1914            "duration_api_ms": 200,
1915            "num_turns": 1,
1916            "session_id": "abc",
1917            "total_cost_usd": 0.01
1918        }"#;
1919        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1920        assert!(result.as_assistant().is_none());
1921    }
1922
1923    #[test]
1924    fn test_as_result() {
1925        let json = r#"{
1926            "type": "result",
1927            "subtype": "success",
1928            "is_error": false,
1929            "duration_ms": 100,
1930            "duration_api_ms": 200,
1931            "num_turns": 5,
1932            "session_id": "abc",
1933            "total_cost_usd": 0.05
1934        }"#;
1935        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1936
1937        let result = output.as_result();
1938        assert!(result.is_some());
1939        assert_eq!(result.unwrap().num_turns, 5);
1940        assert_eq!(result.unwrap().total_cost_usd, 0.05);
1941
1942        // Not a result
1943        let assistant_json = r#"{
1944            "type": "assistant",
1945            "message": {
1946                "id": "msg_1",
1947                "role": "assistant",
1948                "model": "claude-3",
1949                "content": []
1950            },
1951            "session_id": "abc"
1952        }"#;
1953        let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1954        assert!(assistant.as_result().is_none());
1955    }
1956
1957    #[test]
1958    fn test_as_system() {
1959        let json = r#"{
1960            "type": "system",
1961            "subtype": "init",
1962            "session_id": "abc",
1963            "model": "claude-3"
1964        }"#;
1965        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1966
1967        let system = output.as_system();
1968        assert!(system.is_some());
1969        assert!(system.unwrap().is_init());
1970
1971        // Not a system message
1972        let result_json = r#"{
1973            "type": "result",
1974            "subtype": "success",
1975            "is_error": false,
1976            "duration_ms": 100,
1977            "duration_api_ms": 200,
1978            "num_turns": 1,
1979            "session_id": "abc",
1980            "total_cost_usd": 0.01
1981        }"#;
1982        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1983        assert!(result.as_system().is_none());
1984    }
1985
1986    // ============================================================================
1987    // ResultMessage Errors Field Tests
1988    // ============================================================================
1989
1990    #[test]
1991    fn test_deserialize_result_message_with_errors() {
1992        let json = r#"{
1993            "type": "result",
1994            "subtype": "error_during_execution",
1995            "duration_ms": 0,
1996            "duration_api_ms": 0,
1997            "is_error": true,
1998            "num_turns": 0,
1999            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2000            "total_cost_usd": 0,
2001            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2002            "permission_denials": []
2003        }"#;
2004
2005        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2006        assert!(output.is_error());
2007
2008        if let ClaudeOutput::Result(res) = output {
2009            assert!(res.is_error);
2010            assert_eq!(res.errors.len(), 1);
2011            assert!(res.errors[0].contains("No conversation found"));
2012        } else {
2013            panic!("Expected Result message");
2014        }
2015    }
2016
2017    #[test]
2018    fn test_deserialize_result_message_errors_defaults_empty() {
2019        // Test that errors field defaults to empty Vec when not present
2020        let json = r#"{
2021            "type": "result",
2022            "subtype": "success",
2023            "is_error": false,
2024            "duration_ms": 100,
2025            "duration_api_ms": 200,
2026            "num_turns": 1,
2027            "session_id": "123",
2028            "total_cost_usd": 0.01
2029        }"#;
2030
2031        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2032        if let ClaudeOutput::Result(res) = output {
2033            assert!(res.errors.is_empty());
2034        } else {
2035            panic!("Expected Result message");
2036        }
2037    }
2038
2039    #[test]
2040    fn test_result_message_errors_roundtrip() {
2041        let json = r#"{
2042            "type": "result",
2043            "subtype": "error_during_execution",
2044            "is_error": true,
2045            "duration_ms": 0,
2046            "duration_api_ms": 0,
2047            "num_turns": 0,
2048            "session_id": "test-session",
2049            "total_cost_usd": 0.0,
2050            "errors": ["Error 1", "Error 2"]
2051        }"#;
2052
2053        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2054        let reserialized = serde_json::to_string(&output).unwrap();
2055
2056        // Verify the errors are preserved
2057        assert!(reserialized.contains("Error 1"));
2058        assert!(reserialized.contains("Error 2"));
2059    }
2060}