Skip to main content

claude_codes/
cli.rs

1//! Builder pattern for configuring and launching the Claude CLI process.
2//!
3//! This module provides [`ClaudeCliBuilder`] for constructing Claude CLI commands
4//! with the correct flags for JSON streaming mode. The builder automatically configures:
5//!
6//! - JSON streaming input/output formats
7//! - Non-interactive print mode
8//! - Verbose output for proper streaming
9//! - OAuth token and API key environment variables for authentication
10//!
11
12#[cfg(feature = "async-client")]
13use crate::error::{Error, Result};
14use log::debug;
15use std::path::PathBuf;
16use std::process::Stdio;
17use uuid::Uuid;
18
19/// Permission mode for Claude CLI
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PermissionMode {
22    AcceptEdits,
23    BypassPermissions,
24    Default,
25    Delegate,
26    DontAsk,
27    Plan,
28}
29
30impl PermissionMode {
31    /// Get the CLI string representation
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            PermissionMode::AcceptEdits => "acceptEdits",
35            PermissionMode::BypassPermissions => "bypassPermissions",
36            PermissionMode::Default => "default",
37            PermissionMode::Delegate => "delegate",
38            PermissionMode::DontAsk => "dontAsk",
39            PermissionMode::Plan => "plan",
40        }
41    }
42}
43
44/// Input format for Claude CLI
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum InputFormat {
47    Text,
48    StreamJson,
49}
50
51impl InputFormat {
52    /// Get the CLI string representation
53    pub fn as_str(&self) -> &'static str {
54        match self {
55            InputFormat::Text => "text",
56            InputFormat::StreamJson => "stream-json",
57        }
58    }
59}
60
61/// Output format for Claude CLI
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum OutputFormat {
64    Text,
65    Json,
66    StreamJson,
67}
68
69impl OutputFormat {
70    /// Get the CLI string representation
71    pub fn as_str(&self) -> &'static str {
72        match self {
73            OutputFormat::Text => "text",
74            OutputFormat::Json => "json",
75            OutputFormat::StreamJson => "stream-json",
76        }
77    }
78}
79
80/// Comprehensive enum of all Claude CLI flags.
81///
82/// This enum represents every flag available in the Claude CLI (`claude --help`).
83/// Each variant carries the appropriate data type for its flag value.
84///
85/// Use `as_flag()` to get the CLI flag string (e.g., `"--model"`),
86/// or `to_args()` to get the complete flag + value as CLI arguments.
87///
88/// # Example
89/// ```
90/// use claude_codes::CliFlag;
91///
92/// let flag = CliFlag::Model("sonnet".to_string());
93/// assert_eq!(flag.as_flag(), "--model");
94/// assert_eq!(flag.to_args(), vec!["--model", "sonnet"]);
95/// ```
96#[derive(Debug, Clone)]
97pub enum CliFlag {
98    /// Additional directories to allow tool access to
99    AddDir(Vec<PathBuf>),
100    /// Agent for the current session
101    Agent(String),
102    /// JSON object defining custom agents
103    Agents(String),
104    /// Enable bypassing all permission checks as an option
105    AllowDangerouslySkipPermissions,
106    /// Tool names to allow (e.g. "Bash(git:*) Edit")
107    AllowedTools(Vec<String>),
108    /// Append to the default system prompt
109    AppendSystemPrompt(String),
110    /// Beta headers for API requests (API key users only)
111    Betas(Vec<String>),
112    /// Enable Claude in Chrome integration
113    Chrome,
114    /// Continue the most recent conversation
115    Continue,
116    /// Bypass all permission checks
117    DangerouslySkipPermissions,
118    /// Enable debug mode with optional category filter
119    Debug(Option<String>),
120    /// Write debug logs to a specific file path
121    DebugFile(PathBuf),
122    /// Disable all skills/slash commands
123    DisableSlashCommands,
124    /// Tool names to deny (e.g. "Bash(git:*) Edit")
125    DisallowedTools(Vec<String>),
126    /// Automatic fallback model when default is overloaded
127    FallbackModel(String),
128    /// File resources to download at startup (format: file_id:relative_path)
129    File(Vec<String>),
130    /// Create a new session ID when resuming instead of reusing original
131    ForkSession,
132    /// Resume a session linked to a PR
133    FromPr(Option<String>),
134    /// Include partial message chunks as they arrive
135    IncludePartialMessages,
136    /// Input format (text or stream-json)
137    InputFormat(InputFormat),
138    /// JSON Schema for structured output validation
139    JsonSchema(String),
140    /// Maximum dollar amount for API calls
141    MaxBudgetUsd(f64),
142    /// Load MCP servers from JSON files or strings
143    McpConfig(Vec<String>),
144    /// Enable MCP debug mode (deprecated, use Debug instead)
145    McpDebug,
146    /// Model for the current session
147    Model(String),
148    /// Disable Claude in Chrome integration
149    NoChrome,
150    /// Disable session persistence
151    NoSessionPersistence,
152    /// Output format (text, json, or stream-json)
153    OutputFormat(OutputFormat),
154    /// Permission mode for the session
155    PermissionMode(PermissionMode),
156    /// Tool for handling permission prompts (e.g., "stdio")
157    PermissionPromptTool(String),
158    /// Load plugins from directories
159    PluginDir(Vec<PathBuf>),
160    /// Print response and exit
161    Print,
162    /// Re-emit user messages from stdin back on stdout
163    ReplayUserMessages,
164    /// Resume a conversation by session ID
165    Resume(Option<String>),
166    /// Use a specific session ID (UUID or tagged ID)
167    SessionId(String),
168    /// Comma-separated list of setting sources (user, project, local)
169    SettingSources(String),
170    /// Path to settings JSON file or JSON string
171    Settings(String),
172    /// Only use MCP servers from --mcp-config
173    StrictMcpConfig,
174    /// System prompt for the session
175    SystemPrompt(String),
176    /// Specify available tools from the built-in set
177    Tools(Vec<String>),
178    /// Override verbose mode setting
179    Verbose,
180}
181
182impl CliFlag {
183    /// Get the CLI flag string (e.g., `"--model"`)
184    pub fn as_flag(&self) -> &'static str {
185        match self {
186            CliFlag::AddDir(_) => "--add-dir",
187            CliFlag::Agent(_) => "--agent",
188            CliFlag::Agents(_) => "--agents",
189            CliFlag::AllowDangerouslySkipPermissions => "--allow-dangerously-skip-permissions",
190            CliFlag::AllowedTools(_) => "--allowed-tools",
191            CliFlag::AppendSystemPrompt(_) => "--append-system-prompt",
192            CliFlag::Betas(_) => "--betas",
193            CliFlag::Chrome => "--chrome",
194            CliFlag::Continue => "--continue",
195            CliFlag::DangerouslySkipPermissions => "--dangerously-skip-permissions",
196            CliFlag::Debug(_) => "--debug",
197            CliFlag::DebugFile(_) => "--debug-file",
198            CliFlag::DisableSlashCommands => "--disable-slash-commands",
199            CliFlag::DisallowedTools(_) => "--disallowed-tools",
200            CliFlag::FallbackModel(_) => "--fallback-model",
201            CliFlag::File(_) => "--file",
202            CliFlag::ForkSession => "--fork-session",
203            CliFlag::FromPr(_) => "--from-pr",
204            CliFlag::IncludePartialMessages => "--include-partial-messages",
205            CliFlag::InputFormat(_) => "--input-format",
206            CliFlag::JsonSchema(_) => "--json-schema",
207            CliFlag::MaxBudgetUsd(_) => "--max-budget-usd",
208            CliFlag::McpConfig(_) => "--mcp-config",
209            CliFlag::McpDebug => "--mcp-debug",
210            CliFlag::Model(_) => "--model",
211            CliFlag::NoChrome => "--no-chrome",
212            CliFlag::NoSessionPersistence => "--no-session-persistence",
213            CliFlag::OutputFormat(_) => "--output-format",
214            CliFlag::PermissionMode(_) => "--permission-mode",
215            CliFlag::PermissionPromptTool(_) => "--permission-prompt-tool",
216            CliFlag::PluginDir(_) => "--plugin-dir",
217            CliFlag::Print => "--print",
218            CliFlag::ReplayUserMessages => "--replay-user-messages",
219            CliFlag::Resume(_) => "--resume",
220            CliFlag::SessionId(_) => "--session-id",
221            CliFlag::SettingSources(_) => "--setting-sources",
222            CliFlag::Settings(_) => "--settings",
223            CliFlag::StrictMcpConfig => "--strict-mcp-config",
224            CliFlag::SystemPrompt(_) => "--system-prompt",
225            CliFlag::Tools(_) => "--tools",
226            CliFlag::Verbose => "--verbose",
227        }
228    }
229
230    /// Convert this flag into CLI arguments (flag + value)
231    pub fn to_args(&self) -> Vec<String> {
232        let flag = self.as_flag().to_string();
233        match self {
234            // Boolean flags (no value)
235            CliFlag::AllowDangerouslySkipPermissions
236            | CliFlag::Chrome
237            | CliFlag::Continue
238            | CliFlag::DangerouslySkipPermissions
239            | CliFlag::DisableSlashCommands
240            | CliFlag::ForkSession
241            | CliFlag::IncludePartialMessages
242            | CliFlag::McpDebug
243            | CliFlag::NoChrome
244            | CliFlag::NoSessionPersistence
245            | CliFlag::Print
246            | CliFlag::ReplayUserMessages
247            | CliFlag::StrictMcpConfig
248            | CliFlag::Verbose => vec![flag],
249
250            // Optional value flags
251            CliFlag::Debug(filter) => match filter {
252                Some(f) => vec![flag, f.clone()],
253                None => vec![flag],
254            },
255            CliFlag::FromPr(value) | CliFlag::Resume(value) => match value {
256                Some(v) => vec![flag, v.clone()],
257                None => vec![flag],
258            },
259
260            // Single string value flags
261            CliFlag::Agent(v)
262            | CliFlag::Agents(v)
263            | CliFlag::AppendSystemPrompt(v)
264            | CliFlag::FallbackModel(v)
265            | CliFlag::JsonSchema(v)
266            | CliFlag::Model(v)
267            | CliFlag::PermissionPromptTool(v)
268            | CliFlag::SessionId(v)
269            | CliFlag::SettingSources(v)
270            | CliFlag::Settings(v)
271            | CliFlag::SystemPrompt(v) => vec![flag, v.clone()],
272
273            // Format flags
274            CliFlag::InputFormat(f) => vec![flag, f.as_str().to_string()],
275            CliFlag::OutputFormat(f) => vec![flag, f.as_str().to_string()],
276            CliFlag::PermissionMode(m) => vec![flag, m.as_str().to_string()],
277
278            // Numeric flags
279            CliFlag::MaxBudgetUsd(amount) => vec![flag, amount.to_string()],
280
281            // Path flags
282            CliFlag::DebugFile(p) => vec![flag, p.to_string_lossy().to_string()],
283
284            // Multi-value string flags
285            CliFlag::AllowedTools(items)
286            | CliFlag::Betas(items)
287            | CliFlag::DisallowedTools(items)
288            | CliFlag::File(items)
289            | CliFlag::McpConfig(items)
290            | CliFlag::Tools(items) => {
291                let mut args = vec![flag];
292                args.extend(items.clone());
293                args
294            }
295
296            // Multi-value path flags
297            CliFlag::AddDir(paths) | CliFlag::PluginDir(paths) => {
298                let mut args = vec![flag];
299                args.extend(paths.iter().map(|p| p.to_string_lossy().to_string()));
300                args
301            }
302        }
303    }
304
305    /// Returns all CLI flag names with their flag strings.
306    ///
307    /// Useful for enumerating available options in a UI or for validation.
308    ///
309    /// # Example
310    /// ```
311    /// use claude_codes::CliFlag;
312    ///
313    /// for (name, flag) in CliFlag::all_flags() {
314    ///     println!("{}: {}", name, flag);
315    /// }
316    /// ```
317    pub fn all_flags() -> Vec<(&'static str, &'static str)> {
318        vec![
319            ("AddDir", "--add-dir"),
320            ("Agent", "--agent"),
321            ("Agents", "--agents"),
322            (
323                "AllowDangerouslySkipPermissions",
324                "--allow-dangerously-skip-permissions",
325            ),
326            ("AllowedTools", "--allowed-tools"),
327            ("AppendSystemPrompt", "--append-system-prompt"),
328            ("Betas", "--betas"),
329            ("Chrome", "--chrome"),
330            ("Continue", "--continue"),
331            (
332                "DangerouslySkipPermissions",
333                "--dangerously-skip-permissions",
334            ),
335            ("Debug", "--debug"),
336            ("DebugFile", "--debug-file"),
337            ("DisableSlashCommands", "--disable-slash-commands"),
338            ("DisallowedTools", "--disallowed-tools"),
339            ("FallbackModel", "--fallback-model"),
340            ("File", "--file"),
341            ("ForkSession", "--fork-session"),
342            ("FromPr", "--from-pr"),
343            ("IncludePartialMessages", "--include-partial-messages"),
344            ("InputFormat", "--input-format"),
345            ("JsonSchema", "--json-schema"),
346            ("MaxBudgetUsd", "--max-budget-usd"),
347            ("McpConfig", "--mcp-config"),
348            ("McpDebug", "--mcp-debug"),
349            ("Model", "--model"),
350            ("NoChrome", "--no-chrome"),
351            ("NoSessionPersistence", "--no-session-persistence"),
352            ("OutputFormat", "--output-format"),
353            ("PermissionMode", "--permission-mode"),
354            ("PermissionPromptTool", "--permission-prompt-tool"),
355            ("PluginDir", "--plugin-dir"),
356            ("Print", "--print"),
357            ("ReplayUserMessages", "--replay-user-messages"),
358            ("Resume", "--resume"),
359            ("SessionId", "--session-id"),
360            ("SettingSources", "--setting-sources"),
361            ("Settings", "--settings"),
362            ("StrictMcpConfig", "--strict-mcp-config"),
363            ("SystemPrompt", "--system-prompt"),
364            ("Tools", "--tools"),
365            ("Verbose", "--verbose"),
366        ]
367    }
368}
369
370/// Builder for creating Claude CLI commands in JSON streaming mode
371///
372/// This builder automatically configures Claude to use:
373/// - `--print` mode for non-interactive operation
374/// - `--output-format stream-json` for streaming JSON responses
375/// - `--input-format stream-json` for JSON input
376/// - `--replay-user-messages` to echo back user messages
377#[derive(Debug, Clone)]
378pub struct ClaudeCliBuilder {
379    command: PathBuf,
380    prompt: Option<String>,
381    debug: Option<String>,
382    verbose: bool,
383    dangerously_skip_permissions: bool,
384    allowed_tools: Vec<String>,
385    disallowed_tools: Vec<String>,
386    mcp_config: Vec<String>,
387    append_system_prompt: Option<String>,
388    permission_mode: Option<PermissionMode>,
389    continue_conversation: bool,
390    resume: Option<String>,
391    model: Option<String>,
392    fallback_model: Option<String>,
393    settings: Option<String>,
394    add_dir: Vec<PathBuf>,
395    ide: bool,
396    strict_mcp_config: bool,
397    session_id: Option<Uuid>,
398    oauth_token: Option<String>,
399    api_key: Option<String>,
400    /// Tool for handling permission prompts (e.g., "stdio" for bidirectional control)
401    permission_prompt_tool: Option<String>,
402}
403
404impl Default for ClaudeCliBuilder {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410impl ClaudeCliBuilder {
411    /// Create a new Claude CLI builder with JSON streaming mode pre-configured
412    pub fn new() -> Self {
413        Self {
414            command: PathBuf::from("claude"),
415            prompt: None,
416            debug: None,
417            verbose: false,
418            dangerously_skip_permissions: false,
419            allowed_tools: Vec::new(),
420            disallowed_tools: Vec::new(),
421            mcp_config: Vec::new(),
422            append_system_prompt: None,
423            permission_mode: None,
424            continue_conversation: false,
425            resume: None,
426            model: None,
427            fallback_model: None,
428            settings: None,
429            add_dir: Vec::new(),
430            ide: false,
431            strict_mcp_config: false,
432            session_id: None,
433            oauth_token: None,
434            api_key: None,
435            permission_prompt_tool: None,
436        }
437    }
438
439    /// Set custom path to Claude binary
440    pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
441        self.command = path.into();
442        self
443    }
444
445    /// Set the prompt for Claude
446    pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
447        self.prompt = Some(prompt.into());
448        self
449    }
450
451    /// Enable debug mode with optional filter
452    pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
453        self.debug = filter.map(|s| s.into());
454        self
455    }
456
457    /// Enable verbose mode
458    pub fn verbose(mut self, verbose: bool) -> Self {
459        self.verbose = verbose;
460        self
461    }
462
463    /// Skip all permission checks (dangerous!)
464    pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
465        self.dangerously_skip_permissions = skip;
466        self
467    }
468
469    /// Add allowed tools
470    pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
471    where
472        I: IntoIterator<Item = S>,
473        S: Into<String>,
474    {
475        self.allowed_tools
476            .extend(tools.into_iter().map(|s| s.into()));
477        self
478    }
479
480    /// Add disallowed tools
481    pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
482    where
483        I: IntoIterator<Item = S>,
484        S: Into<String>,
485    {
486        self.disallowed_tools
487            .extend(tools.into_iter().map(|s| s.into()));
488        self
489    }
490
491    /// Add MCP configuration
492    pub fn mcp_config<I, S>(mut self, configs: I) -> Self
493    where
494        I: IntoIterator<Item = S>,
495        S: Into<String>,
496    {
497        self.mcp_config
498            .extend(configs.into_iter().map(|s| s.into()));
499        self
500    }
501
502    /// Append a system prompt
503    pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
504        self.append_system_prompt = Some(prompt.into());
505        self
506    }
507
508    /// Set permission mode
509    pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
510        self.permission_mode = Some(mode);
511        self
512    }
513
514    /// Continue the most recent conversation
515    pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
516        self.continue_conversation = continue_conv;
517        self
518    }
519
520    /// Resume a specific conversation
521    pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
522        self.resume = session_id.map(|s| s.into());
523        self
524    }
525
526    /// Set the model to use
527    pub fn model<S: Into<String>>(mut self, model: S) -> Self {
528        self.model = Some(model.into());
529        self
530    }
531
532    /// Set fallback model for overload situations
533    pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
534        self.fallback_model = Some(model.into());
535        self
536    }
537
538    /// Load settings from file or JSON
539    pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
540        self.settings = Some(settings.into());
541        self
542    }
543
544    /// Add directories for tool access
545    pub fn add_directories<I, P>(mut self, dirs: I) -> Self
546    where
547        I: IntoIterator<Item = P>,
548        P: Into<PathBuf>,
549    {
550        self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
551        self
552    }
553
554    /// Automatically connect to IDE
555    pub fn ide(mut self, ide: bool) -> Self {
556        self.ide = ide;
557        self
558    }
559
560    /// Use only MCP servers from config
561    pub fn strict_mcp_config(mut self, strict: bool) -> Self {
562        self.strict_mcp_config = strict;
563        self
564    }
565
566    /// Set a specific session ID (must be a UUID)
567    pub fn session_id(mut self, id: Uuid) -> Self {
568        self.session_id = Some(id);
569        self
570    }
571
572    /// Set OAuth token for authentication (must start with "sk-ant-oat")
573    pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
574        let token_str = token.into();
575        if !token_str.starts_with("sk-ant-oat") {
576            eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
577        }
578        self.oauth_token = Some(token_str);
579        self
580    }
581
582    /// Set API key for authentication (must start with "sk-ant-api")
583    pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
584        let key_str = key.into();
585        if !key_str.starts_with("sk-ant-api") {
586            eprintln!("Warning: API key should start with 'sk-ant-api'");
587        }
588        self.api_key = Some(key_str);
589        self
590    }
591
592    /// Enable bidirectional tool permission protocol via stdio
593    ///
594    /// When enabled, Claude CLI will send permission requests via stdout
595    /// and expect responses via stdin. Use "stdio" for standard I/O based
596    /// permission handling.
597    ///
598    /// # Example
599    /// ```
600    /// use claude_codes::ClaudeCliBuilder;
601    ///
602    /// let builder = ClaudeCliBuilder::new()
603    ///     .permission_prompt_tool("stdio")
604    ///     .model("sonnet");
605    /// ```
606    pub fn permission_prompt_tool<S: Into<String>>(mut self, tool: S) -> Self {
607        self.permission_prompt_tool = Some(tool.into());
608        self
609    }
610
611    /// Build the command arguments (always includes JSON streaming flags)
612    fn build_args(&self) -> Vec<String> {
613        // Always add JSON streaming mode flags
614        // Note: --print with stream-json requires --verbose
615        let mut args = vec![
616            "--print".to_string(),
617            "--verbose".to_string(),
618            "--output-format".to_string(),
619            "stream-json".to_string(),
620            "--input-format".to_string(),
621            "stream-json".to_string(),
622        ];
623
624        if let Some(ref debug) = self.debug {
625            args.push("--debug".to_string());
626            if !debug.is_empty() {
627                args.push(debug.clone());
628            }
629        }
630
631        if self.dangerously_skip_permissions {
632            args.push("--dangerously-skip-permissions".to_string());
633        }
634
635        if !self.allowed_tools.is_empty() {
636            args.push("--allowed-tools".to_string());
637            args.extend(self.allowed_tools.clone());
638        }
639
640        if !self.disallowed_tools.is_empty() {
641            args.push("--disallowed-tools".to_string());
642            args.extend(self.disallowed_tools.clone());
643        }
644
645        if !self.mcp_config.is_empty() {
646            args.push("--mcp-config".to_string());
647            args.extend(self.mcp_config.clone());
648        }
649
650        if let Some(ref prompt) = self.append_system_prompt {
651            args.push("--append-system-prompt".to_string());
652            args.push(prompt.clone());
653        }
654
655        if let Some(ref mode) = self.permission_mode {
656            args.push("--permission-mode".to_string());
657            args.push(mode.as_str().to_string());
658        }
659
660        if self.continue_conversation {
661            args.push("--continue".to_string());
662        }
663
664        if let Some(ref session) = self.resume {
665            args.push("--resume".to_string());
666            args.push(session.clone());
667        }
668
669        if let Some(ref model) = self.model {
670            args.push("--model".to_string());
671            args.push(model.clone());
672        }
673
674        if let Some(ref model) = self.fallback_model {
675            args.push("--fallback-model".to_string());
676            args.push(model.clone());
677        }
678
679        if let Some(ref settings) = self.settings {
680            args.push("--settings".to_string());
681            args.push(settings.clone());
682        }
683
684        if !self.add_dir.is_empty() {
685            args.push("--add-dir".to_string());
686            for dir in &self.add_dir {
687                args.push(dir.to_string_lossy().to_string());
688            }
689        }
690
691        if self.ide {
692            args.push("--ide".to_string());
693        }
694
695        if self.strict_mcp_config {
696            args.push("--strict-mcp-config".to_string());
697        }
698
699        if let Some(ref tool) = self.permission_prompt_tool {
700            args.push("--permission-prompt-tool".to_string());
701            args.push(tool.clone());
702        }
703
704        // Only add --session-id when NOT resuming/continuing an existing session
705        // (Claude CLI error: --session-id can only be used with --continue or --resume
706        // if --fork-session is also specified)
707        if self.resume.is_none() && !self.continue_conversation {
708            args.push("--session-id".to_string());
709            let session_uuid = self.session_id.unwrap_or_else(|| {
710                let uuid = Uuid::new_v4();
711                debug!("[CLI] Generated session UUID: {}", uuid);
712                uuid
713            });
714            args.push(session_uuid.to_string());
715        }
716
717        // Add prompt as the last argument if provided
718        if let Some(ref prompt) = self.prompt {
719            args.push(prompt.clone());
720        }
721
722        args
723    }
724
725    /// Spawn the Claude process
726    #[cfg(feature = "async-client")]
727    pub async fn spawn(self) -> Result<tokio::process::Child> {
728        let args = self.build_args();
729
730        // Log the full command being executed
731        debug!(
732            "[CLI] Executing command: {} {}",
733            self.command.display(),
734            args.join(" ")
735        );
736
737        let mut cmd = tokio::process::Command::new(&self.command);
738        cmd.args(&args)
739            .stdin(Stdio::piped())
740            .stdout(Stdio::piped())
741            .stderr(Stdio::piped());
742
743        // Set OAuth token environment variable if provided
744        if let Some(ref token) = self.oauth_token {
745            cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
746            debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
747        }
748
749        // Set API key environment variable if provided
750        if let Some(ref key) = self.api_key {
751            cmd.env("ANTHROPIC_API_KEY", key);
752            debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
753        }
754
755        let child = cmd.spawn().map_err(Error::Io)?;
756
757        Ok(child)
758    }
759
760    /// Build a Command without spawning (for testing or manual execution)
761    #[cfg(feature = "async-client")]
762    pub fn build_command(self) -> tokio::process::Command {
763        let args = self.build_args();
764        let mut cmd = tokio::process::Command::new(&self.command);
765        cmd.args(&args)
766            .stdin(Stdio::piped())
767            .stdout(Stdio::piped())
768            .stderr(Stdio::piped());
769
770        // Set OAuth token environment variable if provided
771        if let Some(ref token) = self.oauth_token {
772            cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
773        }
774
775        // Set API key environment variable if provided
776        if let Some(ref key) = self.api_key {
777            cmd.env("ANTHROPIC_API_KEY", key);
778        }
779
780        cmd
781    }
782
783    /// Spawn the Claude process using synchronous std::process
784    pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
785        let args = self.build_args();
786
787        // Log the full command being executed
788        debug!(
789            "[CLI] Executing sync command: {} {}",
790            self.command.display(),
791            args.join(" ")
792        );
793
794        let mut cmd = std::process::Command::new(&self.command);
795        cmd.args(&args)
796            .stdin(Stdio::piped())
797            .stdout(Stdio::piped())
798            .stderr(Stdio::piped());
799
800        // Set OAuth token environment variable if provided
801        if let Some(ref token) = self.oauth_token {
802            cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
803            debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
804        }
805
806        // Set API key environment variable if provided
807        if let Some(ref key) = self.api_key {
808            cmd.env("ANTHROPIC_API_KEY", key);
809            debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
810        }
811
812        cmd.spawn()
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819
820    #[test]
821    fn test_streaming_flags_always_present() {
822        let builder = ClaudeCliBuilder::new();
823        let args = builder.build_args();
824
825        // Verify all streaming flags are present by default
826        assert!(args.contains(&"--print".to_string()));
827        assert!(args.contains(&"--verbose".to_string())); // Required for --print with stream-json
828        assert!(args.contains(&"--output-format".to_string()));
829        assert!(args.contains(&"stream-json".to_string()));
830        assert!(args.contains(&"--input-format".to_string()));
831    }
832
833    #[test]
834    fn test_with_prompt() {
835        let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
836        let args = builder.build_args();
837
838        assert_eq!(args.last().unwrap(), "Hello, Claude!");
839    }
840
841    #[test]
842    fn test_with_model() {
843        let builder = ClaudeCliBuilder::new()
844            .model("sonnet")
845            .fallback_model("opus");
846        let args = builder.build_args();
847
848        assert!(args.contains(&"--model".to_string()));
849        assert!(args.contains(&"sonnet".to_string()));
850        assert!(args.contains(&"--fallback-model".to_string()));
851        assert!(args.contains(&"opus".to_string()));
852    }
853
854    #[test]
855    fn test_with_debug() {
856        let builder = ClaudeCliBuilder::new().debug(Some("api"));
857        let args = builder.build_args();
858
859        assert!(args.contains(&"--debug".to_string()));
860        assert!(args.contains(&"api".to_string()));
861    }
862
863    #[test]
864    fn test_with_oauth_token() {
865        let valid_token = "sk-ant-oat-123456789";
866        let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
867
868        // OAuth token is set as env var, not in args
869        let args = builder.clone().build_args();
870        assert!(!args.contains(&valid_token.to_string()));
871
872        // Verify it's stored in the builder
873        assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
874    }
875
876    #[test]
877    fn test_oauth_token_validation() {
878        // Test with invalid prefix (should print warning but still accept)
879        let invalid_token = "invalid-token-123";
880        let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
881        assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
882    }
883
884    #[test]
885    fn test_with_api_key() {
886        let valid_key = "sk-ant-api-987654321";
887        let builder = ClaudeCliBuilder::new().api_key(valid_key);
888
889        // API key is set as env var, not in args
890        let args = builder.clone().build_args();
891        assert!(!args.contains(&valid_key.to_string()));
892
893        // Verify it's stored in the builder
894        assert_eq!(builder.api_key, Some(valid_key.to_string()));
895    }
896
897    #[test]
898    fn test_api_key_validation() {
899        // Test with invalid prefix (should print warning but still accept)
900        let invalid_key = "invalid-api-key";
901        let builder = ClaudeCliBuilder::new().api_key(invalid_key);
902        assert_eq!(builder.api_key, Some(invalid_key.to_string()));
903    }
904
905    #[test]
906    fn test_both_auth_methods() {
907        let oauth = "sk-ant-oat-123";
908        let api_key = "sk-ant-api-456";
909        let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
910
911        assert_eq!(builder.oauth_token, Some(oauth.to_string()));
912        assert_eq!(builder.api_key, Some(api_key.to_string()));
913    }
914
915    #[test]
916    fn test_permission_prompt_tool() {
917        let builder = ClaudeCliBuilder::new().permission_prompt_tool("stdio");
918        let args = builder.build_args();
919
920        assert!(args.contains(&"--permission-prompt-tool".to_string()));
921        assert!(args.contains(&"stdio".to_string()));
922    }
923
924    #[test]
925    fn test_permission_prompt_tool_not_present_by_default() {
926        let builder = ClaudeCliBuilder::new();
927        let args = builder.build_args();
928
929        assert!(!args.contains(&"--permission-prompt-tool".to_string()));
930    }
931
932    #[test]
933    fn test_session_id_present_for_new_session() {
934        let builder = ClaudeCliBuilder::new();
935        let args = builder.build_args();
936
937        assert!(
938            args.contains(&"--session-id".to_string()),
939            "New sessions should have --session-id"
940        );
941    }
942
943    #[test]
944    fn test_session_id_not_present_with_resume() {
945        // When resuming a session, --session-id should NOT be added
946        // (Claude CLI rejects --session-id + --resume without --fork-session)
947        let builder = ClaudeCliBuilder::new().resume(Some("existing-uuid".to_string()));
948        let args = builder.build_args();
949
950        assert!(
951            args.contains(&"--resume".to_string()),
952            "Should have --resume flag"
953        );
954        assert!(
955            !args.contains(&"--session-id".to_string()),
956            "--session-id should NOT be present when resuming"
957        );
958    }
959
960    #[test]
961    fn test_session_id_not_present_with_continue() {
962        // When continuing a session, --session-id should NOT be added
963        let builder = ClaudeCliBuilder::new().continue_conversation(true);
964        let args = builder.build_args();
965
966        assert!(
967            args.contains(&"--continue".to_string()),
968            "Should have --continue flag"
969        );
970        assert!(
971            !args.contains(&"--session-id".to_string()),
972            "--session-id should NOT be present when continuing"
973        );
974    }
975}