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