1#[cfg(feature = "async-client")]
13use crate::error::{Error, Result};
14use log::debug;
15use std::path::PathBuf;
16use std::process::Stdio;
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Copy)]
21pub enum PermissionMode {
22 AcceptEdits,
23 BypassPermissions,
24 Default,
25 Plan,
26}
27
28impl PermissionMode {
29 fn as_str(&self) -> &'static str {
30 match self {
31 PermissionMode::AcceptEdits => "acceptEdits",
32 PermissionMode::BypassPermissions => "bypassPermissions",
33 PermissionMode::Default => "default",
34 PermissionMode::Plan => "plan",
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
47pub struct ClaudeCliBuilder {
48 command: PathBuf,
49 prompt: Option<String>,
50 debug: Option<String>,
51 verbose: bool,
52 dangerously_skip_permissions: bool,
53 allowed_tools: Vec<String>,
54 disallowed_tools: Vec<String>,
55 mcp_config: Vec<String>,
56 append_system_prompt: Option<String>,
57 permission_mode: Option<PermissionMode>,
58 continue_conversation: bool,
59 resume: Option<String>,
60 model: Option<String>,
61 fallback_model: Option<String>,
62 settings: Option<String>,
63 add_dir: Vec<PathBuf>,
64 ide: bool,
65 strict_mcp_config: bool,
66 session_id: Option<Uuid>,
67 oauth_token: Option<String>,
68 api_key: Option<String>,
69}
70
71impl Default for ClaudeCliBuilder {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl ClaudeCliBuilder {
78 pub fn new() -> Self {
80 Self {
81 command: PathBuf::from("claude"),
82 prompt: None,
83 debug: None,
84 verbose: false,
85 dangerously_skip_permissions: false,
86 allowed_tools: Vec::new(),
87 disallowed_tools: Vec::new(),
88 mcp_config: Vec::new(),
89 append_system_prompt: None,
90 permission_mode: None,
91 continue_conversation: false,
92 resume: None,
93 model: None,
94 fallback_model: None,
95 settings: None,
96 add_dir: Vec::new(),
97 ide: false,
98 strict_mcp_config: false,
99 session_id: None,
100 oauth_token: None,
101 api_key: None,
102 }
103 }
104
105 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
107 self.command = path.into();
108 self
109 }
110
111 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
113 self.prompt = Some(prompt.into());
114 self
115 }
116
117 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
119 self.debug = filter.map(|s| s.into());
120 self
121 }
122
123 pub fn verbose(mut self, verbose: bool) -> Self {
125 self.verbose = verbose;
126 self
127 }
128
129 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
131 self.dangerously_skip_permissions = skip;
132 self
133 }
134
135 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
137 where
138 I: IntoIterator<Item = S>,
139 S: Into<String>,
140 {
141 self.allowed_tools
142 .extend(tools.into_iter().map(|s| s.into()));
143 self
144 }
145
146 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
148 where
149 I: IntoIterator<Item = S>,
150 S: Into<String>,
151 {
152 self.disallowed_tools
153 .extend(tools.into_iter().map(|s| s.into()));
154 self
155 }
156
157 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
159 where
160 I: IntoIterator<Item = S>,
161 S: Into<String>,
162 {
163 self.mcp_config
164 .extend(configs.into_iter().map(|s| s.into()));
165 self
166 }
167
168 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
170 self.append_system_prompt = Some(prompt.into());
171 self
172 }
173
174 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
176 self.permission_mode = Some(mode);
177 self
178 }
179
180 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
182 self.continue_conversation = continue_conv;
183 self
184 }
185
186 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
188 self.resume = session_id.map(|s| s.into());
189 self
190 }
191
192 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
194 self.model = Some(model.into());
195 self
196 }
197
198 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
200 self.fallback_model = Some(model.into());
201 self
202 }
203
204 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
206 self.settings = Some(settings.into());
207 self
208 }
209
210 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
212 where
213 I: IntoIterator<Item = P>,
214 P: Into<PathBuf>,
215 {
216 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
217 self
218 }
219
220 pub fn ide(mut self, ide: bool) -> Self {
222 self.ide = ide;
223 self
224 }
225
226 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
228 self.strict_mcp_config = strict;
229 self
230 }
231
232 pub fn session_id(mut self, id: Uuid) -> Self {
234 self.session_id = Some(id);
235 self
236 }
237
238 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
240 let token_str = token.into();
241 if !token_str.starts_with("sk-ant-oat") {
242 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
243 }
244 self.oauth_token = Some(token_str);
245 self
246 }
247
248 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
250 let key_str = key.into();
251 if !key_str.starts_with("sk-ant-api") {
252 eprintln!("Warning: API key should start with 'sk-ant-api'");
253 }
254 self.api_key = Some(key_str);
255 self
256 }
257
258 fn build_args(&self) -> Vec<String> {
260 let mut args = vec![
263 "--print".to_string(),
264 "--verbose".to_string(),
265 "--output-format".to_string(),
266 "stream-json".to_string(),
267 "--input-format".to_string(),
268 "stream-json".to_string(),
269 ];
270
271 if let Some(ref debug) = self.debug {
272 args.push("--debug".to_string());
273 if !debug.is_empty() {
274 args.push(debug.clone());
275 }
276 }
277
278 if self.dangerously_skip_permissions {
279 args.push("--dangerously-skip-permissions".to_string());
280 }
281
282 if !self.allowed_tools.is_empty() {
283 args.push("--allowed-tools".to_string());
284 args.extend(self.allowed_tools.clone());
285 }
286
287 if !self.disallowed_tools.is_empty() {
288 args.push("--disallowed-tools".to_string());
289 args.extend(self.disallowed_tools.clone());
290 }
291
292 if !self.mcp_config.is_empty() {
293 args.push("--mcp-config".to_string());
294 args.extend(self.mcp_config.clone());
295 }
296
297 if let Some(ref prompt) = self.append_system_prompt {
298 args.push("--append-system-prompt".to_string());
299 args.push(prompt.clone());
300 }
301
302 if let Some(ref mode) = self.permission_mode {
303 args.push("--permission-mode".to_string());
304 args.push(mode.as_str().to_string());
305 }
306
307 if self.continue_conversation {
308 args.push("--continue".to_string());
309 }
310
311 if let Some(ref session) = self.resume {
312 args.push("--resume".to_string());
313 args.push(session.clone());
314 }
315
316 if let Some(ref model) = self.model {
317 args.push("--model".to_string());
318 args.push(model.clone());
319 }
320
321 if let Some(ref model) = self.fallback_model {
322 args.push("--fallback-model".to_string());
323 args.push(model.clone());
324 }
325
326 if let Some(ref settings) = self.settings {
327 args.push("--settings".to_string());
328 args.push(settings.clone());
329 }
330
331 if !self.add_dir.is_empty() {
332 args.push("--add-dir".to_string());
333 for dir in &self.add_dir {
334 args.push(dir.to_string_lossy().to_string());
335 }
336 }
337
338 if self.ide {
339 args.push("--ide".to_string());
340 }
341
342 if self.strict_mcp_config {
343 args.push("--strict-mcp-config".to_string());
344 }
345
346 args.push("--session-id".to_string());
348 let session_uuid = self.session_id.unwrap_or_else(|| {
349 let uuid = Uuid::new_v4();
350 debug!("[CLI] Generated session UUID: {}", uuid);
351 uuid
352 });
353 args.push(session_uuid.to_string());
354
355 if let Some(ref prompt) = self.prompt {
357 args.push(prompt.clone());
358 }
359
360 args
361 }
362
363 #[cfg(feature = "async-client")]
365 pub async fn spawn(self) -> Result<tokio::process::Child> {
366 let args = self.build_args();
367
368 debug!(
370 "[CLI] Executing command: {} {}",
371 self.command.display(),
372 args.join(" ")
373 );
374
375 let mut cmd = tokio::process::Command::new(&self.command);
376 cmd.args(&args)
377 .stdin(Stdio::piped())
378 .stdout(Stdio::piped())
379 .stderr(Stdio::piped());
380
381 if let Some(ref token) = self.oauth_token {
383 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
384 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
385 }
386
387 if let Some(ref key) = self.api_key {
389 cmd.env("ANTHROPIC_API_KEY", key);
390 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
391 }
392
393 let child = cmd.spawn().map_err(Error::Io)?;
394
395 Ok(child)
396 }
397
398 #[cfg(feature = "async-client")]
400 pub fn build_command(self) -> tokio::process::Command {
401 let args = self.build_args();
402 let mut cmd = tokio::process::Command::new(&self.command);
403 cmd.args(&args)
404 .stdin(Stdio::piped())
405 .stdout(Stdio::piped())
406 .stderr(Stdio::piped());
407
408 if let Some(ref token) = self.oauth_token {
410 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
411 }
412
413 if let Some(ref key) = self.api_key {
415 cmd.env("ANTHROPIC_API_KEY", key);
416 }
417
418 cmd
419 }
420
421 pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
423 let args = self.build_args();
424
425 debug!(
427 "[CLI] Executing sync command: {} {}",
428 self.command.display(),
429 args.join(" ")
430 );
431
432 let mut cmd = std::process::Command::new(&self.command);
433 cmd.args(&args)
434 .stdin(Stdio::piped())
435 .stdout(Stdio::piped())
436 .stderr(Stdio::piped());
437
438 if let Some(ref token) = self.oauth_token {
440 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
441 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
442 }
443
444 if let Some(ref key) = self.api_key {
446 cmd.env("ANTHROPIC_API_KEY", key);
447 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
448 }
449
450 cmd.spawn()
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_streaming_flags_always_present() {
460 let builder = ClaudeCliBuilder::new();
461 let args = builder.build_args();
462
463 assert!(args.contains(&"--print".to_string()));
465 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
467 assert!(args.contains(&"stream-json".to_string()));
468 assert!(args.contains(&"--input-format".to_string()));
469 }
470
471 #[test]
472 fn test_with_prompt() {
473 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
474 let args = builder.build_args();
475
476 assert_eq!(args.last().unwrap(), "Hello, Claude!");
477 }
478
479 #[test]
480 fn test_with_model() {
481 let builder = ClaudeCliBuilder::new()
482 .model("sonnet")
483 .fallback_model("opus");
484 let args = builder.build_args();
485
486 assert!(args.contains(&"--model".to_string()));
487 assert!(args.contains(&"sonnet".to_string()));
488 assert!(args.contains(&"--fallback-model".to_string()));
489 assert!(args.contains(&"opus".to_string()));
490 }
491
492 #[test]
493 fn test_with_debug() {
494 let builder = ClaudeCliBuilder::new().debug(Some("api"));
495 let args = builder.build_args();
496
497 assert!(args.contains(&"--debug".to_string()));
498 assert!(args.contains(&"api".to_string()));
499 }
500
501 #[test]
502 fn test_with_oauth_token() {
503 let valid_token = "sk-ant-oat-123456789";
504 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
505
506 let args = builder.clone().build_args();
508 assert!(!args.contains(&valid_token.to_string()));
509
510 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
512 }
513
514 #[test]
515 fn test_oauth_token_validation() {
516 let invalid_token = "invalid-token-123";
518 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
519 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
520 }
521
522 #[test]
523 fn test_with_api_key() {
524 let valid_key = "sk-ant-api-987654321";
525 let builder = ClaudeCliBuilder::new().api_key(valid_key);
526
527 let args = builder.clone().build_args();
529 assert!(!args.contains(&valid_key.to_string()));
530
531 assert_eq!(builder.api_key, Some(valid_key.to_string()));
533 }
534
535 #[test]
536 fn test_api_key_validation() {
537 let invalid_key = "invalid-api-key";
539 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
540 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
541 }
542
543 #[test]
544 fn test_both_auth_methods() {
545 let oauth = "sk-ant-oat-123";
546 let api_key = "sk-ant-api-456";
547 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
548
549 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
550 assert_eq!(builder.api_key, Some(api_key.to_string()));
551 }
552}