claude_code_sdk_rust/internal/
cli_args.rs1use 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}