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}