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