Skip to main content

claude_code/
config.rs

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