claude_code_agent_sdk/
client.rs

1//! ClaudeClient for bidirectional streaming interactions with hook support
2
3use futures::stream::Stream;
4use std::pin::Pin;
5use std::sync::Arc;
6use tokio::io::AsyncWriteExt;
7use tokio::sync::Mutex;
8
9use crate::errors::{ClaudeError, Result};
10use crate::internal::message_parser::MessageParser;
11use crate::internal::query_full::QueryFull;
12use crate::internal::transport::subprocess::QueryPrompt;
13use crate::internal::transport::{SubprocessTransport, Transport};
14use crate::types::config::{ClaudeAgentOptions, PermissionMode};
15use crate::types::hooks::HookEvent;
16use crate::types::messages::{Message, UserContentBlock};
17
18/// Client for bidirectional streaming interactions with Claude
19///
20/// This client provides the same functionality as Python's ClaudeSDKClient,
21/// supporting bidirectional communication, streaming responses, and dynamic
22/// control over the Claude session.
23///
24/// # Example
25///
26/// ```no_run
27/// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
28/// use futures::StreamExt;
29///
30/// #[tokio::main]
31/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
32///     let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
33///
34///     // Connect to Claude
35///     client.connect().await?;
36///
37///     // Send a query
38///     client.query("Hello Claude!").await?;
39///
40///     // Receive response as a stream
41///     {
42///         let mut stream = client.receive_response();
43///         while let Some(message) = stream.next().await {
44///             println!("Received: {:?}", message?);
45///         }
46///     }
47///
48///     // Disconnect
49///     client.disconnect().await?;
50///     Ok(())
51/// }
52/// ```
53pub struct ClaudeClient {
54    options: ClaudeAgentOptions,
55    query: Option<Arc<Mutex<QueryFull>>>,
56    connected: bool,
57}
58
59impl ClaudeClient {
60    /// Create a new ClaudeClient
61    ///
62    /// # Arguments
63    ///
64    /// * `options` - Configuration options for the Claude client
65    ///
66    /// # Example
67    ///
68    /// ```no_run
69    /// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
70    ///
71    /// let client = ClaudeClient::new(ClaudeAgentOptions::default());
72    /// ```
73    pub fn new(options: ClaudeAgentOptions) -> Self {
74        Self {
75            options,
76            query: None,
77            connected: false,
78        }
79    }
80
81    /// Create a new ClaudeClient with early validation
82    ///
83    /// Unlike `new()`, this validates the configuration eagerly by attempting
84    /// to create the transport. This catches issues like invalid working directory
85    /// or missing CLI before `connect()` is called.
86    ///
87    /// # Arguments
88    ///
89    /// * `options` - Configuration options for the Claude client
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if:
94    /// - The working directory does not exist or is not a directory
95    /// - Claude CLI cannot be found
96    ///
97    /// # Example
98    ///
99    /// ```no_run
100    /// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
101    ///
102    /// let client = ClaudeClient::try_new(ClaudeAgentOptions::default())?;
103    /// # Ok::<(), claude_agent_sdk_rs::ClaudeError>(())
104    /// ```
105    pub fn try_new(options: ClaudeAgentOptions) -> Result<Self> {
106        // Validate by attempting to create transport (but don't keep it)
107        let prompt = QueryPrompt::Streaming;
108        let _ = SubprocessTransport::new(prompt, options.clone())?;
109
110        Ok(Self {
111            options,
112            query: None,
113            connected: false,
114        })
115    }
116
117    /// Connect to Claude (analogous to Python's __aenter__)
118    ///
119    /// This establishes the connection to the Claude Code CLI and initializes
120    /// the bidirectional communication channel.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if:
125    /// - Claude CLI cannot be found or started
126    /// - The initialization handshake fails
127    /// - Hook registration fails
128    pub async fn connect(&mut self) -> Result<()> {
129        if self.connected {
130            return Ok(());
131        }
132
133        // Create transport in streaming mode (no initial prompt)
134        let prompt = QueryPrompt::Streaming;
135        let mut transport = SubprocessTransport::new(prompt, self.options.clone())?;
136
137        // Don't send initial prompt - we'll use query() for that
138        transport.connect().await?;
139
140        // Extract stdin for direct access (avoids transport lock deadlock)
141        let stdin = Arc::clone(&transport.stdin);
142
143        // Create Query with hooks
144        let mut query = QueryFull::new(Box::new(transport));
145        query.set_stdin(stdin);
146
147        // Extract SDK MCP servers from options
148        let sdk_mcp_servers =
149            if let crate::types::mcp::McpServers::Dict(servers_dict) = &self.options.mcp_servers {
150                servers_dict
151                    .iter()
152                    .filter_map(|(name, config)| {
153                        if let crate::types::mcp::McpServerConfig::Sdk(sdk_config) = config {
154                            Some((name.clone(), sdk_config.clone()))
155                        } else {
156                            None
157                        }
158                    })
159                    .collect()
160            } else {
161                std::collections::HashMap::new()
162            };
163        query.set_sdk_mcp_servers(sdk_mcp_servers).await;
164
165        // Convert hooks to internal format
166        let hooks = self.options.hooks.as_ref().map(|hooks_map| {
167            hooks_map
168                .iter()
169                .map(|(event, matchers)| {
170                    let event_name = match event {
171                        HookEvent::PreToolUse => "PreToolUse",
172                        HookEvent::PostToolUse => "PostToolUse",
173                        HookEvent::UserPromptSubmit => "UserPromptSubmit",
174                        HookEvent::Stop => "Stop",
175                        HookEvent::SubagentStop => "SubagentStop",
176                        HookEvent::PreCompact => "PreCompact",
177                    };
178                    (event_name.to_string(), matchers.clone())
179                })
180                .collect()
181        });
182
183        // Start reading messages in background FIRST
184        // This must happen before initialize() because initialize()
185        // sends a control request and waits for response
186        query.start().await?;
187
188        // Initialize with hooks (sends control request)
189        query.initialize(hooks).await?;
190
191        self.query = Some(Arc::new(Mutex::new(query)));
192        self.connected = true;
193
194        Ok(())
195    }
196
197    /// Send a query to Claude
198    ///
199    /// This sends a new user prompt to Claude. Claude will remember the context
200    /// of previous queries within the same session.
201    ///
202    /// # Arguments
203    ///
204    /// * `prompt` - The user prompt to send
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the client is not connected or if sending fails.
209    ///
210    /// # Example
211    ///
212    /// ```no_run
213    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
214    /// # #[tokio::main]
215    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
217    /// # client.connect().await?;
218    /// client.query("What is 2 + 2?").await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    pub async fn query(&mut self, prompt: impl Into<String>) -> Result<()> {
223        self.query_with_session(prompt, "default").await
224    }
225
226    /// Send a query to Claude with a specific session ID
227    ///
228    /// This sends a new user prompt to Claude. Different session IDs maintain
229    /// separate conversation contexts.
230    ///
231    /// # Arguments
232    ///
233    /// * `prompt` - The user prompt to send
234    /// * `session_id` - Session identifier for the conversation
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if the client is not connected or if sending fails.
239    ///
240    /// # Example
241    ///
242    /// ```no_run
243    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
244    /// # #[tokio::main]
245    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
246    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
247    /// # client.connect().await?;
248    /// // Separate conversation contexts
249    /// client.query_with_session("First question", "session-1").await?;
250    /// client.query_with_session("Different question", "session-2").await?;
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub async fn query_with_session(
255        &mut self,
256        prompt: impl Into<String>,
257        session_id: impl Into<String>,
258    ) -> Result<()> {
259        let query = self.query.as_ref().ok_or_else(|| {
260            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
261        })?;
262
263        let prompt_str = prompt.into();
264        let session_id_str = session_id.into();
265
266        // Format as JSON message for stream-json input format
267        let user_message = serde_json::json!({
268            "type": "user",
269            "message": {
270                "role": "user",
271                "content": prompt_str
272            },
273            "session_id": session_id_str
274        });
275
276        let message_str = serde_json::to_string(&user_message).map_err(|e| {
277            ClaudeError::Transport(format!("Failed to serialize user message: {}", e))
278        })?;
279
280        // Write directly to stdin (bypasses transport lock)
281        let query_guard = query.lock().await;
282        let stdin = query_guard.stdin.clone();
283        drop(query_guard);
284
285        if let Some(stdin_arc) = stdin {
286            let mut stdin_guard = stdin_arc.lock().await;
287            if let Some(ref mut stdin_stream) = *stdin_guard {
288                stdin_stream
289                    .write_all(message_str.as_bytes())
290                    .await
291                    .map_err(|e| ClaudeError::Transport(format!("Failed to write query: {}", e)))?;
292                stdin_stream.write_all(b"\n").await.map_err(|e| {
293                    ClaudeError::Transport(format!("Failed to write newline: {}", e))
294                })?;
295                stdin_stream
296                    .flush()
297                    .await
298                    .map_err(|e| ClaudeError::Transport(format!("Failed to flush: {}", e)))?;
299            } else {
300                return Err(ClaudeError::Transport("stdin not available".to_string()));
301            }
302        } else {
303            return Err(ClaudeError::Transport("stdin not set".to_string()));
304        }
305
306        Ok(())
307    }
308
309    /// Send a query with structured content blocks (supports images)
310    ///
311    /// This method enables multimodal queries in bidirectional streaming mode.
312    /// Use it to send images alongside text for vision-related tasks.
313    ///
314    /// # Arguments
315    ///
316    /// * `content` - A vector of content blocks (text and/or images)
317    ///
318    /// # Errors
319    ///
320    /// Returns an error if:
321    /// - The content vector is empty (must include at least one text or image block)
322    /// - The client is not connected (call `connect()` first)
323    /// - Sending the message fails
324    ///
325    /// # Example
326    ///
327    /// ```no_run
328    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, UserContentBlock};
329    /// # #[tokio::main]
330    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
331    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
332    /// # client.connect().await?;
333    /// let base64_data = "iVBORw0KGgo..."; // base64 encoded image
334    /// client.query_with_content(vec![
335    ///     UserContentBlock::text("What's in this image?"),
336    ///     UserContentBlock::image_base64("image/png", base64_data)?,
337    /// ]).await?;
338    /// # Ok(())
339    /// # }
340    /// ```
341    pub async fn query_with_content(
342        &mut self,
343        content: impl Into<Vec<UserContentBlock>>,
344    ) -> Result<()> {
345        self.query_with_content_and_session(content, "default")
346            .await
347    }
348
349    /// Send a query with structured content blocks and a specific session ID
350    ///
351    /// This method enables multimodal queries with session management for
352    /// maintaining separate conversation contexts.
353    ///
354    /// # Arguments
355    ///
356    /// * `content` - A vector of content blocks (text and/or images)
357    /// * `session_id` - Session identifier for the conversation
358    ///
359    /// # Errors
360    ///
361    /// Returns an error if:
362    /// - The content vector is empty (must include at least one text or image block)
363    /// - The client is not connected (call `connect()` first)
364    /// - Sending the message fails
365    ///
366    /// # Example
367    ///
368    /// ```no_run
369    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, UserContentBlock};
370    /// # #[tokio::main]
371    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
372    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
373    /// # client.connect().await?;
374    /// client.query_with_content_and_session(
375    ///     vec![
376    ///         UserContentBlock::text("Analyze this chart"),
377    ///         UserContentBlock::image_url("https://example.com/chart.png"),
378    ///     ],
379    ///     "analysis-session",
380    /// ).await?;
381    /// # Ok(())
382    /// # }
383    /// ```
384    pub async fn query_with_content_and_session(
385        &mut self,
386        content: impl Into<Vec<UserContentBlock>>,
387        session_id: impl Into<String>,
388    ) -> Result<()> {
389        let query = self.query.as_ref().ok_or_else(|| {
390            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
391        })?;
392
393        let content_blocks: Vec<UserContentBlock> = content.into();
394        UserContentBlock::validate_content(&content_blocks)?;
395
396        let session_id_str = session_id.into();
397
398        // Format as JSON message for stream-json input format
399        // Content is an array of content blocks, not a simple string
400        let user_message = serde_json::json!({
401            "type": "user",
402            "message": {
403                "role": "user",
404                "content": content_blocks
405            },
406            "session_id": session_id_str
407        });
408
409        let message_str = serde_json::to_string(&user_message).map_err(|e| {
410            ClaudeError::Transport(format!("Failed to serialize user message: {}", e))
411        })?;
412
413        // Write directly to stdin (bypasses transport lock)
414        let query_guard = query.lock().await;
415        let stdin = query_guard.stdin.clone();
416        drop(query_guard);
417
418        if let Some(stdin_arc) = stdin {
419            let mut stdin_guard = stdin_arc.lock().await;
420            if let Some(ref mut stdin_stream) = *stdin_guard {
421                stdin_stream
422                    .write_all(message_str.as_bytes())
423                    .await
424                    .map_err(|e| ClaudeError::Transport(format!("Failed to write query: {}", e)))?;
425                stdin_stream.write_all(b"\n").await.map_err(|e| {
426                    ClaudeError::Transport(format!("Failed to write newline: {}", e))
427                })?;
428                stdin_stream
429                    .flush()
430                    .await
431                    .map_err(|e| ClaudeError::Transport(format!("Failed to flush: {}", e)))?;
432            } else {
433                return Err(ClaudeError::Transport("stdin not available".to_string()));
434            }
435        } else {
436            return Err(ClaudeError::Transport("stdin not set".to_string()));
437        }
438
439        Ok(())
440    }
441
442    /// Receive all messages as a stream (continuous)
443    ///
444    /// This method returns a stream that yields all messages from Claude
445    /// indefinitely until the stream is closed or an error occurs.
446    ///
447    /// Use this when you want to process all messages, including multiple
448    /// responses and system events.
449    ///
450    /// # Returns
451    ///
452    /// A stream of `Result<Message>` that continues until the connection closes.
453    ///
454    /// # Example
455    ///
456    /// ```no_run
457    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
458    /// # use futures::StreamExt;
459    /// # #[tokio::main]
460    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
461    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
462    /// # client.connect().await?;
463    /// # client.query("Hello").await?;
464    /// let mut stream = client.receive_messages();
465    /// while let Some(message) = stream.next().await {
466    ///     println!("Received: {:?}", message?);
467    /// }
468    /// # Ok(())
469    /// # }
470    /// ```
471    pub fn receive_messages(&self) -> Pin<Box<dyn Stream<Item = Result<Message>> + Send + '_>> {
472        let query = match &self.query {
473            Some(q) => Arc::clone(q),
474            None => {
475                return Box::pin(futures::stream::once(async {
476                    Err(ClaudeError::InvalidConfig(
477                        "Client not connected. Call connect() first.".to_string(),
478                    ))
479                }));
480            }
481        };
482
483        Box::pin(async_stream::stream! {
484            let rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>> = {
485                let query_guard = query.lock().await;
486                Arc::clone(&query_guard.message_rx)
487            };
488
489            loop {
490                let message = {
491                    let mut rx_guard = rx.lock().await;
492                    rx_guard.recv().await
493                };
494
495                match message {
496                    Some(json) => {
497                        match MessageParser::parse(json) {
498                            Ok(msg) => yield Ok(msg),
499                            Err(e) => {
500                                eprintln!("Failed to parse message: {}", e);
501                                yield Err(e);
502                            }
503                        }
504                    }
505                    None => break,
506                }
507            }
508        })
509    }
510
511    /// Receive messages until a ResultMessage
512    ///
513    /// This method returns a stream that yields messages until it encounters
514    /// a `ResultMessage`, which signals the completion of a Claude response.
515    ///
516    /// This is the most common pattern for handling Claude responses, as it
517    /// processes one complete "turn" of the conversation.
518    ///
519    /// # Returns
520    ///
521    /// A stream of `Result<Message>` that ends when a ResultMessage is received.
522    ///
523    /// # Example
524    ///
525    /// ```no_run
526    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, Message};
527    /// # use futures::StreamExt;
528    /// # #[tokio::main]
529    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
530    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
531    /// # client.connect().await?;
532    /// # client.query("Hello").await?;
533    /// let mut stream = client.receive_response();
534    /// while let Some(message) = stream.next().await {
535    ///     match message? {
536    ///         Message::Assistant(msg) => println!("Assistant: {:?}", msg),
537    ///         Message::Result(result) => {
538    ///             println!("Done! Cost: ${:?}", result.total_cost_usd);
539    ///             break;
540    ///         }
541    ///         _ => {}
542    ///     }
543    /// }
544    /// # Ok(())
545    /// # }
546    /// ```
547    pub fn receive_response(&self) -> Pin<Box<dyn Stream<Item = Result<Message>> + Send + '_>> {
548        let query = match &self.query {
549            Some(q) => Arc::clone(q),
550            None => {
551                return Box::pin(futures::stream::once(async {
552                    Err(ClaudeError::InvalidConfig(
553                        "Client not connected. Call connect() first.".to_string(),
554                    ))
555                }));
556            }
557        };
558
559        Box::pin(async_stream::stream! {
560            let rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>> = {
561                let query_guard = query.lock().await;
562                Arc::clone(&query_guard.message_rx)
563            };
564
565            loop {
566                let message = {
567                    let mut rx_guard = rx.lock().await;
568                    rx_guard.recv().await
569                };
570
571                match message {
572                    Some(json) => {
573                        match MessageParser::parse(json) {
574                            Ok(msg) => {
575                                let is_result = matches!(msg, Message::Result(_));
576                                yield Ok(msg);
577                                if is_result {
578                                    break;
579                                }
580                            }
581                            Err(e) => {
582                                eprintln!("Failed to parse message: {}", e);
583                                yield Err(e);
584                            }
585                        }
586                    }
587                    None => break,
588                }
589            }
590        })
591    }
592
593    /// Send an interrupt signal to stop the current Claude operation
594    ///
595    /// This is analogous to Python's `client.interrupt()`.
596    ///
597    /// # Errors
598    ///
599    /// Returns an error if the client is not connected or if sending fails.
600    pub async fn interrupt(&self) -> Result<()> {
601        let query = self.query.as_ref().ok_or_else(|| {
602            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
603        })?;
604
605        let query_guard = query.lock().await;
606        query_guard.interrupt().await
607    }
608
609    /// Change the permission mode dynamically
610    ///
611    /// This is analogous to Python's `client.set_permission_mode()`.
612    ///
613    /// # Arguments
614    ///
615    /// * `mode` - The new permission mode to set
616    ///
617    /// # Errors
618    ///
619    /// Returns an error if the client is not connected or if sending fails.
620    pub async fn set_permission_mode(&self, mode: PermissionMode) -> Result<()> {
621        let query = self.query.as_ref().ok_or_else(|| {
622            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
623        })?;
624
625        let query_guard = query.lock().await;
626        query_guard.set_permission_mode(mode).await
627    }
628
629    /// Change the AI model dynamically
630    ///
631    /// This is analogous to Python's `client.set_model()`.
632    ///
633    /// # Arguments
634    ///
635    /// * `model` - The new model name, or None to use default
636    ///
637    /// # Errors
638    ///
639    /// Returns an error if the client is not connected or if sending fails.
640    pub async fn set_model(&self, model: Option<&str>) -> Result<()> {
641        let query = self.query.as_ref().ok_or_else(|| {
642            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
643        })?;
644
645        let query_guard = query.lock().await;
646        query_guard.set_model(model).await
647    }
648
649    /// Rewind tracked files to their state at a specific user message.
650    ///
651    /// This is analogous to Python's `client.rewind_files()`.
652    ///
653    /// # Requirements
654    ///
655    /// - `enable_file_checkpointing=true` in options to track file changes
656    /// - `extra_args={"replay-user-messages": None}` to receive UserMessage
657    ///   objects with `uuid` in the response stream
658    ///
659    /// # Arguments
660    ///
661    /// * `user_message_id` - UUID of the user message to rewind to. This should be
662    ///   the `uuid` field from a `UserMessage` received during the conversation.
663    ///
664    /// # Errors
665    ///
666    /// Returns an error if the client is not connected or if sending fails.
667    ///
668    /// # Example
669    ///
670    /// ```no_run
671    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, Message};
672    /// # use std::collections::HashMap;
673    /// # #[tokio::main]
674    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
675    /// let options = ClaudeAgentOptions::builder()
676    ///     .enable_file_checkpointing(true)
677    ///     .extra_args(HashMap::from([("replay-user-messages".to_string(), None)]))
678    ///     .build();
679    /// let mut client = ClaudeClient::new(options);
680    /// client.connect().await?;
681    ///
682    /// client.query("Make some changes to my files").await?;
683    /// let mut checkpoint_id = None;
684    /// {
685    ///     let mut stream = client.receive_response();
686    ///     use futures::StreamExt;
687    ///     while let Some(Ok(msg)) = stream.next().await {
688    ///         if let Message::User(user_msg) = &msg {
689    ///             if let Some(uuid) = &user_msg.uuid {
690    ///                 checkpoint_id = Some(uuid.clone());
691    ///             }
692    ///         }
693    ///     }
694    /// }
695    ///
696    /// // Later, rewind to that point
697    /// if let Some(id) = checkpoint_id {
698    ///     client.rewind_files(&id).await?;
699    /// }
700    /// # Ok(())
701    /// # }
702    /// ```
703    pub async fn rewind_files(&self, user_message_id: &str) -> Result<()> {
704        let query = self.query.as_ref().ok_or_else(|| {
705            ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
706        })?;
707
708        let query_guard = query.lock().await;
709        query_guard.rewind_files(user_message_id).await
710    }
711
712    /// Get server initialization info including available commands and output styles
713    ///
714    /// Returns initialization information from the Claude Code server including:
715    /// - Available commands (slash commands, system commands, etc.)
716    /// - Current and available output styles
717    /// - Server capabilities
718    ///
719    /// This is analogous to Python's `client.get_server_info()`.
720    ///
721    /// # Returns
722    ///
723    /// Dictionary with server info, or None if not connected
724    ///
725    /// # Example
726    ///
727    /// ```no_run
728    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
729    /// # #[tokio::main]
730    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
731    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
732    /// # client.connect().await?;
733    /// if let Some(info) = client.get_server_info().await {
734    ///     println!("Commands available: {}", info.get("commands").map(|c| c.as_array().map(|a| a.len()).unwrap_or(0)).unwrap_or(0));
735    ///     println!("Output style: {:?}", info.get("output_style"));
736    /// }
737    /// # Ok(())
738    /// # }
739    /// ```
740    pub async fn get_server_info(&self) -> Option<serde_json::Value> {
741        let query = self.query.as_ref()?;
742        let query_guard = query.lock().await;
743        query_guard.get_initialization_result().await
744    }
745
746    /// Start a new session by switching to a different session ID
747    ///
748    /// This is a convenience method that creates a new conversation context.
749    /// It's equivalent to calling `query_with_session()` with a new session ID.
750    ///
751    /// To completely clear memory and start fresh, use `ClaudeAgentOptions::builder().fork_session(true).build()`
752    /// when creating a new client.
753    ///
754    /// # Arguments
755    ///
756    /// * `session_id` - The new session ID to use
757    /// * `prompt` - Initial message for the new session
758    ///
759    /// # Errors
760    ///
761    /// Returns an error if the client is not connected or if sending fails.
762    ///
763    /// # Example
764    ///
765    /// ```no_run
766    /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
767    /// # #[tokio::main]
768    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
769    /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
770    /// # client.connect().await?;
771    /// // First conversation
772    /// client.query("Hello").await?;
773    ///
774    /// // Start new conversation with different context
775    /// client.new_session("session-2", "Tell me about Rust").await?;
776    /// # Ok(())
777    /// # }
778    /// ```
779    pub async fn new_session(
780        &mut self,
781        session_id: impl Into<String>,
782        prompt: impl Into<String>,
783    ) -> Result<()> {
784        self.query_with_session(prompt, session_id).await
785    }
786
787    /// Disconnect from Claude (analogous to Python's __aexit__)
788    ///
789    /// This cleanly shuts down the connection to Claude Code CLI.
790    ///
791    /// # Errors
792    ///
793    /// Returns an error if disconnection fails.
794    pub async fn disconnect(&mut self) -> Result<()> {
795        if !self.connected {
796            return Ok(());
797        }
798
799        if let Some(query) = self.query.take() {
800            // Close stdin first (using direct access) to signal CLI to exit
801            // This will cause the background task to finish and release transport lock
802            let query_guard = query.lock().await;
803            if let Some(ref stdin_arc) = query_guard.stdin {
804                let mut stdin_guard = stdin_arc.lock().await;
805                if let Some(mut stdin_stream) = stdin_guard.take() {
806                    let _ = stdin_stream.shutdown().await;
807                }
808            }
809            let transport = Arc::clone(&query_guard.transport);
810            drop(query_guard);
811
812            // Give background task a moment to finish reading and release lock
813            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
814
815            let mut transport_guard = transport.lock().await;
816            transport_guard.close().await?;
817        }
818
819        self.connected = false;
820        Ok(())
821    }
822}
823
824impl Drop for ClaudeClient {
825    fn drop(&mut self) {
826        // Note: We can't run async code in Drop, so we can't guarantee clean shutdown
827        // Users should call disconnect() explicitly
828        if self.connected {
829            eprintln!(
830                "Warning: ClaudeClient dropped without calling disconnect(). Resources may not be cleaned up properly."
831            );
832        }
833    }
834}