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 permission_prompt_tool: Option<String>,
71}
72
73impl Default for ClaudeCliBuilder {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl ClaudeCliBuilder {
80 pub fn new() -> Self {
82 Self {
83 command: PathBuf::from("claude"),
84 prompt: None,
85 debug: None,
86 verbose: false,
87 dangerously_skip_permissions: false,
88 allowed_tools: Vec::new(),
89 disallowed_tools: Vec::new(),
90 mcp_config: Vec::new(),
91 append_system_prompt: None,
92 permission_mode: None,
93 continue_conversation: false,
94 resume: None,
95 model: None,
96 fallback_model: None,
97 settings: None,
98 add_dir: Vec::new(),
99 ide: false,
100 strict_mcp_config: false,
101 session_id: None,
102 oauth_token: None,
103 api_key: None,
104 permission_prompt_tool: None,
105 }
106 }
107
108 pub fn command<P: Into<PathBuf>>(mut self, path: P) -> Self {
110 self.command = path.into();
111 self
112 }
113
114 pub fn prompt<S: Into<String>>(mut self, prompt: S) -> Self {
116 self.prompt = Some(prompt.into());
117 self
118 }
119
120 pub fn debug<S: Into<String>>(mut self, filter: Option<S>) -> Self {
122 self.debug = filter.map(|s| s.into());
123 self
124 }
125
126 pub fn verbose(mut self, verbose: bool) -> Self {
128 self.verbose = verbose;
129 self
130 }
131
132 pub fn dangerously_skip_permissions(mut self, skip: bool) -> Self {
134 self.dangerously_skip_permissions = skip;
135 self
136 }
137
138 pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
140 where
141 I: IntoIterator<Item = S>,
142 S: Into<String>,
143 {
144 self.allowed_tools
145 .extend(tools.into_iter().map(|s| s.into()));
146 self
147 }
148
149 pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
151 where
152 I: IntoIterator<Item = S>,
153 S: Into<String>,
154 {
155 self.disallowed_tools
156 .extend(tools.into_iter().map(|s| s.into()));
157 self
158 }
159
160 pub fn mcp_config<I, S>(mut self, configs: I) -> Self
162 where
163 I: IntoIterator<Item = S>,
164 S: Into<String>,
165 {
166 self.mcp_config
167 .extend(configs.into_iter().map(|s| s.into()));
168 self
169 }
170
171 pub fn append_system_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
173 self.append_system_prompt = Some(prompt.into());
174 self
175 }
176
177 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
179 self.permission_mode = Some(mode);
180 self
181 }
182
183 pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
185 self.continue_conversation = continue_conv;
186 self
187 }
188
189 pub fn resume<S: Into<String>>(mut self, session_id: Option<S>) -> Self {
191 self.resume = session_id.map(|s| s.into());
192 self
193 }
194
195 pub fn model<S: Into<String>>(mut self, model: S) -> Self {
197 self.model = Some(model.into());
198 self
199 }
200
201 pub fn fallback_model<S: Into<String>>(mut self, model: S) -> Self {
203 self.fallback_model = Some(model.into());
204 self
205 }
206
207 pub fn settings<S: Into<String>>(mut self, settings: S) -> Self {
209 self.settings = Some(settings.into());
210 self
211 }
212
213 pub fn add_directories<I, P>(mut self, dirs: I) -> Self
215 where
216 I: IntoIterator<Item = P>,
217 P: Into<PathBuf>,
218 {
219 self.add_dir.extend(dirs.into_iter().map(|p| p.into()));
220 self
221 }
222
223 pub fn ide(mut self, ide: bool) -> Self {
225 self.ide = ide;
226 self
227 }
228
229 pub fn strict_mcp_config(mut self, strict: bool) -> Self {
231 self.strict_mcp_config = strict;
232 self
233 }
234
235 pub fn session_id(mut self, id: Uuid) -> Self {
237 self.session_id = Some(id);
238 self
239 }
240
241 pub fn oauth_token<S: Into<String>>(mut self, token: S) -> Self {
243 let token_str = token.into();
244 if !token_str.starts_with("sk-ant-oat") {
245 eprintln!("Warning: OAuth token should start with 'sk-ant-oat'");
246 }
247 self.oauth_token = Some(token_str);
248 self
249 }
250
251 pub fn api_key<S: Into<String>>(mut self, key: S) -> Self {
253 let key_str = key.into();
254 if !key_str.starts_with("sk-ant-api") {
255 eprintln!("Warning: API key should start with 'sk-ant-api'");
256 }
257 self.api_key = Some(key_str);
258 self
259 }
260
261 pub fn permission_prompt_tool<S: Into<String>>(mut self, tool: S) -> Self {
276 self.permission_prompt_tool = Some(tool.into());
277 self
278 }
279
280 fn build_args(&self) -> Vec<String> {
282 let mut args = vec![
285 "--print".to_string(),
286 "--verbose".to_string(),
287 "--output-format".to_string(),
288 "stream-json".to_string(),
289 "--input-format".to_string(),
290 "stream-json".to_string(),
291 ];
292
293 if let Some(ref debug) = self.debug {
294 args.push("--debug".to_string());
295 if !debug.is_empty() {
296 args.push(debug.clone());
297 }
298 }
299
300 if self.dangerously_skip_permissions {
301 args.push("--dangerously-skip-permissions".to_string());
302 }
303
304 if !self.allowed_tools.is_empty() {
305 args.push("--allowed-tools".to_string());
306 args.extend(self.allowed_tools.clone());
307 }
308
309 if !self.disallowed_tools.is_empty() {
310 args.push("--disallowed-tools".to_string());
311 args.extend(self.disallowed_tools.clone());
312 }
313
314 if !self.mcp_config.is_empty() {
315 args.push("--mcp-config".to_string());
316 args.extend(self.mcp_config.clone());
317 }
318
319 if let Some(ref prompt) = self.append_system_prompt {
320 args.push("--append-system-prompt".to_string());
321 args.push(prompt.clone());
322 }
323
324 if let Some(ref mode) = self.permission_mode {
325 args.push("--permission-mode".to_string());
326 args.push(mode.as_str().to_string());
327 }
328
329 if self.continue_conversation {
330 args.push("--continue".to_string());
331 }
332
333 if let Some(ref session) = self.resume {
334 args.push("--resume".to_string());
335 args.push(session.clone());
336 }
337
338 if let Some(ref model) = self.model {
339 args.push("--model".to_string());
340 args.push(model.clone());
341 }
342
343 if let Some(ref model) = self.fallback_model {
344 args.push("--fallback-model".to_string());
345 args.push(model.clone());
346 }
347
348 if let Some(ref settings) = self.settings {
349 args.push("--settings".to_string());
350 args.push(settings.clone());
351 }
352
353 if !self.add_dir.is_empty() {
354 args.push("--add-dir".to_string());
355 for dir in &self.add_dir {
356 args.push(dir.to_string_lossy().to_string());
357 }
358 }
359
360 if self.ide {
361 args.push("--ide".to_string());
362 }
363
364 if self.strict_mcp_config {
365 args.push("--strict-mcp-config".to_string());
366 }
367
368 if let Some(ref tool) = self.permission_prompt_tool {
369 args.push("--permission-prompt-tool".to_string());
370 args.push(tool.clone());
371 }
372
373 if self.resume.is_none() && !self.continue_conversation {
377 args.push("--session-id".to_string());
378 let session_uuid = self.session_id.unwrap_or_else(|| {
379 let uuid = Uuid::new_v4();
380 debug!("[CLI] Generated session UUID: {}", uuid);
381 uuid
382 });
383 args.push(session_uuid.to_string());
384 }
385
386 if let Some(ref prompt) = self.prompt {
388 args.push(prompt.clone());
389 }
390
391 args
392 }
393
394 #[cfg(feature = "async-client")]
396 pub async fn spawn(self) -> Result<tokio::process::Child> {
397 let args = self.build_args();
398
399 debug!(
401 "[CLI] Executing command: {} {}",
402 self.command.display(),
403 args.join(" ")
404 );
405
406 let mut cmd = tokio::process::Command::new(&self.command);
407 cmd.args(&args)
408 .stdin(Stdio::piped())
409 .stdout(Stdio::piped())
410 .stderr(Stdio::piped());
411
412 if let Some(ref token) = self.oauth_token {
414 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
415 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
416 }
417
418 if let Some(ref key) = self.api_key {
420 cmd.env("ANTHROPIC_API_KEY", key);
421 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
422 }
423
424 let child = cmd.spawn().map_err(Error::Io)?;
425
426 Ok(child)
427 }
428
429 #[cfg(feature = "async-client")]
431 pub fn build_command(self) -> tokio::process::Command {
432 let args = self.build_args();
433 let mut cmd = tokio::process::Command::new(&self.command);
434 cmd.args(&args)
435 .stdin(Stdio::piped())
436 .stdout(Stdio::piped())
437 .stderr(Stdio::piped());
438
439 if let Some(ref token) = self.oauth_token {
441 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
442 }
443
444 if let Some(ref key) = self.api_key {
446 cmd.env("ANTHROPIC_API_KEY", key);
447 }
448
449 cmd
450 }
451
452 pub fn spawn_sync(self) -> std::io::Result<std::process::Child> {
454 let args = self.build_args();
455
456 debug!(
458 "[CLI] Executing sync command: {} {}",
459 self.command.display(),
460 args.join(" ")
461 );
462
463 let mut cmd = std::process::Command::new(&self.command);
464 cmd.args(&args)
465 .stdin(Stdio::piped())
466 .stdout(Stdio::piped())
467 .stderr(Stdio::piped());
468
469 if let Some(ref token) = self.oauth_token {
471 cmd.env("CLAUDE_CODE_OAUTH_TOKEN", token);
472 debug!("[CLI] Setting CLAUDE_CODE_OAUTH_TOKEN environment variable");
473 }
474
475 if let Some(ref key) = self.api_key {
477 cmd.env("ANTHROPIC_API_KEY", key);
478 debug!("[CLI] Setting ANTHROPIC_API_KEY environment variable");
479 }
480
481 cmd.spawn()
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_streaming_flags_always_present() {
491 let builder = ClaudeCliBuilder::new();
492 let args = builder.build_args();
493
494 assert!(args.contains(&"--print".to_string()));
496 assert!(args.contains(&"--verbose".to_string())); assert!(args.contains(&"--output-format".to_string()));
498 assert!(args.contains(&"stream-json".to_string()));
499 assert!(args.contains(&"--input-format".to_string()));
500 }
501
502 #[test]
503 fn test_with_prompt() {
504 let builder = ClaudeCliBuilder::new().prompt("Hello, Claude!");
505 let args = builder.build_args();
506
507 assert_eq!(args.last().unwrap(), "Hello, Claude!");
508 }
509
510 #[test]
511 fn test_with_model() {
512 let builder = ClaudeCliBuilder::new()
513 .model("sonnet")
514 .fallback_model("opus");
515 let args = builder.build_args();
516
517 assert!(args.contains(&"--model".to_string()));
518 assert!(args.contains(&"sonnet".to_string()));
519 assert!(args.contains(&"--fallback-model".to_string()));
520 assert!(args.contains(&"opus".to_string()));
521 }
522
523 #[test]
524 fn test_with_debug() {
525 let builder = ClaudeCliBuilder::new().debug(Some("api"));
526 let args = builder.build_args();
527
528 assert!(args.contains(&"--debug".to_string()));
529 assert!(args.contains(&"api".to_string()));
530 }
531
532 #[test]
533 fn test_with_oauth_token() {
534 let valid_token = "sk-ant-oat-123456789";
535 let builder = ClaudeCliBuilder::new().oauth_token(valid_token);
536
537 let args = builder.clone().build_args();
539 assert!(!args.contains(&valid_token.to_string()));
540
541 assert_eq!(builder.oauth_token, Some(valid_token.to_string()));
543 }
544
545 #[test]
546 fn test_oauth_token_validation() {
547 let invalid_token = "invalid-token-123";
549 let builder = ClaudeCliBuilder::new().oauth_token(invalid_token);
550 assert_eq!(builder.oauth_token, Some(invalid_token.to_string()));
551 }
552
553 #[test]
554 fn test_with_api_key() {
555 let valid_key = "sk-ant-api-987654321";
556 let builder = ClaudeCliBuilder::new().api_key(valid_key);
557
558 let args = builder.clone().build_args();
560 assert!(!args.contains(&valid_key.to_string()));
561
562 assert_eq!(builder.api_key, Some(valid_key.to_string()));
564 }
565
566 #[test]
567 fn test_api_key_validation() {
568 let invalid_key = "invalid-api-key";
570 let builder = ClaudeCliBuilder::new().api_key(invalid_key);
571 assert_eq!(builder.api_key, Some(invalid_key.to_string()));
572 }
573
574 #[test]
575 fn test_both_auth_methods() {
576 let oauth = "sk-ant-oat-123";
577 let api_key = "sk-ant-api-456";
578 let builder = ClaudeCliBuilder::new().oauth_token(oauth).api_key(api_key);
579
580 assert_eq!(builder.oauth_token, Some(oauth.to_string()));
581 assert_eq!(builder.api_key, Some(api_key.to_string()));
582 }
583
584 #[test]
585 fn test_permission_prompt_tool() {
586 let builder = ClaudeCliBuilder::new().permission_prompt_tool("stdio");
587 let args = builder.build_args();
588
589 assert!(args.contains(&"--permission-prompt-tool".to_string()));
590 assert!(args.contains(&"stdio".to_string()));
591 }
592
593 #[test]
594 fn test_permission_prompt_tool_not_present_by_default() {
595 let builder = ClaudeCliBuilder::new();
596 let args = builder.build_args();
597
598 assert!(!args.contains(&"--permission-prompt-tool".to_string()));
599 }
600
601 #[test]
602 fn test_session_id_present_for_new_session() {
603 let builder = ClaudeCliBuilder::new();
604 let args = builder.build_args();
605
606 assert!(
607 args.contains(&"--session-id".to_string()),
608 "New sessions should have --session-id"
609 );
610 }
611
612 #[test]
613 fn test_session_id_not_present_with_resume() {
614 let builder = ClaudeCliBuilder::new().resume(Some("existing-uuid".to_string()));
617 let args = builder.build_args();
618
619 assert!(
620 args.contains(&"--resume".to_string()),
621 "Should have --resume flag"
622 );
623 assert!(
624 !args.contains(&"--session-id".to_string()),
625 "--session-id should NOT be present when resuming"
626 );
627 }
628
629 #[test]
630 fn test_session_id_not_present_with_continue() {
631 let builder = ClaudeCliBuilder::new().continue_conversation(true);
633 let args = builder.build_args();
634
635 assert!(
636 args.contains(&"--continue".to_string()),
637 "Should have --continue flag"
638 );
639 assert!(
640 !args.contains(&"--session-id".to_string()),
641 "--session-id should NOT be present when continuing"
642 );
643 }
644}