Skip to main content

claude_code_sdk_rust/internal/
cli_args.rs

1use crate::error::{ClaudeSDKError, Result};
2use crate::types::{SettingSource, SkillsConfig};
3
4use super::transport::TransportOptions;
5
6fn serialize_cli_value<T: serde::Serialize>(value: &T) -> Result<String> {
7    let value = serde_json::to_value(value)?;
8    match value {
9        serde_json::Value::String(s) => Ok(s),
10        other => Ok(other.to_string()),
11    }
12}
13
14fn serialize_setting_sources(sources: &[SettingSource]) -> Result<String> {
15    sources
16        .iter()
17        .map(serialize_cli_value)
18        .collect::<Result<Vec<_>>>()
19        .map(|sources| sources.join(","))
20}
21
22fn effective_allowed_tools(options: &TransportOptions) -> Vec<String> {
23    let mut allowed_tools = options.allowed_tools.clone();
24
25    match &options.skills {
26        Some(SkillsConfig::All) if !allowed_tools.iter().any(|tool| tool == "Skill") => {
27            allowed_tools.push("Skill".to_string());
28        }
29        Some(SkillsConfig::Names(names)) => {
30            for name in names {
31                let pattern = format!("Skill({name})");
32                if !allowed_tools.iter().any(|tool| tool == &pattern) {
33                    allowed_tools.push(pattern);
34                }
35            }
36        }
37        Some(SkillsConfig::All) | None => {}
38    }
39
40    allowed_tools
41}
42
43fn effective_setting_sources(options: &TransportOptions) -> Option<Vec<SettingSource>> {
44    if let Some(sources) = &options.setting_sources {
45        return Some(sources.clone());
46    }
47
48    options
49        .skills
50        .as_ref()
51        .map(|_| vec![SettingSource::User, SettingSource::Project])
52}
53
54fn read_settings_file(path: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
55    let content = std::fs::read_to_string(path).ok()?;
56    serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&content).ok()
57}
58
59fn parse_settings_value(settings: &str) -> serde_json::Map<String, serde_json::Value> {
60    let trimmed = settings.trim();
61    if trimmed.starts_with('{') && trimmed.ends_with('}') {
62        serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(trimmed)
63            .unwrap_or_else(|_| read_settings_file(trimmed).unwrap_or_default())
64    } else {
65        read_settings_file(trimmed).unwrap_or_default()
66    }
67}
68
69fn build_settings_value(options: &TransportOptions) -> Result<Option<String>> {
70    let Some(sandbox) = &options.sandbox else {
71        return Ok(options.settings.clone());
72    };
73
74    let mut settings = options
75        .settings
76        .as_deref()
77        .map(parse_settings_value)
78        .unwrap_or_default();
79    settings.insert("sandbox".to_string(), serde_json::to_value(sandbox)?);
80
81    Ok(Some(serde_json::Value::Object(settings).to_string()))
82}
83
84pub(crate) fn build_cli_args(options: &TransportOptions) -> Result<Vec<String>> {
85    let mut args = vec![
86        "--output-format".to_string(),
87        "stream-json".to_string(),
88        "--verbose".to_string(),
89        "--input-format".to_string(),
90        "stream-json".to_string(),
91    ];
92
93    if let Some(ref file) = options.system_prompt_file {
94        args.push("--system-prompt-file".to_string());
95        args.push(file.path.clone());
96    } else if let Some(ref preset) = options.system_prompt_preset {
97        if let Some(ref append) = preset.append {
98            args.push("--append-system-prompt".to_string());
99            args.push(append.clone());
100        }
101    } else {
102        let prompt = options.system_prompt.as_deref().unwrap_or("");
103        args.push("--system-prompt".to_string());
104        args.push(prompt.to_string());
105    }
106
107    if let Some(ref preset) = options.tools_preset {
108        let preset_name = if preset.preset.is_empty() || preset.preset == "claude_code" {
109            "default"
110        } else {
111            &preset.preset
112        };
113        args.push("--tools".to_string());
114        args.push(preset_name.to_string());
115    } else if options.tools_set {
116        args.push("--tools".to_string());
117        args.push(options.tools.join(","));
118    }
119
120    let allowed_tools = effective_allowed_tools(options);
121    if !allowed_tools.is_empty() {
122        args.push("--allowedTools".to_string());
123        args.push(allowed_tools.join(","));
124    }
125
126    if !options.disallowed_tools.is_empty() {
127        args.push("--disallowedTools".to_string());
128        args.push(options.disallowed_tools.join(","));
129    }
130
131    if let Some(turns) = options.max_turns {
132        args.push("--max-turns".to_string());
133        args.push(turns.to_string());
134    }
135
136    if let Some(budget) = options.max_budget_usd {
137        args.push("--max-budget-usd".to_string());
138        args.push(budget.to_string());
139    }
140
141    if let Some(ref model) = options.model {
142        args.push("--model".to_string());
143        args.push(model.clone());
144    }
145
146    if let Some(ref fallback) = options.fallback_model {
147        args.push("--fallback-model".to_string());
148        args.push(fallback.clone());
149    }
150
151    if !options.betas.is_empty() {
152        args.push("--betas".to_string());
153        let betas: Vec<String> = options
154            .betas
155            .iter()
156            .map(serialize_cli_value)
157            .collect::<Result<Vec<_>>>()?;
158        args.push(betas.join(","));
159    }
160
161    if let Some(ref name) = options.permission_prompt_tool_name {
162        args.push("--permission-prompt-tool".to_string());
163        args.push(name.clone());
164    }
165
166    if let Some(mode) = options.permission_mode {
167        args.push("--permission-mode".to_string());
168        args.push(serialize_cli_value(&mode)?);
169    }
170
171    if options.continue_conversation {
172        args.push("--continue".to_string());
173    }
174
175    if let Some(ref resume) = options.resume {
176        args.push("--resume".to_string());
177        args.push(resume.clone());
178    }
179
180    if let Some(ref session_id) = options.session_id {
181        args.push("--session-id".to_string());
182        args.push(session_id.clone());
183    }
184
185    if let Some(ref task_budget) = options.task_budget {
186        args.push("--task-budget".to_string());
187        args.push(task_budget.total.to_string());
188    }
189
190    if let Some(settings) = build_settings_value(options)? {
191        args.push("--settings".to_string());
192        args.push(settings);
193    }
194
195    for dir in &options.add_dirs {
196        args.push("--add-dir".to_string());
197        args.push(dir.clone());
198    }
199
200    if let Some(config) = &options.mcp_servers_config {
201        args.push("--mcp-config".to_string());
202        args.push(config.clone());
203    } else if !options.mcp_servers.is_empty() {
204        let mcp_config = serde_json::json!({
205            "mcpServers": options.mcp_servers
206        });
207        args.push("--mcp-config".to_string());
208        args.push(mcp_config.to_string());
209    }
210
211    if options.include_partial_messages {
212        args.push("--include-partial-messages".to_string());
213    }
214
215    if options.include_hook_events {
216        args.push("--include-hook-events".to_string());
217    }
218
219    if options.strict_mcp_config {
220        args.push("--strict-mcp-config".to_string());
221    }
222
223    if options.fork_session {
224        args.push("--fork-session".to_string());
225    }
226
227    if options.session_store_enabled {
228        args.push("--session-mirror".to_string());
229    }
230
231    if let Some(setting_sources) = effective_setting_sources(options) {
232        let setting_sources = serialize_setting_sources(&setting_sources)?;
233        args.push(format!("--setting-sources={setting_sources}"));
234    }
235
236    for plugin in &options.plugins {
237        if plugin.r#type != "local" {
238            return Err(ClaudeSDKError::Other(format!(
239                "Unsupported plugin type: {}",
240                plugin.r#type
241            )));
242        }
243        if !plugin.path.is_empty() {
244            args.push("--plugin-dir".to_string());
245            args.push(plugin.path.clone());
246        }
247    }
248
249    if let Some(ref thinking) = options.thinking {
250        match thinking.r#type {
251            crate::types::ThinkingConfigType::Adaptive => {
252                args.push("--thinking".to_string());
253                args.push("adaptive".to_string());
254            }
255            crate::types::ThinkingConfigType::Enabled => {
256                if let Some(tokens) = thinking.budget_tokens {
257                    args.push("--max-thinking-tokens".to_string());
258                    args.push(tokens.to_string());
259                }
260            }
261            crate::types::ThinkingConfigType::Disabled => {
262                args.push("--thinking".to_string());
263                args.push("disabled".to_string());
264            }
265        }
266        if thinking.r#type != crate::types::ThinkingConfigType::Disabled {
267            if let Some(ref display) = thinking.display {
268                args.push("--thinking-display".to_string());
269                args.push(display.clone());
270            }
271        }
272    } else if let Some(tokens) = options.max_thinking_tokens {
273        args.push("--max-thinking-tokens".to_string());
274        args.push(tokens.to_string());
275    }
276
277    if let Some(ref effort) = options.effort {
278        args.push("--effort".to_string());
279        args.push(effort.clone());
280    }
281
282    if let Some(ref output_format) = options.output_format {
283        if output_format.get("type").and_then(|v| v.as_str()) == Some("json_schema") {
284            if let Some(schema) = output_format.get("schema") {
285                args.push("--json-schema".to_string());
286                args.push(schema.to_string());
287            }
288        }
289    }
290
291    let mut extra_keys: Vec<&String> = options.extra_args.keys().collect();
292    extra_keys.sort();
293    for key in extra_keys {
294        args.push(format!("--{}", key));
295        if let Some(ref value) = options.extra_args[key] {
296            args.push(value.clone());
297        }
298    }
299
300    Ok(args)
301}