Skip to main content

claude_code/
config.rs

1use std::time::Duration;
2
3/// Configuration options for Claude CLI execution.
4#[derive(Debug, Clone, Default)]
5#[non_exhaustive]
6pub struct ClaudeConfig {
7    /// Model to use (`--model`).
8    pub model: Option<String>,
9    /// System prompt (`--system-prompt`). Defaults to empty string when `None`.
10    pub system_prompt: Option<String>,
11    /// Append to the default system prompt (`--append-system-prompt`).
12    pub append_system_prompt: Option<String>,
13    /// Maximum number of turns (`--max-turns`).
14    pub max_turns: Option<u32>,
15    /// Timeout duration. No timeout when `None`. Library-only; not a CLI flag.
16    pub timeout: Option<Duration>,
17    /// Idle timeout for streams. If no event arrives within this duration,
18    /// the stream yields [`ClaudeError::Timeout`](crate::ClaudeError::Timeout)
19    /// and terminates. Library-only; not a CLI flag.
20    pub stream_idle_timeout: Option<Duration>,
21    /// Fallback model when default is overloaded (`--fallback-model`).
22    pub fallback_model: Option<String>,
23    /// Effort level (`--effort`). Use [`effort`] constants for known values.
24    pub effort: Option<String>,
25    /// Maximum dollar amount for API calls (`--max-budget-usd`).
26    pub max_budget_usd: Option<f64>,
27    /// Tools to allow (`--allowedTools`).
28    pub allowed_tools: Vec<String>,
29    /// Tools to deny (`--disallowedTools`).
30    pub disallowed_tools: Vec<String>,
31    /// Built-in tool set override (`--tools`). Defaults to `""` (none) when `None`.
32    pub tools: Option<String>,
33    /// MCP server configs (`--mcp-config`). Defaults to `{"mcpServers":{}}` when empty.
34    pub mcp_config: Vec<String>,
35    /// Setting sources to load (`--setting-sources`). Defaults to `""` (none) when `None`.
36    pub setting_sources: Option<String>,
37    /// Path to settings file or JSON string (`--settings`).
38    pub settings: Option<String>,
39    /// JSON Schema for structured output (`--json-schema`).
40    pub json_schema: Option<String>,
41    /// Include partial message chunks in stream output (`--include-partial-messages`).
42    pub include_partial_messages: Option<bool>,
43    /// Include hook events in stream output (`--include-hook-events`).
44    pub include_hook_events: Option<bool>,
45    /// Permission mode (`--permission-mode`). Use [`permission_mode`] constants for known values.
46    pub permission_mode: Option<String>,
47    /// Bypass all permission checks (`--dangerously-skip-permissions`).
48    pub dangerously_skip_permissions: Option<bool>,
49    /// Additional directories for tool access (`--add-dir`).
50    pub add_dir: Vec<String>,
51    /// File resources to download at startup (`--file`).
52    pub file: Vec<String>,
53    /// Resume a conversation by session ID (`--resume`).
54    pub resume: Option<String>,
55    /// Use a specific session ID (`--session-id`).
56    pub session_id: Option<String>,
57    /// Minimal mode (`--bare`).
58    pub bare: Option<bool>,
59    /// Disable session persistence (`--no-session-persistence`). Enabled by default.
60    pub no_session_persistence: Option<bool>,
61    /// Disable all skills (`--disable-slash-commands`). Enabled by default.
62    pub disable_slash_commands: Option<bool>,
63    /// Only use MCP servers from `--mcp-config` (`--strict-mcp-config`). Enabled by default.
64    pub strict_mcp_config: Option<bool>,
65    /// Arbitrary CLI arguments for forward compatibility.
66    ///
67    /// Appended before the prompt. Use typed fields when available;
68    /// duplicating a typed field here may cause unpredictable CLI behavior.
69    pub extra_args: Vec<String>,
70}
71
72impl ClaudeConfig {
73    /// Returns a new builder.
74    #[must_use]
75    pub fn builder() -> ClaudeConfigBuilder {
76        ClaudeConfigBuilder::default()
77    }
78
79    /// Creates a builder pre-filled with this configuration's values.
80    #[must_use]
81    pub fn to_builder(&self) -> ClaudeConfigBuilder {
82        ClaudeConfigBuilder {
83            model: self.model.clone(),
84            system_prompt: self.system_prompt.clone(),
85            append_system_prompt: self.append_system_prompt.clone(),
86            max_turns: self.max_turns,
87            timeout: self.timeout,
88            stream_idle_timeout: self.stream_idle_timeout,
89            fallback_model: self.fallback_model.clone(),
90            effort: self.effort.clone(),
91            max_budget_usd: self.max_budget_usd,
92            allowed_tools: self.allowed_tools.clone(),
93            disallowed_tools: self.disallowed_tools.clone(),
94            tools: self.tools.clone(),
95            mcp_config: self.mcp_config.clone(),
96            setting_sources: self.setting_sources.clone(),
97            settings: self.settings.clone(),
98            json_schema: self.json_schema.clone(),
99            include_partial_messages: self.include_partial_messages,
100            include_hook_events: self.include_hook_events,
101            permission_mode: self.permission_mode.clone(),
102            dangerously_skip_permissions: self.dangerously_skip_permissions,
103            add_dir: self.add_dir.clone(),
104            file: self.file.clone(),
105            resume: self.resume.clone(),
106            session_id: self.session_id.clone(),
107            bare: self.bare,
108            no_session_persistence: self.no_session_persistence,
109            disable_slash_commands: self.disable_slash_commands,
110            strict_mcp_config: self.strict_mcp_config,
111            extra_args: self.extra_args.clone(),
112        }
113    }
114
115    /// Builds common CLI arguments shared by JSON and stream-json modes.
116    fn base_args(&self) -> Vec<String> {
117        let mut args = vec!["--print".into()];
118
119        // --- Context minimization defaults (overridable) ---
120
121        // no_session_persistence: None → enabled, Some(false) → disabled
122        if self.no_session_persistence != Some(false) {
123            args.push("--no-session-persistence".into());
124        }
125
126        // setting_sources: None → "" (minimal), Some(val) → val
127        args.push("--setting-sources".into());
128        args.push(self.setting_sources.clone().unwrap_or_default());
129
130        // strict_mcp_config: None → enabled, Some(false) → disabled
131        if self.strict_mcp_config != Some(false) {
132            args.push("--strict-mcp-config".into());
133        }
134
135        // mcp_config: [] → '{"mcpServers":{}}' (minimal), non-empty → user values
136        if self.mcp_config.is_empty() {
137            args.push("--mcp-config".into());
138            args.push(r#"{"mcpServers":{}}"#.into());
139        } else {
140            for cfg in &self.mcp_config {
141                args.push("--mcp-config".into());
142                args.push(cfg.clone());
143            }
144        }
145
146        // tools: None → "" (minimal), Some(val) → val
147        args.push("--tools".into());
148        args.push(self.tools.clone().unwrap_or_default());
149
150        // disable_slash_commands: None → enabled, Some(false) → disabled
151        if self.disable_slash_commands != Some(false) {
152            args.push("--disable-slash-commands".into());
153        }
154
155        // --- Standard options ---
156
157        args.push("--system-prompt".into());
158        args.push(self.system_prompt.clone().unwrap_or_default());
159
160        if let Some(ref val) = self.append_system_prompt {
161            args.push("--append-system-prompt".into());
162            args.push(val.clone());
163        }
164
165        if let Some(ref val) = self.model {
166            args.push("--model".into());
167            args.push(val.clone());
168        }
169
170        if let Some(ref val) = self.fallback_model {
171            args.push("--fallback-model".into());
172            args.push(val.clone());
173        }
174
175        if let Some(ref val) = self.effort {
176            args.push("--effort".into());
177            args.push(val.clone());
178        }
179
180        if let Some(max_turns) = self.max_turns {
181            args.push("--max-turns".into());
182            args.push(max_turns.to_string());
183        }
184
185        if let Some(budget) = self.max_budget_usd {
186            args.push("--max-budget-usd".into());
187            args.push(budget.to_string());
188        }
189
190        if !self.allowed_tools.is_empty() {
191            args.push("--allowedTools".into());
192            args.extend(self.allowed_tools.iter().cloned());
193        }
194
195        if !self.disallowed_tools.is_empty() {
196            args.push("--disallowedTools".into());
197            args.extend(self.disallowed_tools.iter().cloned());
198        }
199
200        if let Some(ref val) = self.settings {
201            args.push("--settings".into());
202            args.push(val.clone());
203        }
204
205        if let Some(ref val) = self.json_schema {
206            args.push("--json-schema".into());
207            args.push(val.clone());
208        }
209
210        if self.include_hook_events == Some(true) {
211            args.push("--include-hook-events".into());
212        }
213
214        if let Some(ref val) = self.permission_mode {
215            args.push("--permission-mode".into());
216            args.push(val.clone());
217        }
218
219        if self.dangerously_skip_permissions == Some(true) {
220            args.push("--dangerously-skip-permissions".into());
221        }
222
223        if !self.add_dir.is_empty() {
224            args.push("--add-dir".into());
225            args.extend(self.add_dir.iter().cloned());
226        }
227
228        if !self.file.is_empty() {
229            args.push("--file".into());
230            args.extend(self.file.iter().cloned());
231        }
232
233        if let Some(ref val) = self.resume {
234            args.push("--resume".into());
235            args.push(val.clone());
236        }
237
238        if let Some(ref val) = self.session_id {
239            args.push("--session-id".into());
240            args.push(val.clone());
241        }
242
243        if self.bare == Some(true) {
244            args.push("--bare".into());
245        }
246
247        // extra_args: appended before prompt
248        args.extend(self.extra_args.iter().cloned());
249
250        args
251    }
252
253    /// Builds command-line arguments for JSON output mode.
254    ///
255    /// Includes fixed options such as `--print --output-format json`.
256    #[must_use]
257    pub fn to_args(&self, prompt: &str) -> Vec<String> {
258        let mut args = self.base_args();
259        args.push("--output-format".into());
260        args.push("json".into());
261        args.push(prompt.into());
262        args
263    }
264
265    /// Builds command-line arguments for stream-json output mode.
266    ///
267    /// Includes `--verbose` (required for stream-json) and optionally
268    /// `--include-partial-messages`.
269    #[must_use]
270    pub fn to_stream_args(&self, prompt: &str) -> Vec<String> {
271        let mut args = self.base_args();
272        args.push("--output-format".into());
273        args.push("stream-json".into());
274        args.push("--verbose".into());
275
276        if self.include_partial_messages == Some(true) {
277            args.push("--include-partial-messages".into());
278        }
279
280        args.push(prompt.into());
281        args
282    }
283}
284
285/// Builder for [`ClaudeConfig`].
286#[derive(Debug, Clone, Default)]
287pub struct ClaudeConfigBuilder {
288    model: Option<String>,
289    system_prompt: Option<String>,
290    append_system_prompt: Option<String>,
291    max_turns: Option<u32>,
292    timeout: Option<Duration>,
293    stream_idle_timeout: Option<Duration>,
294    fallback_model: Option<String>,
295    effort: Option<String>,
296    max_budget_usd: Option<f64>,
297    allowed_tools: Vec<String>,
298    disallowed_tools: Vec<String>,
299    tools: Option<String>,
300    mcp_config: Vec<String>,
301    setting_sources: Option<String>,
302    settings: Option<String>,
303    json_schema: Option<String>,
304    include_partial_messages: Option<bool>,
305    include_hook_events: Option<bool>,
306    permission_mode: Option<String>,
307    dangerously_skip_permissions: Option<bool>,
308    add_dir: Vec<String>,
309    file: Vec<String>,
310    resume: Option<String>,
311    session_id: Option<String>,
312    bare: Option<bool>,
313    no_session_persistence: Option<bool>,
314    disable_slash_commands: Option<bool>,
315    strict_mcp_config: Option<bool>,
316    extra_args: Vec<String>,
317}
318
319impl ClaudeConfigBuilder {
320    /// Sets the model.
321    #[must_use]
322    pub fn model(mut self, model: impl Into<String>) -> Self {
323        self.model = Some(model.into());
324        self
325    }
326
327    /// Sets the system prompt.
328    #[must_use]
329    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
330        self.system_prompt = Some(prompt.into());
331        self
332    }
333
334    /// Sets the append system prompt.
335    #[must_use]
336    pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
337        self.append_system_prompt = Some(prompt.into());
338        self
339    }
340
341    /// Sets the maximum number of turns.
342    #[must_use]
343    pub fn max_turns(mut self, max_turns: u32) -> Self {
344        self.max_turns = Some(max_turns);
345        self
346    }
347
348    /// Sets the timeout duration.
349    #[must_use]
350    pub fn timeout(mut self, timeout: Duration) -> Self {
351        self.timeout = Some(timeout);
352        self
353    }
354
355    /// Sets the idle timeout for streams.
356    ///
357    /// If no event arrives within this duration, the stream yields
358    /// [`ClaudeError::Timeout`](crate::ClaudeError::Timeout) and terminates.
359    #[must_use]
360    pub fn stream_idle_timeout(mut self, timeout: Duration) -> Self {
361        self.stream_idle_timeout = Some(timeout);
362        self
363    }
364
365    /// Sets the fallback model.
366    #[must_use]
367    pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
368        self.fallback_model = Some(model.into());
369        self
370    }
371
372    /// Sets the effort level. See [`effort`] constants for known values.
373    #[must_use]
374    pub fn effort(mut self, effort: impl Into<String>) -> Self {
375        self.effort = Some(effort.into());
376        self
377    }
378
379    /// Sets the maximum budget in USD.
380    #[must_use]
381    pub fn max_budget_usd(mut self, budget: f64) -> Self {
382        self.max_budget_usd = Some(budget);
383        self
384    }
385
386    /// Sets allowed tools (replaces any previous values).
387    #[must_use]
388    pub fn allowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
389        self.allowed_tools = tools.into_iter().map(Into::into).collect();
390        self
391    }
392
393    /// Adds a single allowed tool.
394    #[must_use]
395    pub fn add_allowed_tool(mut self, tool: impl Into<String>) -> Self {
396        self.allowed_tools.push(tool.into());
397        self
398    }
399
400    /// Sets disallowed tools (replaces any previous values).
401    #[must_use]
402    pub fn disallowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
403        self.disallowed_tools = tools.into_iter().map(Into::into).collect();
404        self
405    }
406
407    /// Adds a single disallowed tool.
408    #[must_use]
409    pub fn add_disallowed_tool(mut self, tool: impl Into<String>) -> Self {
410        self.disallowed_tools.push(tool.into());
411        self
412    }
413
414    /// Sets the built-in tool set. `""` disables all, `"default"` enables all.
415    #[must_use]
416    pub fn tools(mut self, tools: impl Into<String>) -> Self {
417        self.tools = Some(tools.into());
418        self
419    }
420
421    /// Sets MCP server configs (replaces any previous values).
422    #[must_use]
423    pub fn mcp_configs(mut self, configs: impl IntoIterator<Item = impl Into<String>>) -> Self {
424        self.mcp_config = configs.into_iter().map(Into::into).collect();
425        self
426    }
427
428    /// Adds a single MCP server config.
429    #[must_use]
430    pub fn add_mcp_config(mut self, config: impl Into<String>) -> Self {
431        self.mcp_config.push(config.into());
432        self
433    }
434
435    /// Sets the setting sources to load.
436    #[must_use]
437    pub fn setting_sources(mut self, sources: impl Into<String>) -> Self {
438        self.setting_sources = Some(sources.into());
439        self
440    }
441
442    /// Sets the path to a settings file or JSON string.
443    #[must_use]
444    pub fn settings(mut self, settings: impl Into<String>) -> Self {
445        self.settings = Some(settings.into());
446        self
447    }
448
449    /// Sets the JSON Schema for structured output.
450    #[must_use]
451    pub fn json_schema(mut self, schema: impl Into<String>) -> Self {
452        self.json_schema = Some(schema.into());
453        self
454    }
455
456    /// Enables or disables partial message chunks in stream output.
457    #[must_use]
458    pub fn include_partial_messages(mut self, enabled: bool) -> Self {
459        self.include_partial_messages = Some(enabled);
460        self
461    }
462
463    /// Enables or disables hook events in stream output.
464    #[must_use]
465    pub fn include_hook_events(mut self, enabled: bool) -> Self {
466        self.include_hook_events = Some(enabled);
467        self
468    }
469
470    /// Sets the permission mode. See [`permission_mode`] constants for known values.
471    #[must_use]
472    pub fn permission_mode(mut self, mode: impl Into<String>) -> Self {
473        self.permission_mode = Some(mode.into());
474        self
475    }
476
477    /// Enables or disables bypassing all permission checks.
478    #[must_use]
479    pub fn dangerously_skip_permissions(mut self, enabled: bool) -> Self {
480        self.dangerously_skip_permissions = Some(enabled);
481        self
482    }
483
484    /// Sets additional directories (replaces any previous values).
485    #[must_use]
486    pub fn add_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<String>>) -> Self {
487        self.add_dir = dirs.into_iter().map(Into::into).collect();
488        self
489    }
490
491    /// Adds a single additional directory.
492    #[must_use]
493    pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
494        self.add_dir.push(dir.into());
495        self
496    }
497
498    /// Sets file resources (replaces any previous values).
499    #[must_use]
500    pub fn files(mut self, files: impl IntoIterator<Item = impl Into<String>>) -> Self {
501        self.file = files.into_iter().map(Into::into).collect();
502        self
503    }
504
505    /// Adds a single file resource.
506    #[must_use]
507    pub fn file(mut self, file: impl Into<String>) -> Self {
508        self.file.push(file.into());
509        self
510    }
511
512    /// Sets the session ID to resume.
513    #[must_use]
514    pub fn resume(mut self, session_id: impl Into<String>) -> Self {
515        self.resume = Some(session_id.into());
516        self
517    }
518
519    /// Sets a specific session ID.
520    #[must_use]
521    pub fn session_id(mut self, id: impl Into<String>) -> Self {
522        self.session_id = Some(id.into());
523        self
524    }
525
526    /// Enables or disables bare/minimal mode.
527    #[must_use]
528    pub fn bare(mut self, enabled: bool) -> Self {
529        self.bare = Some(enabled);
530        self
531    }
532
533    /// Enables or disables session persistence.
534    /// Enabled by default; set to `false` to allow session persistence.
535    #[must_use]
536    pub fn no_session_persistence(mut self, enabled: bool) -> Self {
537        self.no_session_persistence = Some(enabled);
538        self
539    }
540
541    /// Enables or disables slash commands.
542    /// Disabled by default; set to `false` to enable slash commands.
543    #[must_use]
544    pub fn disable_slash_commands(mut self, enabled: bool) -> Self {
545        self.disable_slash_commands = Some(enabled);
546        self
547    }
548
549    /// Enables or disables strict MCP config mode.
550    /// Enabled by default; set to `false` to allow non-`--mcp-config` MCP servers.
551    #[must_use]
552    pub fn strict_mcp_config(mut self, enabled: bool) -> Self {
553        self.strict_mcp_config = Some(enabled);
554        self
555    }
556
557    /// Sets arbitrary extra CLI arguments (replaces any previous values).
558    ///
559    /// These are appended before the prompt. Use typed fields when available;
560    /// duplicating a typed field here may cause unpredictable CLI behavior.
561    #[must_use]
562    pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
563        self.extra_args = args.into_iter().map(Into::into).collect();
564        self
565    }
566
567    /// Adds a single extra CLI argument.
568    #[must_use]
569    pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self {
570        self.extra_args.push(arg.into());
571        self
572    }
573
574    /// Builds the [`ClaudeConfig`].
575    #[must_use]
576    pub fn build(self) -> ClaudeConfig {
577        ClaudeConfig {
578            model: self.model,
579            system_prompt: self.system_prompt,
580            append_system_prompt: self.append_system_prompt,
581            max_turns: self.max_turns,
582            timeout: self.timeout,
583            stream_idle_timeout: self.stream_idle_timeout,
584            fallback_model: self.fallback_model,
585            effort: self.effort,
586            max_budget_usd: self.max_budget_usd,
587            allowed_tools: self.allowed_tools,
588            disallowed_tools: self.disallowed_tools,
589            tools: self.tools,
590            mcp_config: self.mcp_config,
591            setting_sources: self.setting_sources,
592            settings: self.settings,
593            json_schema: self.json_schema,
594            include_partial_messages: self.include_partial_messages,
595            include_hook_events: self.include_hook_events,
596            permission_mode: self.permission_mode,
597            dangerously_skip_permissions: self.dangerously_skip_permissions,
598            add_dir: self.add_dir,
599            file: self.file,
600            resume: self.resume,
601            session_id: self.session_id,
602            bare: self.bare,
603            no_session_persistence: self.no_session_persistence,
604            disable_slash_commands: self.disable_slash_commands,
605            strict_mcp_config: self.strict_mcp_config,
606            extra_args: self.extra_args,
607        }
608    }
609}
610
611/// Known values for the `--effort` CLI option.
612pub mod effort {
613    /// Low effort.
614    pub const LOW: &str = "low";
615    /// Medium effort.
616    pub const MEDIUM: &str = "medium";
617    /// High effort.
618    pub const HIGH: &str = "high";
619    /// Maximum effort.
620    pub const MAX: &str = "max";
621}
622
623/// Known values for the `--permission-mode` CLI option.
624pub mod permission_mode {
625    /// Default permission mode.
626    pub const DEFAULT: &str = "default";
627    /// Accept edits without confirmation.
628    pub const ACCEPT_EDITS: &str = "acceptEdits";
629    /// Automatic permission handling.
630    pub const AUTO: &str = "auto";
631    /// Bypass all permission checks.
632    pub const BYPASS_PERMISSIONS: &str = "bypassPermissions";
633    /// Never ask for permission.
634    pub const DONT_ASK: &str = "dontAsk";
635    /// Plan mode.
636    pub const PLAN: &str = "plan";
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    #[test]
644    fn default_config() {
645        let config = ClaudeConfig::default();
646        assert!(config.model.is_none());
647        assert!(config.system_prompt.is_none());
648        assert!(config.max_turns.is_none());
649        assert!(config.timeout.is_none());
650    }
651
652    #[test]
653    fn builder_sets_stream_idle_timeout() {
654        let config = ClaudeConfig::builder()
655            .stream_idle_timeout(Duration::from_secs(60))
656            .build();
657        assert_eq!(config.stream_idle_timeout, Some(Duration::from_secs(60)));
658    }
659
660    #[test]
661    fn default_stream_idle_timeout_is_none() {
662        let config = ClaudeConfig::default();
663        assert!(config.stream_idle_timeout.is_none());
664    }
665
666    #[test]
667    fn builder_sets_all_fields() {
668        let config = ClaudeConfig::builder()
669            .model("haiku")
670            .system_prompt("You are helpful")
671            .max_turns(3)
672            .timeout(Duration::from_secs(30))
673            .build();
674
675        assert_eq!(config.model.as_deref(), Some("haiku"));
676        assert_eq!(config.system_prompt.as_deref(), Some("You are helpful"));
677        assert_eq!(config.max_turns, Some(3));
678        assert_eq!(config.timeout, Some(Duration::from_secs(30)));
679    }
680
681    #[test]
682    fn to_args_minimal() {
683        let config = ClaudeConfig::default();
684        let args = config.to_args("hello");
685
686        assert!(args.contains(&"--print".to_string()));
687        assert!(args.contains(&"json".to_string()));
688        assert!(args.contains(&"--no-session-persistence".to_string()));
689        assert!(args.contains(&"--disable-slash-commands".to_string()));
690        assert!(args.contains(&"--strict-mcp-config".to_string()));
691        // system-prompt defaults to empty string
692        let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
693        assert_eq!(args[sp_idx + 1], "");
694        // model, max-turns should not be present
695        assert!(!args.contains(&"--model".to_string()));
696        assert!(!args.contains(&"--max-turns".to_string()));
697        // prompt should be the last argument
698        assert_eq!(args.last().unwrap(), "hello");
699    }
700
701    #[test]
702    fn to_args_with_options() {
703        let config = ClaudeConfig::builder()
704            .model("haiku")
705            .system_prompt("Be concise")
706            .max_turns(5)
707            .build();
708        let args = config.to_args("test prompt");
709
710        let model_idx = args.iter().position(|a| a == "--model").unwrap();
711        assert_eq!(args[model_idx + 1], "haiku");
712
713        let sp_idx = args.iter().position(|a| a == "--system-prompt").unwrap();
714        assert_eq!(args[sp_idx + 1], "Be concise");
715
716        let mt_idx = args.iter().position(|a| a == "--max-turns").unwrap();
717        assert_eq!(args[mt_idx + 1], "5");
718
719        assert_eq!(args.last().unwrap(), "test prompt");
720    }
721
722    #[test]
723    fn to_stream_args_minimal() {
724        let config = ClaudeConfig::default();
725        let args = config.to_stream_args("hello");
726
727        assert!(args.contains(&"--print".to_string()));
728        assert!(args.contains(&"stream-json".to_string()));
729        assert!(args.contains(&"--verbose".to_string()));
730        assert!(!args.contains(&"json".to_string()));
731        assert!(!args.contains(&"--include-partial-messages".to_string()));
732        assert_eq!(args.last().unwrap(), "hello");
733    }
734
735    #[test]
736    fn to_stream_args_with_partial_messages() {
737        let config = ClaudeConfig::builder()
738            .include_partial_messages(true)
739            .build();
740        let args = config.to_stream_args("hello");
741
742        assert!(args.contains(&"--include-partial-messages".to_string()));
743    }
744
745    #[test]
746    fn builder_sets_include_partial_messages() {
747        let config = ClaudeConfig::builder()
748            .include_partial_messages(true)
749            .build();
750        assert_eq!(config.include_partial_messages, Some(true));
751    }
752
753    #[test]
754    fn all_new_fields_in_builder() {
755        let config = ClaudeConfig::builder()
756            .append_system_prompt("extra context")
757            .fallback_model("haiku")
758            .effort("high")
759            .max_budget_usd(1.0)
760            .allowed_tools(["Bash", "Edit"])
761            .disallowed_tools(["Write"])
762            .tools("Bash,Edit")
763            .mcp_configs(["config.json"])
764            .setting_sources("user,project")
765            .settings("settings.json")
766            .json_schema(r#"{"type":"object"}"#)
767            .include_hook_events(true)
768            .permission_mode("auto")
769            .dangerously_skip_permissions(true)
770            .add_dirs(["/path/a"])
771            .file("spec:file.txt")
772            .resume("session-123")
773            .session_id("uuid-456")
774            .bare(true)
775            .no_session_persistence(false)
776            .disable_slash_commands(false)
777            .strict_mcp_config(false)
778            .extra_args(["--custom", "val"])
779            .build();
780
781        assert_eq!(
782            config.append_system_prompt.as_deref(),
783            Some("extra context")
784        );
785        assert_eq!(config.fallback_model.as_deref(), Some("haiku"));
786        assert_eq!(config.effort.as_deref(), Some("high"));
787        assert_eq!(config.max_budget_usd, Some(1.0));
788        assert_eq!(config.allowed_tools, vec!["Bash", "Edit"]);
789        assert_eq!(config.disallowed_tools, vec!["Write"]);
790        assert_eq!(config.tools.as_deref(), Some("Bash,Edit"));
791        assert_eq!(config.mcp_config, vec!["config.json"]);
792        assert_eq!(config.setting_sources.as_deref(), Some("user,project"));
793        assert_eq!(config.settings.as_deref(), Some("settings.json"));
794        assert_eq!(config.json_schema.as_deref(), Some(r#"{"type":"object"}"#));
795        assert_eq!(config.include_hook_events, Some(true));
796        assert_eq!(config.permission_mode.as_deref(), Some("auto"));
797        assert_eq!(config.dangerously_skip_permissions, Some(true));
798        assert_eq!(config.add_dir, vec!["/path/a"]);
799        assert_eq!(config.file, vec!["spec:file.txt"]);
800        assert_eq!(config.resume.as_deref(), Some("session-123"));
801        assert_eq!(config.session_id.as_deref(), Some("uuid-456"));
802        assert_eq!(config.bare, Some(true));
803        assert_eq!(config.no_session_persistence, Some(false));
804        assert_eq!(config.disable_slash_commands, Some(false));
805        assert_eq!(config.strict_mcp_config, Some(false));
806        assert_eq!(config.extra_args, vec!["--custom", "val"]);
807    }
808
809    #[test]
810    fn default_uses_minimal_context() {
811        let config = ClaudeConfig::default();
812        let args = config.to_args("test");
813
814        assert!(args.contains(&"--no-session-persistence".to_string()));
815        assert!(args.contains(&"--strict-mcp-config".to_string()));
816        assert!(args.contains(&"--disable-slash-commands".to_string()));
817
818        let ss_idx = args.iter().position(|a| a == "--setting-sources").unwrap();
819        assert_eq!(args[ss_idx + 1], "");
820
821        let mcp_idx = args.iter().position(|a| a == "--mcp-config").unwrap();
822        assert_eq!(args[mcp_idx + 1], r#"{"mcpServers":{}}"#);
823
824        let tools_idx = args.iter().position(|a| a == "--tools").unwrap();
825        assert_eq!(args[tools_idx + 1], "");
826    }
827
828    #[test]
829    fn override_no_session_persistence_false() {
830        let config = ClaudeConfig::builder()
831            .no_session_persistence(false)
832            .build();
833        let args = config.to_args("test");
834        assert!(!args.contains(&"--no-session-persistence".to_string()));
835    }
836
837    #[test]
838    fn override_strict_mcp_config_false() {
839        let config = ClaudeConfig::builder().strict_mcp_config(false).build();
840        let args = config.to_args("test");
841        assert!(!args.contains(&"--strict-mcp-config".to_string()));
842    }
843
844    #[test]
845    fn override_disable_slash_commands_false() {
846        let config = ClaudeConfig::builder()
847            .disable_slash_commands(false)
848            .build();
849        let args = config.to_args("test");
850        assert!(!args.contains(&"--disable-slash-commands".to_string()));
851    }
852
853    #[test]
854    fn override_tools() {
855        let config = ClaudeConfig::builder().tools("Bash,Edit").build();
856        let args = config.to_args("test");
857        let idx = args.iter().position(|a| a == "--tools").unwrap();
858        assert_eq!(args[idx + 1], "Bash,Edit");
859    }
860
861    #[test]
862    fn override_setting_sources() {
863        let config = ClaudeConfig::builder()
864            .setting_sources("user,project")
865            .build();
866        let args = config.to_args("test");
867        let idx = args.iter().position(|a| a == "--setting-sources").unwrap();
868        assert_eq!(args[idx + 1], "user,project");
869    }
870
871    #[test]
872    fn override_mcp_config() {
873        let config = ClaudeConfig::builder()
874            .mcp_configs(["path/config.json"])
875            .build();
876        let args = config.to_args("test");
877        let idx = args.iter().position(|a| a == "--mcp-config").unwrap();
878        assert_eq!(args[idx + 1], "path/config.json");
879        assert!(!args.contains(&r#"{"mcpServers":{}}"#.to_string()));
880    }
881
882    #[test]
883    fn effort_with_constant() {
884        let config = ClaudeConfig::builder().effort(effort::HIGH).build();
885        let args = config.to_args("test");
886        let idx = args.iter().position(|a| a == "--effort").unwrap();
887        assert_eq!(args[idx + 1], "high");
888    }
889
890    #[test]
891    fn effort_with_custom_string() {
892        let config = ClaudeConfig::builder().effort("ultra").build();
893        let args = config.to_args("test");
894        let idx = args.iter().position(|a| a == "--effort").unwrap();
895        assert_eq!(args[idx + 1], "ultra");
896    }
897
898    #[test]
899    fn allowed_tools_multiple() {
900        let config = ClaudeConfig::builder()
901            .allowed_tools(["Bash(git:*)", "Edit", "Read"])
902            .build();
903        let args = config.to_args("test");
904        let idx = args.iter().position(|a| a == "--allowedTools").unwrap();
905        assert_eq!(args[idx + 1], "Bash(git:*)");
906        assert_eq!(args[idx + 2], "Edit");
907        assert_eq!(args[idx + 3], "Read");
908    }
909
910    #[test]
911    fn bare_flag() {
912        let config = ClaudeConfig::builder().bare(true).build();
913        let args = config.to_args("test");
914        assert!(args.contains(&"--bare".to_string()));
915    }
916
917    #[test]
918    fn dangerously_skip_permissions_flag() {
919        let config = ClaudeConfig::builder()
920            .dangerously_skip_permissions(true)
921            .build();
922        let args = config.to_args("test");
923        assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
924    }
925
926    #[test]
927    fn resume_session() {
928        let config = ClaudeConfig::builder().resume("session-abc").build();
929        let args = config.to_args("test");
930        let idx = args.iter().position(|a| a == "--resume").unwrap();
931        assert_eq!(args[idx + 1], "session-abc");
932    }
933
934    #[test]
935    fn session_id_field() {
936        let config = ClaudeConfig::builder()
937            .session_id("550e8400-e29b-41d4-a716-446655440000")
938            .build();
939        let args = config.to_args("test");
940        let idx = args.iter().position(|a| a == "--session-id").unwrap();
941        assert_eq!(args[idx + 1], "550e8400-e29b-41d4-a716-446655440000");
942    }
943
944    #[test]
945    fn json_schema_field() {
946        let schema = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
947        let config = ClaudeConfig::builder().json_schema(schema).build();
948        let args = config.to_args("test");
949        let idx = args.iter().position(|a| a == "--json-schema").unwrap();
950        assert_eq!(args[idx + 1], schema);
951    }
952
953    #[test]
954    fn add_dir_multiple() {
955        let config = ClaudeConfig::builder()
956            .add_dirs(["/path/a", "/path/b"])
957            .build();
958        let args = config.to_args("test");
959        let idx = args.iter().position(|a| a == "--add-dir").unwrap();
960        assert_eq!(args[idx + 1], "/path/a");
961        assert_eq!(args[idx + 2], "/path/b");
962    }
963
964    #[test]
965    fn file_multiple() {
966        let config = ClaudeConfig::builder()
967            .files(["file_abc:doc.txt", "file_def:img.png"])
968            .build();
969        let args = config.to_args("test");
970        let idx = args.iter().position(|a| a == "--file").unwrap();
971        assert_eq!(args[idx + 1], "file_abc:doc.txt");
972        assert_eq!(args[idx + 2], "file_def:img.png");
973    }
974
975    #[test]
976    fn extra_args_before_prompt() {
977        let config = ClaudeConfig::builder()
978            .extra_args(["--custom-flag", "value"])
979            .build();
980        let args = config.to_args("my prompt");
981        let custom_idx = args.iter().position(|a| a == "--custom-flag").unwrap();
982        let prompt_idx = args.iter().position(|a| a == "my prompt").unwrap();
983        assert!(custom_idx < prompt_idx);
984        assert_eq!(args[custom_idx + 1], "value");
985    }
986
987    #[test]
988    fn extra_args_with_typed_fields() {
989        let config = ClaudeConfig::builder()
990            .model("sonnet")
991            .extra_args(["--custom", "val"])
992            .build();
993        let args = config.to_args("test");
994        assert!(args.contains(&"--model".to_string()));
995        assert!(args.contains(&"sonnet".to_string()));
996        assert!(args.contains(&"--custom".to_string()));
997        assert!(args.contains(&"val".to_string()));
998    }
999
1000    #[test]
1001    fn disallowed_tools_multiple() {
1002        let config = ClaudeConfig::builder()
1003            .disallowed_tools(["Write", "Bash"])
1004            .build();
1005        let args = config.to_args("test");
1006        let idx = args.iter().position(|a| a == "--disallowedTools").unwrap();
1007        assert_eq!(args[idx + 1], "Write");
1008        assert_eq!(args[idx + 2], "Bash");
1009    }
1010
1011    #[test]
1012    fn to_builder_round_trip_fields() {
1013        let original = ClaudeConfig::builder()
1014            .model("haiku")
1015            .system_prompt("test")
1016            .max_turns(5)
1017            .timeout(Duration::from_secs(30))
1018            .stream_idle_timeout(Duration::from_secs(45))
1019            .no_session_persistence(false)
1020            .resume("session-123")
1021            .build();
1022
1023        let rebuilt = original.to_builder().build();
1024
1025        assert_eq!(rebuilt.model, original.model);
1026        assert_eq!(rebuilt.system_prompt, original.system_prompt);
1027        assert_eq!(rebuilt.max_turns, original.max_turns);
1028        assert_eq!(rebuilt.timeout, original.timeout);
1029        assert_eq!(rebuilt.stream_idle_timeout, original.stream_idle_timeout);
1030        assert_eq!(
1031            rebuilt.no_session_persistence,
1032            original.no_session_persistence
1033        );
1034        assert_eq!(rebuilt.resume, original.resume);
1035    }
1036
1037    #[test]
1038    fn to_builder_round_trip_args() {
1039        let config = ClaudeConfig::builder()
1040            .model("haiku")
1041            .max_turns(3)
1042            .effort("high")
1043            .allowed_tools(["Bash", "Read"])
1044            .no_session_persistence(false)
1045            .build();
1046
1047        let rebuilt = config.to_builder().build();
1048        assert_eq!(config.to_args("hi"), rebuilt.to_args("hi"));
1049    }
1050}