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