1use crate::cli::completion::{generate_completion, list_aliases_for_completion};
2use crate::cli::{Cli, Commands};
3use crate::config::types::{AddCommandParams, ClaudeSettings, StorageMode};
4use crate::config::{ConfigStorage, Configuration, EnvironmentConfig, validate_alias_name};
5use crate::interactive::{
6 handle_interactive_selection, launch_claude_with_env, read_input, read_sensitive_input,
7};
8use anyhow::{Result, anyhow};
9use clap::Parser;
10use std::fs;
11use std::path::Path;
12
13fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
21 match store_str.to_lowercase().as_str() {
22 "env" => Ok(StorageMode::Env),
23 "config" => Ok(StorageMode::Config),
24 _ => Err(anyhow!(
25 "Invalid storage mode '{}'. Use 'env' or 'config'",
26 store_str
27 )),
28 }
29}
30
31#[allow(clippy::type_complexity)]
42fn parse_config_from_file(
43 file_path: &str,
44) -> Result<(
45 String,
46 String,
47 String,
48 Option<String>,
49 Option<String>,
50 Option<u32>,
51 Option<u32>,
52 Option<u32>,
53 Option<String>,
54 Option<String>,
55 Option<String>,
56 Option<String>,
57 Option<u32>,
58 Option<String>,
59)> {
60 let file_content = fs::read_to_string(file_path)
62 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
63
64 let json: serde_json::Value = serde_json::from_str(&file_content)
66 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
67
68 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
70 anyhow!(
71 "File '{}' does not contain a valid 'env' section",
72 file_path
73 )
74 })?;
75
76 let path = Path::new(file_path);
78 let alias_name = path
79 .file_stem()
80 .and_then(|s| s.to_str())
81 .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
82 .to_string();
83
84 let token = env
86 .get("ANTHROPIC_AUTH_TOKEN")
87 .and_then(|v| v.as_str())
88 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
89 .to_string();
90
91 let url = env
92 .get("ANTHROPIC_BASE_URL")
93 .and_then(|v| v.as_str())
94 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
95 .to_string();
96
97 let model = env
98 .get("ANTHROPIC_MODEL")
99 .and_then(|v| v.as_str())
100 .map(|s| s.to_string());
101
102 let small_fast_model = env
103 .get("ANTHROPIC_SMALL_FAST_MODEL")
104 .and_then(|v| v.as_str())
105 .map(|s| s.to_string());
106
107 let max_thinking_tokens = env
108 .get("ANTHROPIC_MAX_THINKING_TOKENS")
109 .and_then(|v| v.as_u64())
110 .map(|u| u as u32);
111
112 let api_timeout_ms = env
113 .get("API_TIMEOUT_MS")
114 .and_then(|v| v.as_u64())
115 .map(|u| u as u32);
116
117 let claude_code_disable_nonessential_traffic = env
118 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
119 .and_then(|v| v.as_u64())
120 .map(|u| u as u32);
121
122 let anthropic_default_sonnet_model = env
123 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
124 .and_then(|v| v.as_str())
125 .map(|s| s.to_string());
126
127 let anthropic_default_opus_model = env
128 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
129 .and_then(|v| v.as_str())
130 .map(|s| s.to_string());
131
132 let anthropic_default_haiku_model = env
133 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
134 .and_then(|v| v.as_str())
135 .map(|s| s.to_string());
136
137 let claude_code_subagent_model = env
138 .get("CLAUDE_CODE_SUBAGENT_MODEL")
139 .and_then(|v| v.as_str())
140 .map(|s| s.to_string());
141
142 let claude_code_disable_nonstreaming_fallback = env
143 .get("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
144 .and_then(|v| v.as_u64())
145 .map(|u| u as u32);
146
147 let claude_code_effort_level = env
148 .get("CLAUDE_CODE_EFFORT_LEVEL")
149 .and_then(|v| v.as_str())
150 .map(|s| s.to_string());
151
152 Ok((
153 alias_name,
154 token,
155 url,
156 model,
157 small_fast_model,
158 max_thinking_tokens,
159 api_timeout_ms,
160 claude_code_disable_nonessential_traffic,
161 anthropic_default_sonnet_model,
162 anthropic_default_opus_model,
163 anthropic_default_haiku_model,
164 claude_code_subagent_model,
165 claude_code_disable_nonstreaming_fallback,
166 claude_code_effort_level,
167 ))
168}
169
170fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
179 if let Some(file_path) = ¶ms.from_file {
181 println!("Importing configuration from file: {}", file_path);
182
183 let (
184 file_alias_name,
185 file_token,
186 file_url,
187 file_model,
188 file_small_fast_model,
189 file_max_thinking_tokens,
190 file_api_timeout_ms,
191 file_claude_disable_nonessential_traffic,
192 file_sonnet_model,
193 file_opus_model,
194 file_haiku_model,
195 file_subagent_model,
196 file_disable_nonstreaming_fallback,
197 file_effort_level,
198 ) = parse_config_from_file(file_path)?;
199
200 params.alias_name = file_alias_name;
202
203 params.token = Some(file_token);
205 params.url = Some(file_url);
206 params.model = file_model;
207 params.small_fast_model = file_small_fast_model;
208 params.max_thinking_tokens = file_max_thinking_tokens;
209 params.api_timeout_ms = file_api_timeout_ms;
210 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
211 params.anthropic_default_sonnet_model = file_sonnet_model;
212 params.anthropic_default_opus_model = file_opus_model;
213 params.anthropic_default_haiku_model = file_haiku_model;
214 params.claude_code_subagent_model = file_subagent_model;
215 params.claude_code_disable_nonstreaming_fallback = file_disable_nonstreaming_fallback;
216 params.claude_code_effort_level = file_effort_level;
217
218 println!(
219 "Configuration '{}' will be imported from file",
220 params.alias_name
221 );
222 }
223
224 validate_alias_name(¶ms.alias_name)?;
226
227 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
229 eprintln!("Configuration '{}' already exists.", params.alias_name);
230 eprintln!("Use --force to overwrite or choose a different alias name.");
231 return Ok(());
232 }
233
234 if params.interactive && params.from_file.is_some() {
236 anyhow::bail!("Cannot use --interactive mode with --from-file");
237 }
238
239 let final_token = if params.interactive {
241 if params.token.is_some() || params.token_arg.is_some() {
242 eprintln!(
243 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
244 );
245 }
246 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
247 } else {
248 match (¶ms.token, ¶ms.token_arg) {
249 (Some(t), _) => t.clone(),
250 (None, Some(t)) => t.clone(),
251 (None, None) => {
252 anyhow::bail!(
253 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
254 );
255 }
256 }
257 };
258
259 let final_url = if params.interactive {
261 if params.url.is_some() || params.url_arg.is_some() {
262 eprintln!(
263 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
264 );
265 }
266 read_input("Enter API URL (default: https://api.anthropic.com): ")?
267 } else {
268 match (¶ms.url, ¶ms.url_arg) {
269 (Some(u), _) => u.clone(),
270 (None, Some(u)) => u.clone(),
271 (None, None) => "https://api.anthropic.com".to_string(),
272 }
273 };
274
275 let final_url = if final_url.is_empty() {
277 "https://api.anthropic.com".to_string()
278 } else {
279 final_url
280 };
281
282 let final_model = if params.interactive {
284 if params.model.is_some() {
285 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
286 }
287 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
288 if model_input.is_empty() {
289 None
290 } else {
291 Some(model_input)
292 }
293 } else {
294 params.model
295 };
296
297 let final_small_fast_model = if params.interactive {
299 if params.small_fast_model.is_some() {
300 eprintln!(
301 "Warning: Small fast model provided via flags will be ignored in interactive mode"
302 );
303 }
304 let small_model_input =
305 read_input("Enter small fast model name (optional, press enter to skip): ")?;
306 if small_model_input.is_empty() {
307 None
308 } else {
309 Some(small_model_input)
310 }
311 } else {
312 params.small_fast_model
313 };
314
315 let final_max_thinking_tokens = if params.interactive {
317 if params.max_thinking_tokens.is_some() {
318 eprintln!(
319 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
320 );
321 }
322 let tokens_input = read_input(
323 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
324 )?;
325 if tokens_input.is_empty() {
326 None
327 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
328 if tokens == 0 { None } else { Some(tokens) }
329 } else {
330 eprintln!("Warning: Invalid max thinking tokens value, skipping");
331 None
332 }
333 } else {
334 params.max_thinking_tokens
335 };
336
337 let final_api_timeout_ms = if params.interactive {
339 if params.api_timeout_ms.is_some() {
340 eprintln!(
341 "Warning: API timeout provided via flags will be ignored in interactive mode"
342 );
343 }
344 let timeout_input = read_input(
345 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
346 )?;
347 if timeout_input.is_empty() {
348 None
349 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
350 if timeout == 0 { None } else { Some(timeout) }
351 } else {
352 eprintln!("Warning: Invalid API timeout value, skipping");
353 None
354 }
355 } else {
356 params.api_timeout_ms
357 };
358
359 let final_claude_code_disable_nonessential_traffic = if params.interactive {
361 if params.claude_code_disable_nonessential_traffic.is_some() {
362 eprintln!(
363 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
364 );
365 }
366 let flag_input = read_input(
367 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
368 )?;
369 if flag_input.is_empty() {
370 None
371 } else if let Ok(flag) = flag_input.parse::<u32>() {
372 if flag == 0 { None } else { Some(flag) }
373 } else {
374 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
375 None
376 }
377 } else {
378 params.claude_code_disable_nonessential_traffic
379 };
380
381 let final_anthropic_default_sonnet_model = if params.interactive {
383 if params.anthropic_default_sonnet_model.is_some() {
384 eprintln!(
385 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
386 );
387 }
388 let model_input =
389 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
390 if model_input.is_empty() {
391 None
392 } else {
393 Some(model_input)
394 }
395 } else {
396 params.anthropic_default_sonnet_model
397 };
398
399 let final_anthropic_default_opus_model = if params.interactive {
401 if params.anthropic_default_opus_model.is_some() {
402 eprintln!(
403 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
404 );
405 }
406 let model_input =
407 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
408 if model_input.is_empty() {
409 None
410 } else {
411 Some(model_input)
412 }
413 } else {
414 params.anthropic_default_opus_model
415 };
416
417 let final_anthropic_default_haiku_model = if params.interactive {
419 if params.anthropic_default_haiku_model.is_some() {
420 eprintln!(
421 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
422 );
423 }
424 let model_input =
425 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
426 if model_input.is_empty() {
427 None
428 } else {
429 Some(model_input)
430 }
431 } else {
432 params.anthropic_default_haiku_model
433 };
434
435 let final_claude_code_subagent_model = if params.interactive {
437 if params.claude_code_subagent_model.is_some() {
438 eprintln!(
439 "Warning: Subagent model provided via flags will be ignored in interactive mode"
440 );
441 }
442 let model_input =
443 read_input("Enter subagent model name (optional, press enter to skip): ")?;
444 if model_input.is_empty() {
445 None
446 } else {
447 Some(model_input)
448 }
449 } else {
450 params.claude_code_subagent_model
451 };
452
453 let final_claude_code_disable_nonstreaming_fallback = if params.interactive {
455 if params.claude_code_disable_nonstreaming_fallback.is_some() {
456 eprintln!(
457 "Warning: Disable non-streaming fallback flag provided via flags will be ignored in interactive mode"
458 );
459 }
460 let flag_input = read_input(
461 "Enter disable non-streaming fallback flag (optional, press enter to skip, enter 0 to clear): ",
462 )?;
463 if flag_input.is_empty() {
464 None
465 } else if let Ok(flag) = flag_input.parse::<u32>() {
466 if flag == 0 { None } else { Some(flag) }
467 } else {
468 eprintln!("Warning: Invalid disable non-streaming fallback flag value, skipping");
469 None
470 }
471 } else {
472 params.claude_code_disable_nonstreaming_fallback
473 };
474
475 let final_claude_code_effort_level = if params.interactive {
477 if params.claude_code_effort_level.is_some() {
478 eprintln!(
479 "Warning: Effort level provided via flags will be ignored in interactive mode"
480 );
481 }
482 let level_input = read_input(
483 "Enter effort level (optional, press enter to skip): ",
484 )?;
485 if level_input.is_empty() {
486 None
487 } else {
488 Some(level_input)
489 }
490 } else {
491 params.claude_code_effort_level
492 };
493
494 let is_anthropic_official = final_url.contains("api.anthropic.com");
496 if is_anthropic_official {
497 if !final_token.starts_with("sk-ant-api03-") {
498 eprintln!(
499 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
500 );
501 }
502 } else {
503 if final_token.starts_with("sk-ant-api03-") {
505 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
506 }
507 }
509
510 let config = Configuration {
512 alias_name: params.alias_name.clone(),
513 token: final_token,
514 url: final_url,
515 model: final_model,
516 small_fast_model: final_small_fast_model,
517 max_thinking_tokens: final_max_thinking_tokens,
518 api_timeout_ms: final_api_timeout_ms,
519 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
520 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
521 anthropic_default_opus_model: final_anthropic_default_opus_model,
522 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
523 claude_code_subagent_model: final_claude_code_subagent_model,
524 claude_code_disable_nonstreaming_fallback: final_claude_code_disable_nonstreaming_fallback,
525 claude_code_effort_level: final_claude_code_effort_level,
526 claude_code_experimental_agent_teams: None,
527 claude_code_disable_1m_context: None,
528 };
529
530 storage.add_configuration(config);
531 storage.save()?;
532
533 println!("Configuration '{}' added successfully", params.alias_name);
534 if params.force {
535 println!("(Overwrote existing configuration)");
536 }
537
538 Ok(())
539}
540
541pub fn run() -> Result<()> {
551 let cli = Cli::parse();
552
553 if cli.migrate {
555 ConfigStorage::migrate_from_old_path()?;
556 return Ok(());
557 }
558
559 if cli.list_aliases {
561 list_aliases_for_completion()?;
562 return Ok(());
563 }
564
565 if let Some(ref store_str) = cli.store
567 && cli.command.is_none()
568 {
569 let mode = match parse_storage_mode(store_str) {
571 Ok(mode) => mode,
572 Err(e) => {
573 eprintln!("Error: {}", e);
574 std::process::exit(1);
575 }
576 };
577
578 let mut storage = ConfigStorage::load()?;
579 storage.default_storage_mode = Some(mode.clone());
580 storage.save()?;
581
582 let mode_str = match mode {
583 StorageMode::Env => "env",
584 StorageMode::Config => "config",
585 };
586
587 println!("Default storage mode set to: {}", mode_str);
588 return Ok(());
589 }
590
591 if let Some(command) = cli.command {
593 let mut storage = ConfigStorage::load()?;
594
595 match command {
596 Commands::Add {
597 alias_name,
598 token,
599 url,
600 model,
601 small_fast_model,
602 max_thinking_tokens,
603 api_timeout_ms,
604 claude_code_disable_nonessential_traffic,
605 anthropic_default_sonnet_model,
606 anthropic_default_opus_model,
607 anthropic_default_haiku_model,
608 claude_code_subagent_model,
609 claude_code_disable_nonstreaming_fallback,
610 claude_code_effort_level,
611 force,
612 interactive,
613 token_arg,
614 url_arg,
615 from_file,
616 } => {
617 let final_alias_name = if from_file.is_some() {
620 "placeholder".to_string()
622 } else {
623 alias_name.unwrap_or_else(|| {
624 eprintln!("Error: alias_name is required when not using --from-file");
625 std::process::exit(1);
626 })
627 };
628
629 let params = AddCommandParams {
630 alias_name: final_alias_name,
631 token,
632 url,
633 model,
634 small_fast_model,
635 max_thinking_tokens,
636 api_timeout_ms,
637 claude_code_disable_nonessential_traffic,
638 anthropic_default_sonnet_model,
639 anthropic_default_opus_model,
640 anthropic_default_haiku_model,
641 claude_code_subagent_model,
642 claude_code_disable_nonstreaming_fallback,
643 claude_code_effort_level,
644 force,
645 interactive,
646 token_arg,
647 url_arg,
648 from_file,
649 };
650 handle_add_command(params, &mut storage)?;
651 }
652 Commands::Remove { alias_names } => {
653 let mut removed_count = 0;
654 let mut not_found_aliases = Vec::new();
655
656 for alias_name in &alias_names {
657 if storage.remove_configuration(alias_name) {
658 removed_count += 1;
659 println!("Configuration '{alias_name}' removed successfully");
660 } else {
661 not_found_aliases.push(alias_name.clone());
662 println!("Configuration '{alias_name}' not found");
663 }
664 }
665
666 if removed_count > 0 {
667 storage.save()?;
668 }
669
670 if !not_found_aliases.is_empty() {
671 eprintln!(
672 "Warning: The following configurations were not found: {}",
673 not_found_aliases.join(", ")
674 );
675 }
676
677 if removed_count > 0 {
678 println!("Successfully removed {removed_count} configuration(s)");
679 }
680 }
681 Commands::List { plain } => {
682 if plain {
683 if storage.configurations.is_empty() {
685 println!("No configurations stored");
686 } else {
687 println!("Stored configurations:");
688 for (alias_name, config) in &storage.configurations {
689 let mut info = format!("token={}, url={}", config.token, config.url);
690 if let Some(model) = &config.model {
691 info.push_str(&format!(", model={model}"));
692 }
693 if let Some(small_fast_model) = &config.small_fast_model {
694 info.push_str(&format!(", small_fast_model={small_fast_model}"));
695 }
696 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
697 info.push_str(&format!(
698 ", max_thinking_tokens={max_thinking_tokens}"
699 ));
700 }
701 if let Some(subagent_model) = &config.claude_code_subagent_model {
702 info.push_str(&format!(", subagent_model={subagent_model}"));
703 }
704 if let Some(flag) = config.claude_code_disable_nonstreaming_fallback {
705 info.push_str(&format!(
706 ", disable_nonstreaming_fallback={flag}"
707 ));
708 }
709 if let Some(effort_level) = &config.claude_code_effort_level {
710 info.push_str(&format!(", effort_level={effort_level}"));
711 }
712 println!(" {alias_name}: {info}");
713 }
714 }
715 } else {
716 println!(
718 "{}",
719 serde_json::to_string_pretty(&storage.configurations)
720 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
721 );
722 }
723 }
724 Commands::Completion { shell } => {
725 generate_completion(&shell)?;
726 }
727 Commands::Use {
728 alias_name,
729 resume,
730 r#continue,
731 prompt,
732 } => {
733 if alias_name == "cc" || alias_name == "official" {
735 println!("Using official Claude configuration");
736
737 let mut settings =
738 ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
739 settings.remove_anthropic_env();
740 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
741
742 launch_claude_with_env(
743 EnvironmentConfig::empty(),
744 None,
745 None,
746 r#continue,
747 )?;
748 return Ok(());
749 }
750
751 let config = storage
752 .configurations
753 .get(&alias_name)
754 .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
755 .clone();
756
757 let env_config = EnvironmentConfig::from_config(&config);
758 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
759
760 let mut settings =
762 ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
763 settings.switch_to_config_with_mode(
764 &config,
765 storage_mode,
766 storage.get_claude_settings_dir().map(|s| s.as_str()),
767 )?;
768
769 println!("Switched to configuration '{}'", alias_name);
770 println!(" URL: {}", config.url);
771 println!(
772 " Token: {}",
773 crate::cli::display_utils::format_token_for_display(&config.token)
774 );
775
776 let prompt_str = if prompt.is_empty() {
777 None
778 } else {
779 Some(prompt.join(" "))
780 };
781
782 launch_claude_with_env(
783 env_config,
784 prompt_str.as_deref(),
785 resume.as_deref(),
786 r#continue,
787 )?;
788 }
789 }
790 } else {
791 let storage = ConfigStorage::load()?;
793 handle_interactive_selection(&storage)?;
794 }
795
796 Ok(())
797}