Skip to main content

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    /// API error from Anthropic (500, 529 overloaded, etc.)
120    Error(AnthropicError),
121}
122
123/// API error message from Anthropic.
124///
125/// When Claude Code encounters an API error (e.g., 500, 529 overloaded), it outputs
126/// a JSON message with `type: "error"`. This struct captures that error information.
127///
128/// # Example JSON
129///
130/// ```json
131/// {
132///   "type": "error",
133///   "error": {
134///     "type": "api_error",
135///     "message": "Internal server error"
136///   },
137///   "request_id": "req_011CXPC6BqUogB959LWEf52X"
138/// }
139/// ```
140///
141/// # Example
142///
143/// ```
144/// use claude_codes::ClaudeOutput;
145///
146/// let json = r#"{"type":"error","error":{"type":"api_error","message":"Internal server error"},"request_id":"req_123"}"#;
147/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
148///
149/// if let ClaudeOutput::Error(err) = output {
150///     println!("Error type: {}", err.error.error_type);
151///     println!("Message: {}", err.error.message);
152/// }
153/// ```
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct AnthropicError {
156    /// The nested error details
157    pub error: AnthropicErrorDetails,
158    /// The request ID for debugging/support
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub request_id: Option<String>,
161}
162
163impl AnthropicError {
164    /// Check if this is an overloaded error (HTTP 529)
165    pub fn is_overloaded(&self) -> bool {
166        self.error.error_type == "overloaded_error"
167    }
168
169    /// Check if this is a server error (HTTP 500)
170    pub fn is_server_error(&self) -> bool {
171        self.error.error_type == "api_error"
172    }
173
174    /// Check if this is an invalid request error (HTTP 400)
175    pub fn is_invalid_request(&self) -> bool {
176        self.error.error_type == "invalid_request_error"
177    }
178
179    /// Check if this is an authentication error (HTTP 401)
180    pub fn is_authentication_error(&self) -> bool {
181        self.error.error_type == "authentication_error"
182    }
183
184    /// Check if this is a rate limit error (HTTP 429)
185    pub fn is_rate_limited(&self) -> bool {
186        self.error.error_type == "rate_limit_error"
187    }
188}
189
190/// Details of an Anthropic API error.
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct AnthropicErrorDetails {
193    /// The type of error (e.g., "api_error", "overloaded_error", "invalid_request_error")
194    #[serde(rename = "type")]
195    pub error_type: String,
196    /// Human-readable error message
197    pub message: String,
198}
199
200/// User message
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct UserMessage {
203    pub message: MessageContent,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    #[serde(
206        serialize_with = "serialize_optional_uuid",
207        deserialize_with = "deserialize_optional_uuid"
208    )]
209    pub session_id: Option<Uuid>,
210}
211
212/// Message content with role
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct MessageContent {
215    pub role: String,
216    #[serde(deserialize_with = "deserialize_content_blocks")]
217    pub content: Vec<ContentBlock>,
218}
219
220/// Deserialize content blocks that can be either a string or array
221fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
222where
223    D: Deserializer<'de>,
224{
225    let value: Value = Value::deserialize(deserializer)?;
226    match value {
227        Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
228        Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
229        _ => Err(serde::de::Error::custom(
230            "content must be a string or array",
231        )),
232    }
233}
234
235/// System message with metadata
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct SystemMessage {
238    pub subtype: String,
239    #[serde(flatten)]
240    pub data: Value, // Captures all other fields
241}
242
243impl SystemMessage {
244    /// Check if this is an init message
245    pub fn is_init(&self) -> bool {
246        self.subtype == "init"
247    }
248
249    /// Check if this is a status message
250    pub fn is_status(&self) -> bool {
251        self.subtype == "status"
252    }
253
254    /// Check if this is a compact_boundary message
255    pub fn is_compact_boundary(&self) -> bool {
256        self.subtype == "compact_boundary"
257    }
258
259    /// Try to parse as an init message
260    pub fn as_init(&self) -> Option<InitMessage> {
261        if self.subtype != "init" {
262            return None;
263        }
264        serde_json::from_value(self.data.clone()).ok()
265    }
266
267    /// Try to parse as a status message
268    pub fn as_status(&self) -> Option<StatusMessage> {
269        if self.subtype != "status" {
270            return None;
271        }
272        serde_json::from_value(self.data.clone()).ok()
273    }
274
275    /// Try to parse as a compact_boundary message
276    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
277        if self.subtype != "compact_boundary" {
278            return None;
279        }
280        serde_json::from_value(self.data.clone()).ok()
281    }
282}
283
284/// Init system message data - sent at session start
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct InitMessage {
287    /// Session identifier
288    pub session_id: String,
289    /// Current working directory
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub cwd: Option<String>,
292    /// Model being used
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub model: Option<String>,
295    /// List of available tools
296    #[serde(default, skip_serializing_if = "Vec::is_empty")]
297    pub tools: Vec<String>,
298    /// MCP servers configured
299    #[serde(default, skip_serializing_if = "Vec::is_empty")]
300    pub mcp_servers: Vec<Value>,
301}
302
303/// Status system message - sent during operations like context compaction
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct StatusMessage {
306    /// Session identifier
307    pub session_id: String,
308    /// Current status (e.g., "compacting") or null when complete
309    pub status: Option<String>,
310    /// Unique identifier for this message
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub uuid: Option<String>,
313}
314
315/// Compact boundary message - marks where context compaction occurred
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct CompactBoundaryMessage {
318    /// Session identifier
319    pub session_id: String,
320    /// Metadata about the compaction
321    pub compact_metadata: CompactMetadata,
322    /// Unique identifier for this message
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub uuid: Option<String>,
325}
326
327/// Metadata about context compaction
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct CompactMetadata {
330    /// Number of tokens before compaction
331    pub pre_tokens: u64,
332    /// What triggered the compaction ("auto" or "manual")
333    pub trigger: String,
334}
335
336/// Assistant message
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct AssistantMessage {
339    pub message: AssistantMessageContent,
340    pub session_id: String,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub uuid: Option<String>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub parent_tool_use_id: Option<String>,
345}
346
347/// Nested message content for assistant messages
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct AssistantMessageContent {
350    pub id: String,
351    pub role: String,
352    pub model: String,
353    pub content: Vec<ContentBlock>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub stop_reason: Option<String>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub stop_sequence: Option<String>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub usage: Option<AssistantUsage>,
360}
361
362/// Usage information for assistant messages
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AssistantUsage {
365    /// Number of input tokens
366    #[serde(default)]
367    pub input_tokens: u32,
368
369    /// Number of output tokens
370    #[serde(default)]
371    pub output_tokens: u32,
372
373    /// Tokens used to create cache
374    #[serde(default)]
375    pub cache_creation_input_tokens: u32,
376
377    /// Tokens read from cache
378    #[serde(default)]
379    pub cache_read_input_tokens: u32,
380
381    /// Service tier used (e.g., "standard")
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub service_tier: Option<String>,
384
385    /// Detailed cache creation breakdown
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub cache_creation: Option<CacheCreationDetails>,
388}
389
390/// Detailed cache creation information
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct CacheCreationDetails {
393    /// Ephemeral 1-hour input tokens
394    #[serde(default)]
395    pub ephemeral_1h_input_tokens: u32,
396
397    /// Ephemeral 5-minute input tokens
398    #[serde(default)]
399    pub ephemeral_5m_input_tokens: u32,
400}
401
402/// Content blocks for messages
403#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(tag = "type", rename_all = "snake_case")]
405pub enum ContentBlock {
406    Text(TextBlock),
407    Image(ImageBlock),
408    Thinking(ThinkingBlock),
409    ToolUse(ToolUseBlock),
410    ToolResult(ToolResultBlock),
411}
412
413/// Text content block
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct TextBlock {
416    pub text: String,
417}
418
419/// Image content block (follows Anthropic API structure)
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct ImageBlock {
422    pub source: ImageSource,
423}
424
425/// Image source information
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ImageSource {
428    #[serde(rename = "type")]
429    pub source_type: String, // "base64"
430    pub media_type: String, // e.g., "image/jpeg", "image/png"
431    pub data: String,       // Base64-encoded image data
432}
433
434/// Thinking content block
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ThinkingBlock {
437    pub thinking: String,
438    pub signature: String,
439}
440
441/// Tool use content block
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct ToolUseBlock {
444    pub id: String,
445    pub name: String,
446    pub input: Value,
447}
448
449impl ToolUseBlock {
450    /// Try to parse the input as a typed ToolInput.
451    ///
452    /// This attempts to deserialize the raw JSON input into a strongly-typed
453    /// `ToolInput` enum variant. Returns `None` if parsing fails.
454    ///
455    /// # Example
456    ///
457    /// ```
458    /// use claude_codes::{ToolUseBlock, ToolInput};
459    /// use serde_json::json;
460    ///
461    /// let block = ToolUseBlock {
462    ///     id: "toolu_123".to_string(),
463    ///     name: "Bash".to_string(),
464    ///     input: json!({"command": "ls -la"}),
465    /// };
466    ///
467    /// if let Some(ToolInput::Bash(bash)) = block.typed_input() {
468    ///     assert_eq!(bash.command, "ls -la");
469    /// }
470    /// ```
471    pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
472        serde_json::from_value(self.input.clone()).ok()
473    }
474
475    /// Parse the input as a typed ToolInput, returning an error on failure.
476    ///
477    /// Unlike `typed_input()`, this method returns the parsing error for debugging.
478    pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
479        serde_json::from_value(self.input.clone())
480    }
481}
482
483/// Tool result content block
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct ToolResultBlock {
486    pub tool_use_id: String,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub content: Option<ToolResultContent>,
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub is_error: Option<bool>,
491}
492
493/// Tool result content type
494#[derive(Debug, Clone, Serialize, Deserialize)]
495#[serde(untagged)]
496pub enum ToolResultContent {
497    Text(String),
498    Structured(Vec<Value>),
499}
500
501/// Result message for completed queries
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ResultMessage {
504    pub subtype: ResultSubtype,
505    pub is_error: bool,
506    pub duration_ms: u64,
507    pub duration_api_ms: u64,
508    pub num_turns: i32,
509
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub result: Option<String>,
512
513    pub session_id: String,
514    pub total_cost_usd: f64,
515
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub usage: Option<UsageInfo>,
518
519    /// Tools that were blocked due to permission denials during the session
520    #[serde(default)]
521    pub permission_denials: Vec<PermissionDenial>,
522
523    /// Error messages when `is_error` is true.
524    ///
525    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
526    /// This allows typed access to error conditions without needing to serialize to JSON and search.
527    #[serde(default)]
528    pub errors: Vec<String>,
529
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub uuid: Option<String>,
532}
533
534/// A record of a tool permission that was denied during the session.
535///
536/// This is included in `ResultMessage.permission_denials` to provide a summary
537/// of all permission denials that occurred.
538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
539pub struct PermissionDenial {
540    /// The name of the tool that was blocked (e.g., "Bash", "Write")
541    pub tool_name: String,
542
543    /// The input that was passed to the tool
544    pub tool_input: Value,
545
546    /// The unique identifier for this tool use request
547    pub tool_use_id: String,
548}
549
550/// Result subtypes
551#[derive(Debug, Clone, Serialize, Deserialize)]
552#[serde(rename_all = "snake_case")]
553pub enum ResultSubtype {
554    Success,
555    ErrorMaxTurns,
556    ErrorDuringExecution,
557}
558
559/// MCP Server configuration types
560#[derive(Debug, Clone, Serialize, Deserialize)]
561#[serde(tag = "type", rename_all = "snake_case")]
562pub enum McpServerConfig {
563    Stdio(McpStdioServerConfig),
564    Sse(McpSseServerConfig),
565    Http(McpHttpServerConfig),
566}
567
568/// MCP stdio server configuration
569#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct McpStdioServerConfig {
571    pub command: String,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub args: Option<Vec<String>>,
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub env: Option<std::collections::HashMap<String, String>>,
576}
577
578/// MCP SSE server configuration
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct McpSseServerConfig {
581    pub url: String,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub headers: Option<std::collections::HashMap<String, String>>,
584}
585
586/// MCP HTTP server configuration
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct McpHttpServerConfig {
589    pub url: String,
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub headers: Option<std::collections::HashMap<String, String>>,
592}
593
594/// Permission mode for Claude operations
595#[derive(Debug, Clone, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub enum PermissionMode {
598    Default,
599    AcceptEdits,
600    BypassPermissions,
601    Plan,
602}
603
604// ============================================================================
605// Control Protocol Types (for bidirectional tool approval)
606// ============================================================================
607
608/// Control request from CLI (tool permission requests, hooks, etc.)
609///
610/// When using `--permission-prompt-tool stdio`, the CLI sends these requests
611/// asking for approval before executing tools. The SDK must respond with a
612/// [`ControlResponse`].
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct ControlRequest {
615    /// Unique identifier for this request (used to correlate responses)
616    pub request_id: String,
617    /// The request payload
618    pub request: ControlRequestPayload,
619}
620
621/// Control request payload variants
622#[derive(Debug, Clone, Serialize, Deserialize)]
623#[serde(tag = "subtype", rename_all = "snake_case")]
624pub enum ControlRequestPayload {
625    /// Tool permission request - Claude wants to use a tool
626    CanUseTool(ToolPermissionRequest),
627    /// Hook callback request
628    HookCallback(HookCallbackRequest),
629    /// MCP message request
630    McpMessage(McpMessageRequest),
631    /// Initialize request (sent by SDK to CLI)
632    Initialize(InitializeRequest),
633}
634
635/// A permission to grant for "remember this decision" functionality.
636///
637/// When responding to a tool permission request, you can include permissions
638/// that should be granted to avoid repeated prompts for similar actions.
639///
640/// # Example
641///
642/// ```
643/// use claude_codes::Permission;
644///
645/// // Grant permission for a specific bash command
646/// let perm = Permission::allow_tool("Bash", "npm test");
647///
648/// // Grant permission to set a mode for the session
649/// let mode_perm = Permission::set_mode("acceptEdits", "session");
650/// ```
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
652pub struct Permission {
653    /// The type of permission (e.g., "addRules", "setMode")
654    #[serde(rename = "type")]
655    pub permission_type: String,
656    /// Where to apply this permission (e.g., "session", "project")
657    pub destination: String,
658    /// The permission mode (for setMode type)
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub mode: Option<String>,
661    /// The behavior (for addRules type, e.g., "allow", "deny")
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub behavior: Option<String>,
664    /// The rules to add (for addRules type)
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub rules: Option<Vec<PermissionRule>>,
667}
668
669/// A rule within a permission grant.
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
671pub struct PermissionRule {
672    /// The name of the tool this rule applies to
673    #[serde(rename = "toolName")]
674    pub tool_name: String,
675    /// The rule content (glob pattern or command pattern)
676    #[serde(rename = "ruleContent")]
677    pub rule_content: String,
678}
679
680impl Permission {
681    /// Create a permission to allow a specific tool with a rule pattern.
682    ///
683    /// # Example
684    /// ```
685    /// use claude_codes::Permission;
686    ///
687    /// // Allow "npm test" bash command for this session
688    /// let perm = Permission::allow_tool("Bash", "npm test");
689    ///
690    /// // Allow reading from /tmp directory
691    /// let read_perm = Permission::allow_tool("Read", "/tmp/**");
692    /// ```
693    pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
694        Permission {
695            permission_type: "addRules".to_string(),
696            destination: "session".to_string(),
697            mode: None,
698            behavior: Some("allow".to_string()),
699            rules: Some(vec![PermissionRule {
700                tool_name: tool_name.into(),
701                rule_content: rule_content.into(),
702            }]),
703        }
704    }
705
706    /// Create a permission to allow a tool with a specific destination.
707    ///
708    /// # Example
709    /// ```
710    /// use claude_codes::Permission;
711    ///
712    /// // Allow for the entire project, not just session
713    /// let perm = Permission::allow_tool_with_destination("Bash", "npm test", "project");
714    /// ```
715    pub fn allow_tool_with_destination(
716        tool_name: impl Into<String>,
717        rule_content: impl Into<String>,
718        destination: impl Into<String>,
719    ) -> Self {
720        Permission {
721            permission_type: "addRules".to_string(),
722            destination: destination.into(),
723            mode: None,
724            behavior: Some("allow".to_string()),
725            rules: Some(vec![PermissionRule {
726                tool_name: tool_name.into(),
727                rule_content: rule_content.into(),
728            }]),
729        }
730    }
731
732    /// Create a permission to set a mode (like acceptEdits or bypassPermissions).
733    ///
734    /// # Example
735    /// ```
736    /// use claude_codes::Permission;
737    ///
738    /// // Accept all edits for this session
739    /// let perm = Permission::set_mode("acceptEdits", "session");
740    /// ```
741    pub fn set_mode(mode: impl Into<String>, destination: impl Into<String>) -> Self {
742        Permission {
743            permission_type: "setMode".to_string(),
744            destination: destination.into(),
745            mode: Some(mode.into()),
746            behavior: None,
747            rules: None,
748        }
749    }
750
751    /// Create a permission from a PermissionSuggestion.
752    ///
753    /// This is useful when you want to grant a permission that Claude suggested.
754    ///
755    /// # Example
756    /// ```
757    /// use claude_codes::{Permission, PermissionSuggestion};
758    ///
759    /// // Convert a suggestion to a permission for the response
760    /// let suggestion = PermissionSuggestion {
761    ///     suggestion_type: "setMode".to_string(),
762    ///     destination: "session".to_string(),
763    ///     mode: Some("acceptEdits".to_string()),
764    ///     behavior: None,
765    ///     rules: None,
766    /// };
767    /// let perm = Permission::from_suggestion(&suggestion);
768    /// ```
769    pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
770        Permission {
771            permission_type: suggestion.suggestion_type.clone(),
772            destination: suggestion.destination.clone(),
773            mode: suggestion.mode.clone(),
774            behavior: suggestion.behavior.clone(),
775            rules: suggestion.rules.as_ref().map(|rules| {
776                rules
777                    .iter()
778                    .filter_map(|v| {
779                        Some(PermissionRule {
780                            tool_name: v.get("toolName")?.as_str()?.to_string(),
781                            rule_content: v.get("ruleContent")?.as_str()?.to_string(),
782                        })
783                    })
784                    .collect()
785            }),
786        }
787    }
788}
789
790/// A suggested permission for tool approval.
791///
792/// When Claude requests tool permission, it may include suggestions for
793/// permissions that could be granted to avoid repeated prompts for similar
794/// actions. The format varies based on the suggestion type:
795///
796/// - `setMode`: `{"type": "setMode", "mode": "acceptEdits", "destination": "session"}`
797/// - `addRules`: `{"type": "addRules", "rules": [...], "behavior": "allow", "destination": "session"}`
798///
799/// Use the helper methods to access common fields.
800#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
801pub struct PermissionSuggestion {
802    /// The type of suggestion (e.g., "setMode", "addRules")
803    #[serde(rename = "type")]
804    pub suggestion_type: String,
805    /// Where to apply this permission (e.g., "session", "project")
806    pub destination: String,
807    /// The permission mode (for setMode type)
808    #[serde(skip_serializing_if = "Option::is_none")]
809    pub mode: Option<String>,
810    /// The behavior (for addRules type, e.g., "allow")
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub behavior: Option<String>,
813    /// The rules to add (for addRules type)
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub rules: Option<Vec<Value>>,
816}
817
818/// Tool permission request details
819///
820/// This is sent when Claude wants to use a tool. The SDK should evaluate
821/// the request and respond with allow/deny using the ergonomic builder methods.
822///
823/// # Example
824///
825/// ```
826/// use claude_codes::{ToolPermissionRequest, ControlResponse};
827/// use serde_json::json;
828///
829/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
830///     // Block dangerous bash commands
831///     if req.tool_name == "Bash" {
832///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
833///             if cmd.contains("rm -rf") {
834///                 return req.deny("Dangerous command blocked", request_id);
835///             }
836///         }
837///     }
838///
839///     // Allow everything else
840///     req.allow(request_id)
841/// }
842/// ```
843#[derive(Debug, Clone, Serialize, Deserialize)]
844pub struct ToolPermissionRequest {
845    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
846    pub tool_name: String,
847    /// Input parameters for the tool
848    pub input: Value,
849    /// Suggested permissions that could be granted to avoid repeated prompts
850    #[serde(default)]
851    pub permission_suggestions: Vec<PermissionSuggestion>,
852    /// Path that was blocked (if this is a retry after path-based denial)
853    #[serde(skip_serializing_if = "Option::is_none")]
854    pub blocked_path: Option<String>,
855    /// Reason why this tool use requires approval
856    #[serde(skip_serializing_if = "Option::is_none")]
857    pub decision_reason: Option<String>,
858    /// The tool use ID for this request
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub tool_use_id: Option<String>,
861}
862
863impl ToolPermissionRequest {
864    /// Allow the tool to execute with its original input.
865    ///
866    /// # Example
867    /// ```
868    /// # use claude_codes::ToolPermissionRequest;
869    /// # use serde_json::json;
870    /// let req = ToolPermissionRequest {
871    ///     tool_name: "Read".to_string(),
872    ///     input: json!({"file_path": "/tmp/test.txt"}),
873    ///     permission_suggestions: vec![],
874    ///     blocked_path: None,
875    ///     decision_reason: None,
876    ///     tool_use_id: None,
877    /// };
878    /// let response = req.allow("req-123");
879    /// ```
880    pub fn allow(&self, request_id: &str) -> ControlResponse {
881        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
882    }
883
884    /// Allow the tool to execute with modified input.
885    ///
886    /// Use this to sanitize or redirect tool inputs. For example, redirecting
887    /// file writes to a safe directory.
888    ///
889    /// # Example
890    /// ```
891    /// # use claude_codes::ToolPermissionRequest;
892    /// # use serde_json::json;
893    /// let req = ToolPermissionRequest {
894    ///     tool_name: "Write".to_string(),
895    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
896    ///     permission_suggestions: vec![],
897    ///     blocked_path: None,
898    ///     decision_reason: None,
899    ///     tool_use_id: None,
900    /// };
901    /// // Redirect to safe location
902    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
903    /// let response = req.allow_with(safe_input, "req-123");
904    /// ```
905    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
906        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
907    }
908
909    /// Allow with updated permissions list (raw JSON Values).
910    ///
911    /// Prefer using `allow_and_remember` for type safety.
912    pub fn allow_with_permissions(
913        &self,
914        modified_input: Value,
915        permissions: Vec<Value>,
916        request_id: &str,
917    ) -> ControlResponse {
918        ControlResponse::from_result(
919            request_id,
920            PermissionResult::allow_with_permissions(modified_input, permissions),
921        )
922    }
923
924    /// Allow the tool and grant permissions for "remember this decision".
925    ///
926    /// This is the ergonomic way to allow a tool while also granting permissions
927    /// so similar actions won't require approval in the future.
928    ///
929    /// # Example
930    /// ```
931    /// use claude_codes::{ToolPermissionRequest, Permission};
932    /// use serde_json::json;
933    ///
934    /// let req = ToolPermissionRequest {
935    ///     tool_name: "Bash".to_string(),
936    ///     input: json!({"command": "npm test"}),
937    ///     permission_suggestions: vec![],
938    ///     blocked_path: None,
939    ///     decision_reason: None,
940    ///     tool_use_id: None,
941    /// };
942    ///
943    /// // Allow and remember this decision for the session
944    /// let response = req.allow_and_remember(
945    ///     vec![Permission::allow_tool("Bash", "npm test")],
946    ///     "req-123",
947    /// );
948    /// ```
949    pub fn allow_and_remember(
950        &self,
951        permissions: Vec<Permission>,
952        request_id: &str,
953    ) -> ControlResponse {
954        ControlResponse::from_result(
955            request_id,
956            PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
957        )
958    }
959
960    /// Allow the tool with modified input and grant permissions.
961    ///
962    /// Combines input modification with "remember this decision" functionality.
963    pub fn allow_with_and_remember(
964        &self,
965        modified_input: Value,
966        permissions: Vec<Permission>,
967        request_id: &str,
968    ) -> ControlResponse {
969        ControlResponse::from_result(
970            request_id,
971            PermissionResult::allow_with_typed_permissions(modified_input, permissions),
972        )
973    }
974
975    /// Allow the tool and remember using the first permission suggestion.
976    ///
977    /// This is a convenience method for the common case of accepting Claude's
978    /// first suggested permission (usually the most relevant one).
979    ///
980    /// Returns `None` if there are no permission suggestions.
981    ///
982    /// # Example
983    /// ```
984    /// use claude_codes::ToolPermissionRequest;
985    /// use serde_json::json;
986    ///
987    /// let req = ToolPermissionRequest {
988    ///     tool_name: "Bash".to_string(),
989    ///     input: json!({"command": "npm test"}),
990    ///     permission_suggestions: vec![],  // Would have suggestions in real use
991    ///     blocked_path: None,
992    ///     decision_reason: None,
993    ///     tool_use_id: None,
994    /// };
995    ///
996    /// // Try to allow with first suggestion, or just allow without remembering
997    /// let response = req.allow_and_remember_suggestion("req-123")
998    ///     .unwrap_or_else(|| req.allow("req-123"));
999    /// ```
1000    pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
1001        self.permission_suggestions.first().map(|suggestion| {
1002            let perm = Permission::from_suggestion(suggestion);
1003            self.allow_and_remember(vec![perm], request_id)
1004        })
1005    }
1006
1007    /// Deny the tool execution.
1008    ///
1009    /// The message will be shown to Claude, who may try a different approach.
1010    ///
1011    /// # Example
1012    /// ```
1013    /// # use claude_codes::ToolPermissionRequest;
1014    /// # use serde_json::json;
1015    /// let req = ToolPermissionRequest {
1016    ///     tool_name: "Bash".to_string(),
1017    ///     input: json!({"command": "sudo rm -rf /"}),
1018    ///     permission_suggestions: vec![],
1019    ///     blocked_path: None,
1020    ///     decision_reason: None,
1021    ///     tool_use_id: None,
1022    /// };
1023    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
1024    /// ```
1025    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
1026        ControlResponse::from_result(request_id, PermissionResult::deny(message))
1027    }
1028
1029    /// Deny the tool execution and stop the entire session.
1030    ///
1031    /// Use this for severe policy violations that should halt all processing.
1032    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
1033        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
1034    }
1035}
1036
1037/// Result of a permission decision
1038///
1039/// This type represents the decision made by the permission callback.
1040/// It can be serialized directly into the control response format.
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042#[serde(tag = "behavior", rename_all = "snake_case")]
1043pub enum PermissionResult {
1044    /// Allow the tool to execute
1045    Allow {
1046        /// The (possibly modified) input to pass to the tool
1047        #[serde(rename = "updatedInput")]
1048        updated_input: Value,
1049        /// Optional updated permissions list
1050        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
1051        updated_permissions: Option<Vec<Value>>,
1052    },
1053    /// Deny the tool execution
1054    Deny {
1055        /// Message explaining why the tool was denied
1056        message: String,
1057        /// If true, stop the entire session
1058        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1059        interrupt: bool,
1060    },
1061}
1062
1063impl PermissionResult {
1064    /// Create an allow result with the given input
1065    pub fn allow(input: Value) -> Self {
1066        PermissionResult::Allow {
1067            updated_input: input,
1068            updated_permissions: None,
1069        }
1070    }
1071
1072    /// Create an allow result with raw permissions (as JSON Values).
1073    ///
1074    /// Prefer using `allow_with_typed_permissions` for type safety.
1075    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
1076        PermissionResult::Allow {
1077            updated_input: input,
1078            updated_permissions: Some(permissions),
1079        }
1080    }
1081
1082    /// Create an allow result with typed permissions.
1083    ///
1084    /// This is the preferred way to grant permissions for "remember this decision"
1085    /// functionality.
1086    ///
1087    /// # Example
1088    /// ```
1089    /// use claude_codes::{Permission, PermissionResult};
1090    /// use serde_json::json;
1091    ///
1092    /// let result = PermissionResult::allow_with_typed_permissions(
1093    ///     json!({"command": "npm test"}),
1094    ///     vec![Permission::allow_tool("Bash", "npm test")],
1095    /// );
1096    /// ```
1097    pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
1098        let permission_values: Vec<Value> = permissions
1099            .into_iter()
1100            .filter_map(|p| serde_json::to_value(p).ok())
1101            .collect();
1102        PermissionResult::Allow {
1103            updated_input: input,
1104            updated_permissions: Some(permission_values),
1105        }
1106    }
1107
1108    /// Create a deny result
1109    pub fn deny(message: impl Into<String>) -> Self {
1110        PermissionResult::Deny {
1111            message: message.into(),
1112            interrupt: false,
1113        }
1114    }
1115
1116    /// Create a deny result that also interrupts the session
1117    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
1118        PermissionResult::Deny {
1119            message: message.into(),
1120            interrupt: true,
1121        }
1122    }
1123}
1124
1125/// Hook callback request
1126#[derive(Debug, Clone, Serialize, Deserialize)]
1127pub struct HookCallbackRequest {
1128    pub callback_id: String,
1129    pub input: Value,
1130    #[serde(skip_serializing_if = "Option::is_none")]
1131    pub tool_use_id: Option<String>,
1132}
1133
1134/// MCP message request
1135#[derive(Debug, Clone, Serialize, Deserialize)]
1136pub struct McpMessageRequest {
1137    pub server_name: String,
1138    pub message: Value,
1139}
1140
1141/// Initialize request (SDK -> CLI)
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1143pub struct InitializeRequest {
1144    #[serde(skip_serializing_if = "Option::is_none")]
1145    pub hooks: Option<Value>,
1146}
1147
1148/// Control response to CLI
1149///
1150/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
1151/// constructed directly for other control request types.
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub struct ControlResponse {
1154    /// The request ID this response corresponds to
1155    pub response: ControlResponsePayload,
1156}
1157
1158impl ControlResponse {
1159    /// Create a success response from a PermissionResult
1160    ///
1161    /// This is the preferred way to construct permission responses.
1162    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
1163        // Serialize the PermissionResult to Value for the response
1164        let response_value = serde_json::to_value(&result)
1165            .expect("PermissionResult serialization should never fail");
1166        ControlResponse {
1167            response: ControlResponsePayload::Success {
1168                request_id: request_id.to_string(),
1169                response: Some(response_value),
1170            },
1171        }
1172    }
1173
1174    /// Create a success response with the given payload (raw Value)
1175    pub fn success(request_id: &str, response_data: Value) -> Self {
1176        ControlResponse {
1177            response: ControlResponsePayload::Success {
1178                request_id: request_id.to_string(),
1179                response: Some(response_data),
1180            },
1181        }
1182    }
1183
1184    /// Create an empty success response (for acks)
1185    pub fn success_empty(request_id: &str) -> Self {
1186        ControlResponse {
1187            response: ControlResponsePayload::Success {
1188                request_id: request_id.to_string(),
1189                response: None,
1190            },
1191        }
1192    }
1193
1194    /// Create an error response
1195    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
1196        ControlResponse {
1197            response: ControlResponsePayload::Error {
1198                request_id: request_id.to_string(),
1199                error: error_message.into(),
1200            },
1201        }
1202    }
1203}
1204
1205/// Control response payload
1206#[derive(Debug, Clone, Serialize, Deserialize)]
1207#[serde(tag = "subtype", rename_all = "snake_case")]
1208pub enum ControlResponsePayload {
1209    Success {
1210        request_id: String,
1211        #[serde(skip_serializing_if = "Option::is_none")]
1212        response: Option<Value>,
1213    },
1214    Error {
1215        request_id: String,
1216        error: String,
1217    },
1218}
1219
1220/// Wrapper for outgoing control responses (includes type tag)
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222pub struct ControlResponseMessage {
1223    #[serde(rename = "type")]
1224    pub message_type: String,
1225    pub response: ControlResponsePayload,
1226}
1227
1228impl From<ControlResponse> for ControlResponseMessage {
1229    fn from(resp: ControlResponse) -> Self {
1230        ControlResponseMessage {
1231            message_type: "control_response".to_string(),
1232            response: resp.response,
1233        }
1234    }
1235}
1236
1237/// Wrapper for outgoing control requests (includes type tag)
1238#[derive(Debug, Clone, Serialize, Deserialize)]
1239pub struct ControlRequestMessage {
1240    #[serde(rename = "type")]
1241    pub message_type: String,
1242    pub request_id: String,
1243    pub request: ControlRequestPayload,
1244}
1245
1246impl ControlRequestMessage {
1247    /// Create an initialization request to send to CLI
1248    pub fn initialize(request_id: impl Into<String>) -> Self {
1249        ControlRequestMessage {
1250            message_type: "control_request".to_string(),
1251            request_id: request_id.into(),
1252            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1253        }
1254    }
1255
1256    /// Create an initialization request with hooks configuration
1257    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1258        ControlRequestMessage {
1259            message_type: "control_request".to_string(),
1260            request_id: request_id.into(),
1261            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1262        }
1263    }
1264}
1265
1266/// Usage information for the request
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct UsageInfo {
1269    pub input_tokens: u32,
1270    pub cache_creation_input_tokens: u32,
1271    pub cache_read_input_tokens: u32,
1272    pub output_tokens: u32,
1273    pub server_tool_use: ServerToolUse,
1274    pub service_tier: String,
1275}
1276
1277/// Server tool usage information
1278#[derive(Debug, Clone, Serialize, Deserialize)]
1279pub struct ServerToolUse {
1280    pub web_search_requests: u32,
1281}
1282
1283impl ClaudeInput {
1284    /// Create a simple text user message
1285    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
1286        ClaudeInput::User(UserMessage {
1287            message: MessageContent {
1288                role: "user".to_string(),
1289                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
1290            },
1291            session_id: Some(session_id),
1292        })
1293    }
1294
1295    /// Create a user message with content blocks
1296    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
1297        ClaudeInput::User(UserMessage {
1298            message: MessageContent {
1299                role: "user".to_string(),
1300                content: blocks,
1301            },
1302            session_id: Some(session_id),
1303        })
1304    }
1305
1306    /// Create a user message with an image and optional text
1307    /// Only supports JPEG, PNG, GIF, and WebP media types
1308    pub fn user_message_with_image(
1309        image_data: String,
1310        media_type: String,
1311        text: Option<String>,
1312        session_id: Uuid,
1313    ) -> Result<Self, String> {
1314        // Validate media type
1315        let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
1316
1317        if !valid_types.contains(&media_type.as_str()) {
1318            return Err(format!(
1319                "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
1320                media_type
1321            ));
1322        }
1323
1324        let mut blocks = vec![ContentBlock::Image(ImageBlock {
1325            source: ImageSource {
1326                source_type: "base64".to_string(),
1327                media_type,
1328                data: image_data,
1329            },
1330        })];
1331
1332        if let Some(text_content) = text {
1333            blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
1334        }
1335
1336        Ok(Self::user_message_blocks(blocks, session_id))
1337    }
1338}
1339
1340impl ClaudeOutput {
1341    /// Get the message type as a string
1342    pub fn message_type(&self) -> String {
1343        match self {
1344            ClaudeOutput::System(_) => "system".to_string(),
1345            ClaudeOutput::User(_) => "user".to_string(),
1346            ClaudeOutput::Assistant(_) => "assistant".to_string(),
1347            ClaudeOutput::Result(_) => "result".to_string(),
1348            ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
1349            ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
1350            ClaudeOutput::Error(_) => "error".to_string(),
1351        }
1352    }
1353
1354    /// Check if this is a control request (tool permission request)
1355    pub fn is_control_request(&self) -> bool {
1356        matches!(self, ClaudeOutput::ControlRequest(_))
1357    }
1358
1359    /// Check if this is a control response
1360    pub fn is_control_response(&self) -> bool {
1361        matches!(self, ClaudeOutput::ControlResponse(_))
1362    }
1363
1364    /// Check if this is an Anthropic API error
1365    pub fn is_api_error(&self) -> bool {
1366        matches!(self, ClaudeOutput::Error(_))
1367    }
1368
1369    /// Get the control request if this is one
1370    pub fn as_control_request(&self) -> Option<&ControlRequest> {
1371        match self {
1372            ClaudeOutput::ControlRequest(req) => Some(req),
1373            _ => None,
1374        }
1375    }
1376
1377    /// Get the Anthropic error if this is one
1378    ///
1379    /// # Example
1380    /// ```
1381    /// use claude_codes::ClaudeOutput;
1382    ///
1383    /// let json = r#"{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}"#;
1384    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1385    ///
1386    /// if let Some(err) = output.as_anthropic_error() {
1387    ///     if err.is_overloaded() {
1388    ///         println!("API is overloaded, retrying...");
1389    ///     }
1390    /// }
1391    /// ```
1392    pub fn as_anthropic_error(&self) -> Option<&AnthropicError> {
1393        match self {
1394            ClaudeOutput::Error(err) => Some(err),
1395            _ => None,
1396        }
1397    }
1398
1399    /// Check if this is a result with error
1400    pub fn is_error(&self) -> bool {
1401        matches!(self, ClaudeOutput::Result(r) if r.is_error)
1402    }
1403
1404    /// Check if this is an assistant message
1405    pub fn is_assistant_message(&self) -> bool {
1406        matches!(self, ClaudeOutput::Assistant(_))
1407    }
1408
1409    /// Check if this is a system message
1410    pub fn is_system_message(&self) -> bool {
1411        matches!(self, ClaudeOutput::System(_))
1412    }
1413
1414    /// Check if this is a system init message
1415    ///
1416    /// # Example
1417    /// ```
1418    /// use claude_codes::ClaudeOutput;
1419    ///
1420    /// let json = r#"{"type":"system","subtype":"init","session_id":"abc"}"#;
1421    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1422    /// assert!(output.is_system_init());
1423    /// ```
1424    pub fn is_system_init(&self) -> bool {
1425        matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1426    }
1427
1428    /// Get the session ID from any message type that has one.
1429    ///
1430    /// Returns the session ID from System, Assistant, or Result messages.
1431    /// Returns `None` for User, ControlRequest, and ControlResponse messages.
1432    ///
1433    /// # Example
1434    /// ```
1435    /// use claude_codes::ClaudeOutput;
1436    ///
1437    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1438    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1439    ///     "session_id":"my-session","total_cost_usd":0.01}"#;
1440    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1441    /// assert_eq!(output.session_id(), Some("my-session"));
1442    /// ```
1443    pub fn session_id(&self) -> Option<&str> {
1444        match self {
1445            ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1446            ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1447            ClaudeOutput::Result(res) => Some(&res.session_id),
1448            ClaudeOutput::User(_) => None,
1449            ClaudeOutput::ControlRequest(_) => None,
1450            ClaudeOutput::ControlResponse(_) => None,
1451            ClaudeOutput::Error(_) => None,
1452        }
1453    }
1454
1455    /// Get a specific tool use by name from an assistant message.
1456    ///
1457    /// Returns the first `ToolUseBlock` with the given name, or `None` if this
1458    /// is not an assistant message or doesn't contain the specified tool.
1459    ///
1460    /// # Example
1461    /// ```
1462    /// use claude_codes::ClaudeOutput;
1463    ///
1464    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1465    ///     "model":"claude-3","content":[{"type":"tool_use","id":"tu_1",
1466    ///     "name":"Bash","input":{"command":"ls"}}]},"session_id":"abc"}"#;
1467    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1468    ///
1469    /// if let Some(bash) = output.as_tool_use("Bash") {
1470    ///     assert_eq!(bash.name, "Bash");
1471    /// }
1472    /// ```
1473    pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1474        match self {
1475            ClaudeOutput::Assistant(ass) => {
1476                ass.message.content.iter().find_map(|block| match block {
1477                    ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1478                    _ => None,
1479                })
1480            }
1481            _ => None,
1482        }
1483    }
1484
1485    /// Get all tool uses from an assistant message.
1486    ///
1487    /// Returns an iterator over all `ToolUseBlock`s in the message, or an empty
1488    /// iterator if this is not an assistant message.
1489    ///
1490    /// # Example
1491    /// ```
1492    /// use claude_codes::ClaudeOutput;
1493    ///
1494    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1495    ///     "model":"claude-3","content":[
1496    ///         {"type":"tool_use","id":"tu_1","name":"Read","input":{"file_path":"/tmp/a"}},
1497    ///         {"type":"tool_use","id":"tu_2","name":"Write","input":{"file_path":"/tmp/b","content":"x"}}
1498    ///     ]},"session_id":"abc"}"#;
1499    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1500    ///
1501    /// let tools: Vec<_> = output.tool_uses().collect();
1502    /// assert_eq!(tools.len(), 2);
1503    /// ```
1504    pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1505        let content = match self {
1506            ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1507            _ => None,
1508        };
1509
1510        content
1511            .into_iter()
1512            .flat_map(|c| c.iter())
1513            .filter_map(|block| match block {
1514                ContentBlock::ToolUse(tu) => Some(tu),
1515                _ => None,
1516            })
1517    }
1518
1519    /// Get text content from an assistant message.
1520    ///
1521    /// Returns the concatenated text from all text blocks in the message,
1522    /// or `None` if this is not an assistant message or has no text content.
1523    ///
1524    /// # Example
1525    /// ```
1526    /// use claude_codes::ClaudeOutput;
1527    ///
1528    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1529    ///     "model":"claude-3","content":[{"type":"text","text":"Hello, world!"}]},
1530    ///     "session_id":"abc"}"#;
1531    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1532    /// assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1533    /// ```
1534    pub fn text_content(&self) -> Option<String> {
1535        match self {
1536            ClaudeOutput::Assistant(ass) => {
1537                let texts: Vec<&str> = ass
1538                    .message
1539                    .content
1540                    .iter()
1541                    .filter_map(|block| match block {
1542                        ContentBlock::Text(t) => Some(t.text.as_str()),
1543                        _ => None,
1544                    })
1545                    .collect();
1546
1547                if texts.is_empty() {
1548                    None
1549                } else {
1550                    Some(texts.join(""))
1551                }
1552            }
1553            _ => None,
1554        }
1555    }
1556
1557    /// Get the assistant message if this is one.
1558    ///
1559    /// # Example
1560    /// ```
1561    /// use claude_codes::ClaudeOutput;
1562    ///
1563    /// let json = r#"{"type":"assistant","message":{"id":"msg_1","role":"assistant",
1564    ///     "model":"claude-3","content":[]},"session_id":"abc"}"#;
1565    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1566    ///
1567    /// if let Some(assistant) = output.as_assistant() {
1568    ///     assert_eq!(assistant.message.model, "claude-3");
1569    /// }
1570    /// ```
1571    pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1572        match self {
1573            ClaudeOutput::Assistant(ass) => Some(ass),
1574            _ => None,
1575        }
1576    }
1577
1578    /// Get the result message if this is one.
1579    ///
1580    /// # Example
1581    /// ```
1582    /// use claude_codes::ClaudeOutput;
1583    ///
1584    /// let json = r#"{"type":"result","subtype":"success","is_error":false,
1585    ///     "duration_ms":100,"duration_api_ms":200,"num_turns":1,
1586    ///     "session_id":"abc","total_cost_usd":0.01}"#;
1587    /// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1588    ///
1589    /// if let Some(result) = output.as_result() {
1590    ///     assert!(!result.is_error);
1591    /// }
1592    /// ```
1593    pub fn as_result(&self) -> Option<&ResultMessage> {
1594        match self {
1595            ClaudeOutput::Result(res) => Some(res),
1596            _ => None,
1597        }
1598    }
1599
1600    /// Get the system message if this is one.
1601    pub fn as_system(&self) -> Option<&SystemMessage> {
1602        match self {
1603            ClaudeOutput::System(sys) => Some(sys),
1604            _ => None,
1605        }
1606    }
1607
1608    /// Parse a JSON string, handling potential ANSI escape codes and other prefixes
1609    /// This method will:
1610    /// 1. First try to parse as-is
1611    /// 2. If that fails, trim until it finds a '{' and try again
1612    pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1613        // First try to parse as-is
1614        match Self::parse_json(s) {
1615            Ok(output) => Ok(output),
1616            Err(first_error) => {
1617                // If that fails, look for the first '{' character
1618                if let Some(json_start) = s.find('{') {
1619                    let trimmed = &s[json_start..];
1620                    match Self::parse_json(trimmed) {
1621                        Ok(output) => Ok(output),
1622                        Err(_) => {
1623                            // Return the original error if both attempts fail
1624                            Err(first_error)
1625                        }
1626                    }
1627                } else {
1628                    Err(first_error)
1629                }
1630            }
1631        }
1632    }
1633
1634    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
1635    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1636        // First try to parse as a Value
1637        let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1638            raw_json: Value::String(s.to_string()),
1639            error_message: format!("Invalid JSON: {}", e),
1640        })?;
1641
1642        // Then try to parse that Value as ClaudeOutput
1643        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1644            raw_json: value,
1645            error_message: e.to_string(),
1646        })
1647    }
1648}
1649
1650#[cfg(test)]
1651mod tests {
1652    use super::*;
1653
1654    #[test]
1655    fn test_serialize_user_message() {
1656        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1657        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1658        let json = serde_json::to_string(&input).unwrap();
1659        assert!(json.contains("\"type\":\"user\""));
1660        assert!(json.contains("\"role\":\"user\""));
1661        assert!(json.contains("\"text\":\"Hello, Claude!\""));
1662        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1663    }
1664
1665    #[test]
1666    fn test_deserialize_assistant_message() {
1667        let json = r#"{
1668            "type": "assistant",
1669            "message": {
1670                "id": "msg_123",
1671                "role": "assistant",
1672                "model": "claude-3-sonnet",
1673                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1674            },
1675            "session_id": "123"
1676        }"#;
1677
1678        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1679        assert!(output.is_assistant_message());
1680    }
1681
1682    #[test]
1683    fn test_deserialize_result_message() {
1684        let json = r#"{
1685            "type": "result",
1686            "subtype": "success",
1687            "is_error": false,
1688            "duration_ms": 100,
1689            "duration_api_ms": 200,
1690            "num_turns": 1,
1691            "result": "Done",
1692            "session_id": "123",
1693            "total_cost_usd": 0.01,
1694            "permission_denials": []
1695        }"#;
1696
1697        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1698        assert!(!output.is_error());
1699    }
1700
1701    #[test]
1702    fn test_deserialize_result_with_permission_denials() {
1703        let json = r#"{
1704            "type": "result",
1705            "subtype": "success",
1706            "is_error": false,
1707            "duration_ms": 100,
1708            "duration_api_ms": 200,
1709            "num_turns": 2,
1710            "result": "Done",
1711            "session_id": "123",
1712            "total_cost_usd": 0.01,
1713            "permission_denials": [
1714                {
1715                    "tool_name": "Bash",
1716                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1717                    "tool_use_id": "toolu_123"
1718                }
1719            ]
1720        }"#;
1721
1722        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1723        if let ClaudeOutput::Result(result) = output {
1724            assert_eq!(result.permission_denials.len(), 1);
1725            assert_eq!(result.permission_denials[0].tool_name, "Bash");
1726            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1727            assert_eq!(
1728                result.permission_denials[0]
1729                    .tool_input
1730                    .get("command")
1731                    .unwrap(),
1732                "rm -rf /"
1733            );
1734        } else {
1735            panic!("Expected Result");
1736        }
1737    }
1738
1739    #[test]
1740    fn test_permission_denial_roundtrip() {
1741        let denial = PermissionDenial {
1742            tool_name: "Write".to_string(),
1743            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1744            tool_use_id: "toolu_456".to_string(),
1745        };
1746
1747        let json = serde_json::to_string(&denial).unwrap();
1748        assert!(json.contains("\"tool_name\":\"Write\""));
1749        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1750        assert!(json.contains("/etc/passwd"));
1751
1752        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1753        assert_eq!(parsed, denial);
1754    }
1755
1756    // ============================================================================
1757    // Control Protocol Tests
1758    // ============================================================================
1759
1760    #[test]
1761    fn test_deserialize_control_request_can_use_tool() {
1762        let json = r#"{
1763            "type": "control_request",
1764            "request_id": "perm-abc123",
1765            "request": {
1766                "subtype": "can_use_tool",
1767                "tool_name": "Write",
1768                "input": {
1769                    "file_path": "/home/user/hello.py",
1770                    "content": "print('hello')"
1771                },
1772                "permission_suggestions": [],
1773                "blocked_path": null
1774            }
1775        }"#;
1776
1777        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1778        assert!(output.is_control_request());
1779
1780        if let ClaudeOutput::ControlRequest(req) = output {
1781            assert_eq!(req.request_id, "perm-abc123");
1782            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1783                assert_eq!(perm_req.tool_name, "Write");
1784                assert_eq!(
1785                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1786                    "/home/user/hello.py"
1787                );
1788            } else {
1789                panic!("Expected CanUseTool payload");
1790            }
1791        } else {
1792            panic!("Expected ControlRequest");
1793        }
1794    }
1795
1796    #[test]
1797    fn test_deserialize_control_request_edit_tool_real() {
1798        // Real production message from Claude CLI
1799        let json = r#"{"type":"control_request","request_id":"f3cf357c-17d6-4eca-b498-dd17c7ac43dd","request":{"subtype":"can_use_tool","tool_name":"Edit","input":{"file_path":"/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs","old_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n    println!(\n        \"  {} Run: {} to re-authenticate\",\n        \"→\".bright_blue(),\n        \"claude-portal logout && claude-portal login\".bright_cyan()\n    );\n}","new_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n    println!(\n        \"  {} Run: {} to re-authenticate\",\n        \"→\".bright_blue(),\n        \"claude-portal --reauth\".bright_cyan()\n    );\n}","replace_all":false},"permission_suggestions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}],"tool_use_id":"toolu_015BDGtNiqNrRSJSDrWXNckW"}}"#;
1800
1801        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1802        assert!(output.is_control_request());
1803        assert_eq!(output.message_type(), "control_request");
1804
1805        if let ClaudeOutput::ControlRequest(req) = output {
1806            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1807            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1808                assert_eq!(perm_req.tool_name, "Edit");
1809                // Verify input contains the expected Edit fields
1810                assert_eq!(
1811                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1812                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1813                );
1814                assert!(perm_req.input.get("old_string").is_some());
1815                assert!(perm_req.input.get("new_string").is_some());
1816                assert!(!perm_req
1817                    .input
1818                    .get("replace_all")
1819                    .unwrap()
1820                    .as_bool()
1821                    .unwrap());
1822            } else {
1823                panic!("Expected CanUseTool payload");
1824            }
1825        } else {
1826            panic!("Expected ControlRequest");
1827        }
1828    }
1829
1830    #[test]
1831    fn test_tool_permission_request_allow() {
1832        let req = ToolPermissionRequest {
1833            tool_name: "Read".to_string(),
1834            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1835            permission_suggestions: vec![],
1836            blocked_path: None,
1837            decision_reason: None,
1838            tool_use_id: None,
1839        };
1840
1841        let response = req.allow("req-123");
1842        let message: ControlResponseMessage = response.into();
1843
1844        let json = serde_json::to_string(&message).unwrap();
1845        assert!(json.contains("\"type\":\"control_response\""));
1846        assert!(json.contains("\"subtype\":\"success\""));
1847        assert!(json.contains("\"request_id\":\"req-123\""));
1848        assert!(json.contains("\"behavior\":\"allow\""));
1849        assert!(json.contains("\"updatedInput\""));
1850    }
1851
1852    #[test]
1853    fn test_tool_permission_request_allow_with_modified_input() {
1854        let req = ToolPermissionRequest {
1855            tool_name: "Write".to_string(),
1856            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1857            permission_suggestions: vec![],
1858            blocked_path: None,
1859            decision_reason: None,
1860            tool_use_id: None,
1861        };
1862
1863        let modified_input = serde_json::json!({
1864            "file_path": "/tmp/safe/passwd",
1865            "content": "test"
1866        });
1867        let response = req.allow_with(modified_input, "req-456");
1868        let message: ControlResponseMessage = response.into();
1869
1870        let json = serde_json::to_string(&message).unwrap();
1871        assert!(json.contains("/tmp/safe/passwd"));
1872        assert!(!json.contains("/etc/passwd"));
1873    }
1874
1875    #[test]
1876    fn test_tool_permission_request_deny() {
1877        let req = ToolPermissionRequest {
1878            tool_name: "Bash".to_string(),
1879            input: serde_json::json!({"command": "sudo rm -rf /"}),
1880            permission_suggestions: vec![],
1881            blocked_path: None,
1882            decision_reason: None,
1883            tool_use_id: None,
1884        };
1885
1886        let response = req.deny("Dangerous command blocked", "req-789");
1887        let message: ControlResponseMessage = response.into();
1888
1889        let json = serde_json::to_string(&message).unwrap();
1890        assert!(json.contains("\"behavior\":\"deny\""));
1891        assert!(json.contains("Dangerous command blocked"));
1892        assert!(!json.contains("\"interrupt\":true"));
1893    }
1894
1895    #[test]
1896    fn test_tool_permission_request_deny_and_stop() {
1897        let req = ToolPermissionRequest {
1898            tool_name: "Bash".to_string(),
1899            input: serde_json::json!({"command": "rm -rf /"}),
1900            permission_suggestions: vec![],
1901            blocked_path: None,
1902            decision_reason: None,
1903            tool_use_id: None,
1904        };
1905
1906        let response = req.deny_and_stop("Security violation", "req-000");
1907        let message: ControlResponseMessage = response.into();
1908
1909        let json = serde_json::to_string(&message).unwrap();
1910        assert!(json.contains("\"behavior\":\"deny\""));
1911        assert!(json.contains("\"interrupt\":true"));
1912    }
1913
1914    #[test]
1915    fn test_permission_result_serialization() {
1916        // Test allow
1917        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1918        let json = serde_json::to_string(&allow).unwrap();
1919        assert!(json.contains("\"behavior\":\"allow\""));
1920        assert!(json.contains("\"updatedInput\""));
1921
1922        // Test deny
1923        let deny = PermissionResult::deny("Not allowed");
1924        let json = serde_json::to_string(&deny).unwrap();
1925        assert!(json.contains("\"behavior\":\"deny\""));
1926        assert!(json.contains("\"message\":\"Not allowed\""));
1927        assert!(!json.contains("\"interrupt\""));
1928
1929        // Test deny with interrupt
1930        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1931        let json = serde_json::to_string(&deny_stop).unwrap();
1932        assert!(json.contains("\"interrupt\":true"));
1933    }
1934
1935    #[test]
1936    fn test_control_request_message_initialize() {
1937        let init = ControlRequestMessage::initialize("init-1");
1938
1939        let json = serde_json::to_string(&init).unwrap();
1940        assert!(json.contains("\"type\":\"control_request\""));
1941        assert!(json.contains("\"request_id\":\"init-1\""));
1942        assert!(json.contains("\"subtype\":\"initialize\""));
1943    }
1944
1945    #[test]
1946    fn test_control_response_error() {
1947        let response = ControlResponse::error("req-err", "Something went wrong");
1948        let message: ControlResponseMessage = response.into();
1949
1950        let json = serde_json::to_string(&message).unwrap();
1951        assert!(json.contains("\"subtype\":\"error\""));
1952        assert!(json.contains("\"error\":\"Something went wrong\""));
1953    }
1954
1955    #[test]
1956    fn test_roundtrip_control_request() {
1957        // Test that we can serialize and deserialize control requests
1958        let original_json = r#"{
1959            "type": "control_request",
1960            "request_id": "test-123",
1961            "request": {
1962                "subtype": "can_use_tool",
1963                "tool_name": "Bash",
1964                "input": {"command": "ls -la"},
1965                "permission_suggestions": []
1966            }
1967        }"#;
1968
1969        // Parse as ClaudeOutput
1970        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1971
1972        // Serialize back and verify key parts are present
1973        let reserialized = serde_json::to_string(&output).unwrap();
1974        assert!(reserialized.contains("control_request"));
1975        assert!(reserialized.contains("test-123"));
1976        assert!(reserialized.contains("Bash"));
1977    }
1978
1979    #[test]
1980    fn test_permission_suggestions_parsing() {
1981        // Test that permission_suggestions deserialize correctly with real protocol format
1982        let json = r#"{
1983            "type": "control_request",
1984            "request_id": "perm-456",
1985            "request": {
1986                "subtype": "can_use_tool",
1987                "tool_name": "Bash",
1988                "input": {"command": "npm test"},
1989                "permission_suggestions": [
1990                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1991                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1992                ]
1993            }
1994        }"#;
1995
1996        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1997        if let ClaudeOutput::ControlRequest(req) = output {
1998            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1999                assert_eq!(perm_req.permission_suggestions.len(), 2);
2000                assert_eq!(
2001                    perm_req.permission_suggestions[0].suggestion_type,
2002                    "setMode"
2003                );
2004                assert_eq!(
2005                    perm_req.permission_suggestions[0].mode,
2006                    Some("acceptEdits".to_string())
2007                );
2008                assert_eq!(perm_req.permission_suggestions[0].destination, "session");
2009                assert_eq!(
2010                    perm_req.permission_suggestions[1].suggestion_type,
2011                    "setMode"
2012                );
2013                assert_eq!(
2014                    perm_req.permission_suggestions[1].mode,
2015                    Some("bypassPermissions".to_string())
2016                );
2017                assert_eq!(perm_req.permission_suggestions[1].destination, "project");
2018            } else {
2019                panic!("Expected CanUseTool payload");
2020            }
2021        } else {
2022            panic!("Expected ControlRequest");
2023        }
2024    }
2025
2026    #[test]
2027    fn test_permission_suggestion_set_mode_roundtrip() {
2028        let suggestion = PermissionSuggestion {
2029            suggestion_type: "setMode".to_string(),
2030            destination: "session".to_string(),
2031            mode: Some("acceptEdits".to_string()),
2032            behavior: None,
2033            rules: None,
2034        };
2035
2036        let json = serde_json::to_string(&suggestion).unwrap();
2037        assert!(json.contains("\"type\":\"setMode\""));
2038        assert!(json.contains("\"mode\":\"acceptEdits\""));
2039        assert!(json.contains("\"destination\":\"session\""));
2040        assert!(!json.contains("\"behavior\""));
2041        assert!(!json.contains("\"rules\""));
2042
2043        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
2044        assert_eq!(parsed, suggestion);
2045    }
2046
2047    #[test]
2048    fn test_permission_suggestion_add_rules_roundtrip() {
2049        let suggestion = PermissionSuggestion {
2050            suggestion_type: "addRules".to_string(),
2051            destination: "session".to_string(),
2052            mode: None,
2053            behavior: Some("allow".to_string()),
2054            rules: Some(vec![serde_json::json!({
2055                "toolName": "Read",
2056                "ruleContent": "//tmp/**"
2057            })]),
2058        };
2059
2060        let json = serde_json::to_string(&suggestion).unwrap();
2061        assert!(json.contains("\"type\":\"addRules\""));
2062        assert!(json.contains("\"behavior\":\"allow\""));
2063        assert!(json.contains("\"destination\":\"session\""));
2064        assert!(json.contains("\"rules\""));
2065        assert!(json.contains("\"toolName\":\"Read\""));
2066        assert!(!json.contains("\"mode\""));
2067
2068        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
2069        assert_eq!(parsed, suggestion);
2070    }
2071
2072    #[test]
2073    fn test_permission_suggestion_add_rules_from_real_json() {
2074        // Real production message from Claude CLI
2075        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
2076
2077        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
2078        assert_eq!(parsed.suggestion_type, "addRules");
2079        assert_eq!(parsed.destination, "session");
2080        assert_eq!(parsed.behavior, Some("allow".to_string()));
2081        assert!(parsed.rules.is_some());
2082        assert!(parsed.mode.is_none());
2083    }
2084
2085    // ============================================================================
2086    // System Message Subtype Tests
2087    // ============================================================================
2088
2089    #[test]
2090    fn test_system_message_init() {
2091        let json = r#"{
2092            "type": "system",
2093            "subtype": "init",
2094            "session_id": "test-session-123",
2095            "cwd": "/home/user/project",
2096            "model": "claude-sonnet-4",
2097            "tools": ["Bash", "Read", "Write"],
2098            "mcp_servers": []
2099        }"#;
2100
2101        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2102        if let ClaudeOutput::System(sys) = output {
2103            assert!(sys.is_init());
2104            assert!(!sys.is_status());
2105            assert!(!sys.is_compact_boundary());
2106
2107            let init = sys.as_init().expect("Should parse as init");
2108            assert_eq!(init.session_id, "test-session-123");
2109            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2110            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2111            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2112        } else {
2113            panic!("Expected System message");
2114        }
2115    }
2116
2117    #[test]
2118    fn test_system_message_status() {
2119        let json = r#"{
2120            "type": "system",
2121            "subtype": "status",
2122            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2123            "status": "compacting",
2124            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2125        }"#;
2126
2127        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2128        if let ClaudeOutput::System(sys) = output {
2129            assert!(sys.is_status());
2130            assert!(!sys.is_init());
2131
2132            let status = sys.as_status().expect("Should parse as status");
2133            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2134            assert_eq!(status.status, Some("compacting".to_string()));
2135            assert_eq!(
2136                status.uuid,
2137                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2138            );
2139        } else {
2140            panic!("Expected System message");
2141        }
2142    }
2143
2144    #[test]
2145    fn test_system_message_status_null() {
2146        let json = r#"{
2147            "type": "system",
2148            "subtype": "status",
2149            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2150            "status": null,
2151            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2152        }"#;
2153
2154        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2155        if let ClaudeOutput::System(sys) = output {
2156            let status = sys.as_status().expect("Should parse as status");
2157            assert_eq!(status.status, None);
2158        } else {
2159            panic!("Expected System message");
2160        }
2161    }
2162
2163    #[test]
2164    fn test_system_message_compact_boundary() {
2165        let json = r#"{
2166            "type": "system",
2167            "subtype": "compact_boundary",
2168            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2169            "compact_metadata": {
2170                "pre_tokens": 155285,
2171                "trigger": "auto"
2172            },
2173            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2174        }"#;
2175
2176        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2177        if let ClaudeOutput::System(sys) = output {
2178            assert!(sys.is_compact_boundary());
2179            assert!(!sys.is_init());
2180            assert!(!sys.is_status());
2181
2182            let compact = sys
2183                .as_compact_boundary()
2184                .expect("Should parse as compact_boundary");
2185            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2186            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2187            assert_eq!(compact.compact_metadata.trigger, "auto");
2188        } else {
2189            panic!("Expected System message");
2190        }
2191    }
2192
2193    // ============================================================================
2194    // Helper Method Tests
2195    // ============================================================================
2196
2197    #[test]
2198    fn test_is_system_init() {
2199        let init_json = r#"{
2200            "type": "system",
2201            "subtype": "init",
2202            "session_id": "test-session"
2203        }"#;
2204        let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
2205        assert!(output.is_system_init());
2206
2207        let status_json = r#"{
2208            "type": "system",
2209            "subtype": "status",
2210            "session_id": "test-session"
2211        }"#;
2212        let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
2213        assert!(!output.is_system_init());
2214    }
2215
2216    #[test]
2217    fn test_session_id() {
2218        // Result message
2219        let result_json = r#"{
2220            "type": "result",
2221            "subtype": "success",
2222            "is_error": false,
2223            "duration_ms": 100,
2224            "duration_api_ms": 200,
2225            "num_turns": 1,
2226            "session_id": "result-session",
2227            "total_cost_usd": 0.01
2228        }"#;
2229        let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2230        assert_eq!(output.session_id(), Some("result-session"));
2231
2232        // Assistant message
2233        let assistant_json = r#"{
2234            "type": "assistant",
2235            "message": {
2236                "id": "msg_1",
2237                "role": "assistant",
2238                "model": "claude-3",
2239                "content": []
2240            },
2241            "session_id": "assistant-session"
2242        }"#;
2243        let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2244        assert_eq!(output.session_id(), Some("assistant-session"));
2245
2246        // System message
2247        let system_json = r#"{
2248            "type": "system",
2249            "subtype": "init",
2250            "session_id": "system-session"
2251        }"#;
2252        let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
2253        assert_eq!(output.session_id(), Some("system-session"));
2254    }
2255
2256    #[test]
2257    fn test_as_tool_use() {
2258        let json = r#"{
2259            "type": "assistant",
2260            "message": {
2261                "id": "msg_1",
2262                "role": "assistant",
2263                "model": "claude-3",
2264                "content": [
2265                    {"type": "text", "text": "Let me run that command."},
2266                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
2267                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
2268                ]
2269            },
2270            "session_id": "abc"
2271        }"#;
2272        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2273
2274        // Find Bash tool
2275        let bash = output.as_tool_use("Bash");
2276        assert!(bash.is_some());
2277        assert_eq!(bash.unwrap().id, "tu_1");
2278
2279        // Find Read tool
2280        let read = output.as_tool_use("Read");
2281        assert!(read.is_some());
2282        assert_eq!(read.unwrap().id, "tu_2");
2283
2284        // Non-existent tool
2285        assert!(output.as_tool_use("Write").is_none());
2286
2287        // Not an assistant message
2288        let result_json = r#"{
2289            "type": "result",
2290            "subtype": "success",
2291            "is_error": false,
2292            "duration_ms": 100,
2293            "duration_api_ms": 200,
2294            "num_turns": 1,
2295            "session_id": "abc",
2296            "total_cost_usd": 0.01
2297        }"#;
2298        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2299        assert!(result.as_tool_use("Bash").is_none());
2300    }
2301
2302    #[test]
2303    fn test_tool_uses() {
2304        let json = r#"{
2305            "type": "assistant",
2306            "message": {
2307                "id": "msg_1",
2308                "role": "assistant",
2309                "model": "claude-3",
2310                "content": [
2311                    {"type": "text", "text": "Running commands..."},
2312                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
2313                    {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
2314                    {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
2315                ]
2316            },
2317            "session_id": "abc"
2318        }"#;
2319        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2320
2321        let tools: Vec<_> = output.tool_uses().collect();
2322        assert_eq!(tools.len(), 3);
2323        assert_eq!(tools[0].name, "Bash");
2324        assert_eq!(tools[1].name, "Read");
2325        assert_eq!(tools[2].name, "Write");
2326    }
2327
2328    #[test]
2329    fn test_text_content() {
2330        // Single text block
2331        let json = r#"{
2332            "type": "assistant",
2333            "message": {
2334                "id": "msg_1",
2335                "role": "assistant",
2336                "model": "claude-3",
2337                "content": [{"type": "text", "text": "Hello, world!"}]
2338            },
2339            "session_id": "abc"
2340        }"#;
2341        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2342        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
2343
2344        // Multiple text blocks
2345        let json = r#"{
2346            "type": "assistant",
2347            "message": {
2348                "id": "msg_1",
2349                "role": "assistant",
2350                "model": "claude-3",
2351                "content": [
2352                    {"type": "text", "text": "Hello, "},
2353                    {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
2354                    {"type": "text", "text": "world!"}
2355                ]
2356            },
2357            "session_id": "abc"
2358        }"#;
2359        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2360        assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
2361
2362        // No text blocks
2363        let json = r#"{
2364            "type": "assistant",
2365            "message": {
2366                "id": "msg_1",
2367                "role": "assistant",
2368                "model": "claude-3",
2369                "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
2370            },
2371            "session_id": "abc"
2372        }"#;
2373        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2374        assert_eq!(output.text_content(), None);
2375
2376        // Not an assistant message
2377        let json = r#"{
2378            "type": "result",
2379            "subtype": "success",
2380            "is_error": false,
2381            "duration_ms": 100,
2382            "duration_api_ms": 200,
2383            "num_turns": 1,
2384            "session_id": "abc",
2385            "total_cost_usd": 0.01
2386        }"#;
2387        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2388        assert_eq!(output.text_content(), None);
2389    }
2390
2391    #[test]
2392    fn test_as_assistant() {
2393        let json = r#"{
2394            "type": "assistant",
2395            "message": {
2396                "id": "msg_1",
2397                "role": "assistant",
2398                "model": "claude-sonnet-4",
2399                "content": []
2400            },
2401            "session_id": "abc"
2402        }"#;
2403        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2404
2405        let assistant = output.as_assistant();
2406        assert!(assistant.is_some());
2407        assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
2408
2409        // Not an assistant
2410        let result_json = r#"{
2411            "type": "result",
2412            "subtype": "success",
2413            "is_error": false,
2414            "duration_ms": 100,
2415            "duration_api_ms": 200,
2416            "num_turns": 1,
2417            "session_id": "abc",
2418            "total_cost_usd": 0.01
2419        }"#;
2420        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2421        assert!(result.as_assistant().is_none());
2422    }
2423
2424    #[test]
2425    fn test_as_result() {
2426        let json = r#"{
2427            "type": "result",
2428            "subtype": "success",
2429            "is_error": false,
2430            "duration_ms": 100,
2431            "duration_api_ms": 200,
2432            "num_turns": 5,
2433            "session_id": "abc",
2434            "total_cost_usd": 0.05
2435        }"#;
2436        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2437
2438        let result = output.as_result();
2439        assert!(result.is_some());
2440        assert_eq!(result.unwrap().num_turns, 5);
2441        assert_eq!(result.unwrap().total_cost_usd, 0.05);
2442
2443        // Not a result
2444        let assistant_json = r#"{
2445            "type": "assistant",
2446            "message": {
2447                "id": "msg_1",
2448                "role": "assistant",
2449                "model": "claude-3",
2450                "content": []
2451            },
2452            "session_id": "abc"
2453        }"#;
2454        let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2455        assert!(assistant.as_result().is_none());
2456    }
2457
2458    #[test]
2459    fn test_as_system() {
2460        let json = r#"{
2461            "type": "system",
2462            "subtype": "init",
2463            "session_id": "abc",
2464            "model": "claude-3"
2465        }"#;
2466        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2467
2468        let system = output.as_system();
2469        assert!(system.is_some());
2470        assert!(system.unwrap().is_init());
2471
2472        // Not a system message
2473        let result_json = r#"{
2474            "type": "result",
2475            "subtype": "success",
2476            "is_error": false,
2477            "duration_ms": 100,
2478            "duration_api_ms": 200,
2479            "num_turns": 1,
2480            "session_id": "abc",
2481            "total_cost_usd": 0.01
2482        }"#;
2483        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2484        assert!(result.as_system().is_none());
2485    }
2486
2487    // ============================================================================
2488    // ResultMessage Errors Field Tests
2489    // ============================================================================
2490
2491    #[test]
2492    fn test_deserialize_result_message_with_errors() {
2493        let json = r#"{
2494            "type": "result",
2495            "subtype": "error_during_execution",
2496            "duration_ms": 0,
2497            "duration_api_ms": 0,
2498            "is_error": true,
2499            "num_turns": 0,
2500            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2501            "total_cost_usd": 0,
2502            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2503            "permission_denials": []
2504        }"#;
2505
2506        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2507        assert!(output.is_error());
2508
2509        if let ClaudeOutput::Result(res) = output {
2510            assert!(res.is_error);
2511            assert_eq!(res.errors.len(), 1);
2512            assert!(res.errors[0].contains("No conversation found"));
2513        } else {
2514            panic!("Expected Result message");
2515        }
2516    }
2517
2518    #[test]
2519    fn test_deserialize_result_message_errors_defaults_empty() {
2520        // Test that errors field defaults to empty Vec when not present
2521        let json = r#"{
2522            "type": "result",
2523            "subtype": "success",
2524            "is_error": false,
2525            "duration_ms": 100,
2526            "duration_api_ms": 200,
2527            "num_turns": 1,
2528            "session_id": "123",
2529            "total_cost_usd": 0.01
2530        }"#;
2531
2532        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2533        if let ClaudeOutput::Result(res) = output {
2534            assert!(res.errors.is_empty());
2535        } else {
2536            panic!("Expected Result message");
2537        }
2538    }
2539
2540    #[test]
2541    fn test_result_message_errors_roundtrip() {
2542        let json = r#"{
2543            "type": "result",
2544            "subtype": "error_during_execution",
2545            "is_error": true,
2546            "duration_ms": 0,
2547            "duration_api_ms": 0,
2548            "num_turns": 0,
2549            "session_id": "test-session",
2550            "total_cost_usd": 0.0,
2551            "errors": ["Error 1", "Error 2"]
2552        }"#;
2553
2554        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2555        let reserialized = serde_json::to_string(&output).unwrap();
2556
2557        // Verify the errors are preserved
2558        assert!(reserialized.contains("Error 1"));
2559        assert!(reserialized.contains("Error 2"));
2560    }
2561
2562    // ============================================================================
2563    // Permission Builder Tests
2564    // ============================================================================
2565
2566    #[test]
2567    fn test_permission_allow_tool() {
2568        let perm = Permission::allow_tool("Bash", "npm test");
2569
2570        assert_eq!(perm.permission_type, "addRules");
2571        assert_eq!(perm.destination, "session");
2572        assert_eq!(perm.behavior, Some("allow".to_string()));
2573        assert!(perm.mode.is_none());
2574
2575        let rules = perm.rules.unwrap();
2576        assert_eq!(rules.len(), 1);
2577        assert_eq!(rules[0].tool_name, "Bash");
2578        assert_eq!(rules[0].rule_content, "npm test");
2579    }
2580
2581    #[test]
2582    fn test_permission_allow_tool_with_destination() {
2583        let perm = Permission::allow_tool_with_destination("Read", "/tmp/**", "project");
2584
2585        assert_eq!(perm.permission_type, "addRules");
2586        assert_eq!(perm.destination, "project");
2587        assert_eq!(perm.behavior, Some("allow".to_string()));
2588
2589        let rules = perm.rules.unwrap();
2590        assert_eq!(rules[0].tool_name, "Read");
2591        assert_eq!(rules[0].rule_content, "/tmp/**");
2592    }
2593
2594    #[test]
2595    fn test_permission_set_mode() {
2596        let perm = Permission::set_mode("acceptEdits", "session");
2597
2598        assert_eq!(perm.permission_type, "setMode");
2599        assert_eq!(perm.destination, "session");
2600        assert_eq!(perm.mode, Some("acceptEdits".to_string()));
2601        assert!(perm.behavior.is_none());
2602        assert!(perm.rules.is_none());
2603    }
2604
2605    #[test]
2606    fn test_permission_serialization() {
2607        let perm = Permission::allow_tool("Bash", "npm test");
2608        let json = serde_json::to_string(&perm).unwrap();
2609
2610        assert!(json.contains("\"type\":\"addRules\""));
2611        assert!(json.contains("\"destination\":\"session\""));
2612        assert!(json.contains("\"behavior\":\"allow\""));
2613        assert!(json.contains("\"toolName\":\"Bash\""));
2614        assert!(json.contains("\"ruleContent\":\"npm test\""));
2615    }
2616
2617    #[test]
2618    fn test_permission_from_suggestion_set_mode() {
2619        let suggestion = PermissionSuggestion {
2620            suggestion_type: "setMode".to_string(),
2621            destination: "session".to_string(),
2622            mode: Some("acceptEdits".to_string()),
2623            behavior: None,
2624            rules: None,
2625        };
2626
2627        let perm = Permission::from_suggestion(&suggestion);
2628
2629        assert_eq!(perm.permission_type, "setMode");
2630        assert_eq!(perm.destination, "session");
2631        assert_eq!(perm.mode, Some("acceptEdits".to_string()));
2632    }
2633
2634    #[test]
2635    fn test_permission_from_suggestion_add_rules() {
2636        let suggestion = PermissionSuggestion {
2637            suggestion_type: "addRules".to_string(),
2638            destination: "session".to_string(),
2639            mode: None,
2640            behavior: Some("allow".to_string()),
2641            rules: Some(vec![serde_json::json!({
2642                "toolName": "Read",
2643                "ruleContent": "/tmp/**"
2644            })]),
2645        };
2646
2647        let perm = Permission::from_suggestion(&suggestion);
2648
2649        assert_eq!(perm.permission_type, "addRules");
2650        assert_eq!(perm.behavior, Some("allow".to_string()));
2651
2652        let rules = perm.rules.unwrap();
2653        assert_eq!(rules.len(), 1);
2654        assert_eq!(rules[0].tool_name, "Read");
2655        assert_eq!(rules[0].rule_content, "/tmp/**");
2656    }
2657
2658    #[test]
2659    fn test_permission_result_allow_with_typed_permissions() {
2660        let result = PermissionResult::allow_with_typed_permissions(
2661            serde_json::json!({"command": "npm test"}),
2662            vec![Permission::allow_tool("Bash", "npm test")],
2663        );
2664
2665        let json = serde_json::to_string(&result).unwrap();
2666        assert!(json.contains("\"behavior\":\"allow\""));
2667        assert!(json.contains("\"updatedPermissions\""));
2668        assert!(json.contains("\"toolName\":\"Bash\""));
2669    }
2670
2671    #[test]
2672    fn test_tool_permission_request_allow_and_remember() {
2673        let req = ToolPermissionRequest {
2674            tool_name: "Bash".to_string(),
2675            input: serde_json::json!({"command": "npm test"}),
2676            permission_suggestions: vec![],
2677            blocked_path: None,
2678            decision_reason: None,
2679            tool_use_id: None,
2680        };
2681
2682        let response =
2683            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
2684        let message: ControlResponseMessage = response.into();
2685        let json = serde_json::to_string(&message).unwrap();
2686
2687        assert!(json.contains("\"type\":\"control_response\""));
2688        assert!(json.contains("\"behavior\":\"allow\""));
2689        assert!(json.contains("\"updatedPermissions\""));
2690        assert!(json.contains("\"toolName\":\"Bash\""));
2691    }
2692
2693    #[test]
2694    fn test_tool_permission_request_allow_and_remember_suggestion() {
2695        let req = ToolPermissionRequest {
2696            tool_name: "Bash".to_string(),
2697            input: serde_json::json!({"command": "npm test"}),
2698            permission_suggestions: vec![PermissionSuggestion {
2699                suggestion_type: "setMode".to_string(),
2700                destination: "session".to_string(),
2701                mode: Some("acceptEdits".to_string()),
2702                behavior: None,
2703                rules: None,
2704            }],
2705            blocked_path: None,
2706            decision_reason: None,
2707            tool_use_id: None,
2708        };
2709
2710        let response = req.allow_and_remember_suggestion("req-123");
2711        assert!(response.is_some());
2712
2713        let message: ControlResponseMessage = response.unwrap().into();
2714        let json = serde_json::to_string(&message).unwrap();
2715
2716        assert!(json.contains("\"type\":\"setMode\""));
2717        assert!(json.contains("\"mode\":\"acceptEdits\""));
2718    }
2719
2720    #[test]
2721    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
2722        let req = ToolPermissionRequest {
2723            tool_name: "Bash".to_string(),
2724            input: serde_json::json!({"command": "npm test"}),
2725            permission_suggestions: vec![], // No suggestions
2726            blocked_path: None,
2727            decision_reason: None,
2728            tool_use_id: None,
2729        };
2730
2731        let response = req.allow_and_remember_suggestion("req-123");
2732        assert!(response.is_none());
2733    }
2734
2735    // ============================================================================
2736    // Anthropic Error Tests
2737    // ============================================================================
2738
2739    #[test]
2740    fn test_deserialize_anthropic_error() {
2741        let json = r#"{
2742            "type": "error",
2743            "error": {
2744                "type": "api_error",
2745                "message": "Internal server error"
2746            },
2747            "request_id": "req_011CXPC6BqUogB959LWEf52X"
2748        }"#;
2749
2750        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2751        assert!(output.is_api_error());
2752        assert_eq!(output.message_type(), "error");
2753
2754        if let ClaudeOutput::Error(err) = output {
2755            assert_eq!(err.error.error_type, "api_error");
2756            assert_eq!(err.error.message, "Internal server error");
2757            assert_eq!(
2758                err.request_id,
2759                Some("req_011CXPC6BqUogB959LWEf52X".to_string())
2760            );
2761            assert!(err.is_server_error());
2762            assert!(!err.is_overloaded());
2763        } else {
2764            panic!("Expected Error variant");
2765        }
2766    }
2767
2768    #[test]
2769    fn test_deserialize_anthropic_overloaded_error() {
2770        let json = r#"{
2771            "type": "error",
2772            "error": {
2773                "type": "overloaded_error",
2774                "message": "Overloaded"
2775            }
2776        }"#;
2777
2778        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2779
2780        if let ClaudeOutput::Error(err) = output {
2781            assert!(err.is_overloaded());
2782            assert!(!err.is_server_error());
2783            assert!(err.request_id.is_none());
2784        } else {
2785            panic!("Expected Error variant");
2786        }
2787    }
2788
2789    #[test]
2790    fn test_deserialize_anthropic_rate_limit_error() {
2791        let json = r#"{
2792            "type": "error",
2793            "error": {
2794                "type": "rate_limit_error",
2795                "message": "Rate limit exceeded"
2796            },
2797            "request_id": "req_456"
2798        }"#;
2799
2800        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2801
2802        if let ClaudeOutput::Error(err) = output {
2803            assert!(err.is_rate_limited());
2804            assert!(!err.is_overloaded());
2805            assert!(!err.is_server_error());
2806        } else {
2807            panic!("Expected Error variant");
2808        }
2809    }
2810
2811    #[test]
2812    fn test_deserialize_anthropic_authentication_error() {
2813        let json = r#"{
2814            "type": "error",
2815            "error": {
2816                "type": "authentication_error",
2817                "message": "Invalid API key"
2818            }
2819        }"#;
2820
2821        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2822
2823        if let ClaudeOutput::Error(err) = output {
2824            assert!(err.is_authentication_error());
2825        } else {
2826            panic!("Expected Error variant");
2827        }
2828    }
2829
2830    #[test]
2831    fn test_deserialize_anthropic_invalid_request_error() {
2832        let json = r#"{
2833            "type": "error",
2834            "error": {
2835                "type": "invalid_request_error",
2836                "message": "Invalid request body"
2837            }
2838        }"#;
2839
2840        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2841
2842        if let ClaudeOutput::Error(err) = output {
2843            assert!(err.is_invalid_request());
2844        } else {
2845            panic!("Expected Error variant");
2846        }
2847    }
2848
2849    #[test]
2850    fn test_anthropic_error_as_helper() {
2851        let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
2852        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2853
2854        let err = output.as_anthropic_error();
2855        assert!(err.is_some());
2856        assert_eq!(err.unwrap().error.error_type, "api_error");
2857
2858        // Non-error should return None
2859        let result_json = r#"{
2860            "type": "result",
2861            "subtype": "success",
2862            "is_error": false,
2863            "duration_ms": 100,
2864            "duration_api_ms": 200,
2865            "num_turns": 1,
2866            "session_id": "abc",
2867            "total_cost_usd": 0.01
2868        }"#;
2869        let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2870        assert!(result.as_anthropic_error().is_none());
2871    }
2872
2873    #[test]
2874    fn test_anthropic_error_roundtrip() {
2875        let error = AnthropicError {
2876            error: AnthropicErrorDetails {
2877                error_type: "api_error".to_string(),
2878                message: "Test error".to_string(),
2879            },
2880            request_id: Some("req_123".to_string()),
2881        };
2882
2883        let json = serde_json::to_string(&error).unwrap();
2884        assert!(json.contains("\"type\":\"api_error\""));
2885        assert!(json.contains("\"message\":\"Test error\""));
2886        assert!(json.contains("\"request_id\":\"req_123\""));
2887
2888        let parsed: AnthropicError = serde_json::from_str(&json).unwrap();
2889        assert_eq!(parsed, error);
2890    }
2891
2892    #[test]
2893    fn test_anthropic_error_session_id_is_none() {
2894        let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
2895        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2896        assert!(output.session_id().is_none());
2897    }
2898}