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;
17
18fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
26 match store_str.to_lowercase().as_str() {
27 "env" => Ok(StorageMode::Env),
28 "config" => Ok(StorageMode::Config),
29 _ => Err(anyhow!(
30 "Invalid storage mode '{}'. Use 'env' or 'config'",
31 store_str
32 )),
33 }
34}
35
36#[allow(clippy::type_complexity)]
47fn parse_config_from_file(
48 file_path: &str,
49) -> Result<(
50 String,
51 String,
52 Option<String>,
53 Option<String>,
54 Option<u32>,
55 Option<u32>,
56 Option<u32>,
57 Option<String>,
58 Option<String>,
59 Option<String>,
60 Option<String>,
61 Option<u32>,
62 Option<String>,
63 Option<u32>,
64 Option<u32>,
65 Option<u32>,
66)> {
67 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)
71 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
72
73 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
74 anyhow!(
75 "File '{}' does not contain a valid 'env' section",
76 file_path
77 )
78 })?;
79
80 let token = env
81 .get("ANTHROPIC_AUTH_TOKEN")
82 .and_then(|v| v.as_str())
83 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
84 .to_string();
85
86 let url = env
87 .get("ANTHROPIC_BASE_URL")
88 .and_then(|v| v.as_str())
89 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
90 .to_string();
91
92 let model = env
93 .get("ANTHROPIC_MODEL")
94 .and_then(|v| v.as_str())
95 .map(|s| s.to_string());
96
97 let small_fast_model = env
98 .get("ANTHROPIC_SMALL_FAST_MODEL")
99 .and_then(|v| v.as_str())
100 .map(|s| s.to_string());
101
102 let max_thinking_tokens = env
103 .get("ANTHROPIC_MAX_THINKING_TOKENS")
104 .and_then(|v| v.as_u64())
105 .map(|u| u as u32);
106
107 let api_timeout_ms = env
108 .get("API_TIMEOUT_MS")
109 .and_then(|v| v.as_u64())
110 .map(|u| u as u32);
111
112 let claude_code_disable_nonessential_traffic = env
113 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
114 .and_then(|v| v.as_u64())
115 .map(|u| u as u32);
116
117 let anthropic_default_sonnet_model = env
118 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
119 .and_then(|v| v.as_str())
120 .map(|s| s.to_string());
121
122 let anthropic_default_opus_model = env
123 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
124 .and_then(|v| v.as_str())
125 .map(|s| s.to_string());
126
127 let anthropic_default_haiku_model = env
128 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
129 .and_then(|v| v.as_str())
130 .map(|s| s.to_string());
131
132 let claude_code_subagent_model = env
133 .get("CLAUDE_CODE_SUBAGENT_MODEL")
134 .and_then(|v| v.as_str())
135 .map(|s| s.to_string());
136
137 let claude_code_disable_nonstreaming_fallback = env
138 .get("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
139 .and_then(|v| v.as_u64())
140 .map(|u| u as u32);
141
142 let claude_code_effort_level = env
143 .get("CLAUDE_CODE_EFFORT_LEVEL")
144 .and_then(|v| v.as_str())
145 .map(|s| s.to_string());
146
147 let disable_prompt_caching = env
148 .get("DISABLE_PROMPT_CACHING")
149 .and_then(|v| v.as_u64())
150 .map(|u| u as u32);
151
152 let claude_code_disable_experimental_betas = env
153 .get("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS")
154 .and_then(|v| v.as_u64())
155 .map(|u| u as u32);
156
157 let disable_autoupdater = env
158 .get("DISABLE_AUTOUPDATER")
159 .and_then(|v| v.as_u64())
160 .map(|u| u as u32);
161
162 Ok((
163 token,
164 url,
165 model,
166 small_fast_model,
167 max_thinking_tokens,
168 api_timeout_ms,
169 claude_code_disable_nonessential_traffic,
170 anthropic_default_sonnet_model,
171 anthropic_default_opus_model,
172 anthropic_default_haiku_model,
173 claude_code_subagent_model,
174 claude_code_disable_nonstreaming_fallback,
175 claude_code_effort_level,
176 disable_prompt_caching,
177 claude_code_disable_experimental_betas,
178 disable_autoupdater,
179 ))
180}
181
182fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
191 if let Some(file_path) = ¶ms.from_file {
193 println!("Importing configuration from file: {}", file_path);
194
195 let (
196 file_token,
197 file_url,
198 file_model,
199 file_small_fast_model,
200 file_max_thinking_tokens,
201 file_api_timeout_ms,
202 file_claude_disable_nonessential_traffic,
203 file_sonnet_model,
204 file_opus_model,
205 file_haiku_model,
206 file_subagent_model,
207 file_disable_nonstreaming_fallback,
208 file_effort_level,
209 file_disable_prompt_caching,
210 file_disable_experimental_betas,
211 file_disable_autoupdater,
212 ) = parse_config_from_file(file_path)?;
213
214 params.token = Some(file_token);
215 params.url = Some(file_url);
216 params.model = file_model;
217 params.small_fast_model = file_small_fast_model;
218 params.max_thinking_tokens = file_max_thinking_tokens;
219 params.api_timeout_ms = file_api_timeout_ms;
220 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
221 params.anthropic_default_sonnet_model = file_sonnet_model;
222 params.anthropic_default_opus_model = file_opus_model;
223 params.anthropic_default_haiku_model = file_haiku_model;
224 params.claude_code_subagent_model = file_subagent_model;
225 params.claude_code_disable_nonstreaming_fallback = file_disable_nonstreaming_fallback;
226 params.claude_code_effort_level = file_effort_level;
227 params.disable_prompt_caching = file_disable_prompt_caching;
228 params.claude_code_disable_experimental_betas = file_disable_experimental_betas;
229 params.disable_autoupdater = file_disable_autoupdater;
230
231 println!(
232 "Configuration '{}' will be imported from file",
233 params.alias_name
234 );
235 }
236
237 validate_alias_name(¶ms.alias_name)?;
239
240 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
242 eprintln!("Configuration '{}' already exists.", params.alias_name);
243 eprintln!("Use --force to overwrite or choose a different alias name.");
244 return Ok(());
245 }
246
247 if params.interactive && params.from_file.is_some() {
249 anyhow::bail!("Cannot use --interactive mode with --from-file");
250 }
251
252 let final_token = if params.interactive {
254 if params.token.is_some() || params.token_arg.is_some() {
255 eprintln!(
256 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
257 );
258 }
259 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
260 } else {
261 match (¶ms.token, ¶ms.token_arg) {
262 (Some(t), _) => t.clone(),
263 (None, Some(t)) => t.clone(),
264 (None, None) => {
265 anyhow::bail!(
266 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
267 );
268 }
269 }
270 };
271
272 let final_url = if params.interactive {
274 if params.url.is_some() || params.url_arg.is_some() {
275 eprintln!(
276 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
277 );
278 }
279 read_input("Enter API URL (default: https://api.anthropic.com): ")?
280 } else {
281 match (¶ms.url, ¶ms.url_arg) {
282 (Some(u), _) => u.clone(),
283 (None, Some(u)) => u.clone(),
284 (None, None) => "https://api.anthropic.com".to_string(),
285 }
286 };
287
288 let final_url = if final_url.is_empty() {
290 "https://api.anthropic.com".to_string()
291 } else {
292 final_url
293 };
294
295 let final_model = if params.interactive {
297 if params.model.is_some() {
298 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
299 }
300 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
301 if model_input.is_empty() {
302 None
303 } else {
304 Some(model_input)
305 }
306 } else {
307 params.model
308 };
309
310 let final_small_fast_model = if params.interactive {
312 if params.small_fast_model.is_some() {
313 eprintln!(
314 "Warning: Small fast model provided via flags will be ignored in interactive mode"
315 );
316 }
317 let small_model_input =
318 read_input("Enter small fast model name (optional, press enter to skip): ")?;
319 if small_model_input.is_empty() {
320 None
321 } else {
322 Some(small_model_input)
323 }
324 } else {
325 params.small_fast_model
326 };
327
328 let final_max_thinking_tokens = if params.interactive {
330 if params.max_thinking_tokens.is_some() {
331 eprintln!(
332 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
333 );
334 }
335 let tokens_input = read_input(
336 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
337 )?;
338 if tokens_input.is_empty() {
339 None
340 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
341 if tokens == 0 { None } else { Some(tokens) }
342 } else {
343 eprintln!("Warning: Invalid max thinking tokens value, skipping");
344 None
345 }
346 } else {
347 params.max_thinking_tokens
348 };
349
350 let final_api_timeout_ms = if params.interactive {
352 if params.api_timeout_ms.is_some() {
353 eprintln!(
354 "Warning: API timeout provided via flags will be ignored in interactive mode"
355 );
356 }
357 let timeout_input = read_input(
358 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
359 )?;
360 if timeout_input.is_empty() {
361 None
362 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
363 if timeout == 0 { None } else { Some(timeout) }
364 } else {
365 eprintln!("Warning: Invalid API timeout value, skipping");
366 None
367 }
368 } else {
369 params.api_timeout_ms
370 };
371
372 let final_claude_code_disable_nonessential_traffic = if params.interactive {
374 if params.claude_code_disable_nonessential_traffic.is_some() {
375 eprintln!(
376 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
377 );
378 }
379 let flag_input = read_input(
380 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
381 )?;
382 if flag_input.is_empty() {
383 None
384 } else if let Ok(flag) = flag_input.parse::<u32>() {
385 if flag == 0 { None } else { Some(flag) }
386 } else {
387 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
388 None
389 }
390 } else {
391 params.claude_code_disable_nonessential_traffic
392 };
393
394 let final_anthropic_default_sonnet_model = if params.interactive {
396 if params.anthropic_default_sonnet_model.is_some() {
397 eprintln!(
398 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
399 );
400 }
401 let model_input =
402 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
403 if model_input.is_empty() {
404 None
405 } else {
406 Some(model_input)
407 }
408 } else {
409 params.anthropic_default_sonnet_model
410 };
411
412 let final_anthropic_default_opus_model = if params.interactive {
414 if params.anthropic_default_opus_model.is_some() {
415 eprintln!(
416 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
417 );
418 }
419 let model_input =
420 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
421 if model_input.is_empty() {
422 None
423 } else {
424 Some(model_input)
425 }
426 } else {
427 params.anthropic_default_opus_model
428 };
429
430 let final_anthropic_default_haiku_model = if params.interactive {
432 if params.anthropic_default_haiku_model.is_some() {
433 eprintln!(
434 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
435 );
436 }
437 let model_input =
438 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
439 if model_input.is_empty() {
440 None
441 } else {
442 Some(model_input)
443 }
444 } else {
445 params.anthropic_default_haiku_model
446 };
447
448 let final_claude_code_subagent_model = if params.interactive {
450 if params.claude_code_subagent_model.is_some() {
451 eprintln!(
452 "Warning: Subagent model provided via flags will be ignored in interactive mode"
453 );
454 }
455 let model_input =
456 read_input("Enter subagent model name (optional, press enter to skip): ")?;
457 if model_input.is_empty() {
458 None
459 } else {
460 Some(model_input)
461 }
462 } else {
463 params.claude_code_subagent_model
464 };
465
466 let final_claude_code_disable_nonstreaming_fallback = if params.interactive {
468 if params.claude_code_disable_nonstreaming_fallback.is_some() {
469 eprintln!(
470 "Warning: Disable non-streaming fallback flag provided via flags will be ignored in interactive mode"
471 );
472 }
473 let flag_input = read_input(
474 "Enter disable non-streaming fallback flag (optional, press enter to skip, enter 0 to clear): ",
475 )?;
476 if flag_input.is_empty() {
477 None
478 } else if let Ok(flag) = flag_input.parse::<u32>() {
479 if flag == 0 { None } else { Some(flag) }
480 } else {
481 eprintln!("Warning: Invalid disable non-streaming fallback flag value, skipping");
482 None
483 }
484 } else {
485 params.claude_code_disable_nonstreaming_fallback
486 };
487
488 let final_claude_code_effort_level = if params.interactive {
490 if params.claude_code_effort_level.is_some() {
491 eprintln!(
492 "Warning: Effort level provided via flags will be ignored in interactive mode"
493 );
494 }
495 let level_input = read_input("Enter effort level (optional, press enter to skip): ")?;
496 if level_input.is_empty() {
497 None
498 } else {
499 Some(level_input)
500 }
501 } else {
502 params.claude_code_effort_level
503 };
504
505 let final_disable_prompt_caching = if params.interactive {
507 if params.disable_prompt_caching.is_some() {
508 eprintln!(
509 "Warning: Disable prompt caching flag provided via flags will be ignored in interactive mode"
510 );
511 }
512 let flag_input = read_input(
513 "Enter disable prompt caching flag (optional, press enter to skip, enter 0 to clear): ",
514 )?;
515 if flag_input.is_empty() {
516 None
517 } else if let Ok(flag) = flag_input.parse::<u32>() {
518 if flag == 0 { None } else { Some(flag) }
519 } else {
520 eprintln!("Warning: Invalid disable prompt caching flag value, skipping");
521 None
522 }
523 } else {
524 params.disable_prompt_caching
525 };
526
527 let final_claude_code_disable_experimental_betas = if params.interactive {
529 if params.claude_code_disable_experimental_betas.is_some() {
530 eprintln!(
531 "Warning: Disable experimental betas flag provided via flags will be ignored in interactive mode"
532 );
533 }
534 let flag_input = read_input(
535 "Enter disable experimental betas flag (optional, press enter to skip, enter 0 to clear): ",
536 )?;
537 if flag_input.is_empty() {
538 None
539 } else if let Ok(flag) = flag_input.parse::<u32>() {
540 if flag == 0 { None } else { Some(flag) }
541 } else {
542 eprintln!("Warning: Invalid disable experimental betas flag value, skipping");
543 None
544 }
545 } else {
546 params.claude_code_disable_experimental_betas
547 };
548
549 let final_disable_autoupdater = if params.interactive {
551 if params.disable_autoupdater.is_some() {
552 eprintln!(
553 "Warning: Disable auto-updater flag provided via flags will be ignored in interactive mode"
554 );
555 }
556 let flag_input = read_input(
557 "Enter disable auto-updater flag (optional, press enter to skip, enter 0 to clear): ",
558 )?;
559 if flag_input.is_empty() {
560 None
561 } else if let Ok(flag) = flag_input.parse::<u32>() {
562 if flag == 0 { None } else { Some(flag) }
563 } else {
564 eprintln!("Warning: Invalid disable auto-updater flag value, skipping");
565 None
566 }
567 } else {
568 params.disable_autoupdater
569 };
570
571 let is_anthropic_official = final_url.contains("api.anthropic.com");
573 if is_anthropic_official {
574 if !final_token.starts_with("sk-ant-api03-") {
575 eprintln!(
576 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
577 );
578 }
579 } else {
580 if final_token.starts_with("sk-ant-api03-") {
582 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
583 }
584 }
586
587 let config = Configuration {
589 alias_name: params.alias_name.clone(),
590 token: final_token,
591 url: final_url,
592 model: final_model,
593 small_fast_model: final_small_fast_model,
594 max_thinking_tokens: final_max_thinking_tokens,
595 api_timeout_ms: final_api_timeout_ms,
596 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
597 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
598 anthropic_default_opus_model: final_anthropic_default_opus_model,
599 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
600 claude_code_subagent_model: final_claude_code_subagent_model,
601 claude_code_disable_nonstreaming_fallback: final_claude_code_disable_nonstreaming_fallback,
602 claude_code_effort_level: final_claude_code_effort_level,
603 disable_prompt_caching: final_disable_prompt_caching,
604 claude_code_disable_experimental_betas: final_claude_code_disable_experimental_betas,
605 disable_autoupdater: final_disable_autoupdater,
606 claude_code_experimental_agent_teams: None,
607 claude_code_disable_1m_context: None,
608 };
609
610 storage.add_configuration(config);
611 storage.save()?;
612
613 println!("Configuration '{}' added successfully", params.alias_name);
614 if params.force {
615 println!("(Overwrote existing configuration)");
616 }
617
618 Ok(())
619}
620
621pub fn run() -> Result<()> {
631 let cli = Cli::parse();
632
633 if cli.migrate {
635 ConfigStorage::migrate_from_old_path()?;
636 return Ok(());
637 }
638
639 if cli.list_aliases {
641 list_aliases_for_completion()?;
642 return Ok(());
643 }
644
645 if cli.list_codex_aliases {
647 list_codex_aliases_for_completion()?;
648 return Ok(());
649 }
650
651 let _ = ClaudeSettings::cleanup_orphan_alias_files();
654
655 if let Some(ref store_str) = cli.store
657 && cli.command.is_none()
658 {
659 let mode = match parse_storage_mode(store_str) {
661 Ok(mode) => mode,
662 Err(e) => {
663 eprintln!("Error: {}", e);
664 std::process::exit(1);
665 }
666 };
667
668 let mut storage = ConfigStorage::load()?;
669 storage.default_storage_mode = Some(mode.clone());
670 storage.save()?;
671
672 let mode_str = match mode {
673 StorageMode::Env => "env",
674 StorageMode::Config => "config",
675 };
676
677 println!("Default storage mode set to: {}", mode_str);
678 return Ok(());
679 }
680
681 if let Some(command) = cli.command {
683 let mut storage = ConfigStorage::load()?;
684
685 match command {
686 Commands::Add {
687 alias_name,
688 token,
689 url,
690 model,
691 small_fast_model,
692 max_thinking_tokens,
693 api_timeout_ms,
694 claude_code_disable_nonessential_traffic,
695 anthropic_default_sonnet_model,
696 anthropic_default_opus_model,
697 anthropic_default_haiku_model,
698 claude_code_subagent_model,
699 claude_code_disable_nonstreaming_fallback,
700 claude_code_effort_level,
701 disable_prompt_caching,
702 claude_code_disable_experimental_betas,
703 disable_autoupdater,
704 force,
705 interactive,
706 token_arg,
707 url_arg,
708 from_file,
709 } => {
710 let resolved_from_file: Option<String> = match from_file {
711 Some(Some(path)) => {
712 if !std::path::Path::new(&path).exists() {
713 anyhow::bail!("Config file not found: {}", path);
714 }
715 Some(path)
716 }
717 Some(None) => {
718 let custom_dir = storage.get_claude_settings_dir().map(|s| s.as_str());
719 let default_path = crate::utils::get_claude_settings_path(custom_dir)
720 .map(|p| p.to_string_lossy().into_owned())
721 .map_err(|e| {
722 anyhow!("Failed to resolve default Claude settings path: {}", e)
723 })?;
724 if !std::path::Path::new(&default_path).exists() {
725 anyhow::bail!(
726 "Default Claude config not found at: {}\n\
727 Run `claude` once to create it, or pass an explicit path: --from-file <path>",
728 default_path
729 );
730 }
731 Some(default_path)
732 }
733 None => None,
734 };
735
736 let params = AddCommandParams {
737 alias_name,
738 token,
739 url,
740 model,
741 small_fast_model,
742 max_thinking_tokens,
743 api_timeout_ms,
744 claude_code_disable_nonessential_traffic,
745 anthropic_default_sonnet_model,
746 anthropic_default_opus_model,
747 anthropic_default_haiku_model,
748 claude_code_subagent_model,
749 claude_code_disable_nonstreaming_fallback,
750 claude_code_effort_level,
751 disable_prompt_caching,
752 claude_code_disable_experimental_betas,
753 disable_autoupdater,
754 force,
755 interactive,
756 token_arg,
757 url_arg,
758 from_file: resolved_from_file,
759 };
760 handle_add_command(params, &mut storage)?;
761 }
762 Commands::Remove { alias_names } => {
763 let mut removed_count = 0;
764 let mut not_found_aliases = Vec::new();
765
766 for alias_name in &alias_names {
767 if storage.remove_configuration(alias_name) {
768 removed_count += 1;
769 println!("Configuration '{alias_name}' removed successfully");
770 } else {
771 not_found_aliases.push(alias_name.clone());
772 println!("Configuration '{alias_name}' not found");
773 }
774 }
775
776 if removed_count > 0 {
777 storage.save()?;
778 }
779
780 if !not_found_aliases.is_empty() {
781 eprintln!(
782 "Warning: The following configurations were not found: {}",
783 not_found_aliases.join(", ")
784 );
785 }
786
787 if removed_count > 0 {
788 println!("Successfully removed {removed_count} configuration(s)");
789 }
790 }
791 Commands::List { plain, name } => {
792 if name {
793 if storage.configurations.is_empty() {
794 println!("No configurations stored");
795 } else {
796 for (alias_name, config) in &storage.configurations {
797 println!("{}: {}", alias_name, config.url);
798 }
799 }
800 } else if plain {
801 if storage.configurations.is_empty() {
803 println!("No configurations stored");
804 } else {
805 println!("Stored configurations:");
806 for (alias_name, config) in &storage.configurations {
807 let mut info = format!("token={}, url={}", config.token, config.url);
808 if let Some(model) = &config.model {
809 info.push_str(&format!(", model={model}"));
810 }
811 if let Some(small_fast_model) = &config.small_fast_model {
812 info.push_str(&format!(", small_fast_model={small_fast_model}"));
813 }
814 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
815 info.push_str(&format!(
816 ", max_thinking_tokens={max_thinking_tokens}"
817 ));
818 }
819 if let Some(subagent_model) = &config.claude_code_subagent_model {
820 info.push_str(&format!(", subagent_model={subagent_model}"));
821 }
822 if let Some(flag) = config.claude_code_disable_nonstreaming_fallback {
823 info.push_str(&format!(", disable_nonstreaming_fallback={flag}"));
824 }
825 if let Some(effort_level) = &config.claude_code_effort_level {
826 info.push_str(&format!(", effort_level={effort_level}"));
827 }
828 if let Some(flag) = config.disable_prompt_caching {
829 info.push_str(&format!(", disable_prompt_caching={flag}"));
830 }
831 if let Some(flag) = config.claude_code_disable_experimental_betas {
832 info.push_str(&format!(", disable_experimental_betas={flag}"));
833 }
834 if let Some(flag) = config.disable_autoupdater {
835 info.push_str(&format!(", disable_autoupdater={flag}"));
836 }
837 println!(" {alias_name}: {info}");
838 }
839 }
840 } else {
841 println!(
843 "{}",
844 serde_json::to_string_pretty(&storage.configurations)
845 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
846 );
847 }
848 }
849 Commands::Completion { shell } => {
850 generate_completion(&shell)?;
851 }
852 Commands::Use {
853 alias_name,
854 resume,
855 r#continue,
856 prompt,
857 } => {
858 if alias_name == "cc" || alias_name == "official" {
860 println!("Using official Claude configuration");
861
862 let mut settings = ClaudeSettings::load(
863 storage.get_claude_settings_dir().map(|s| s.as_str()),
864 )?;
865 settings.remove_anthropic_env();
866 settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
867
868 launch_claude_with_env(
869 EnvironmentConfig::empty().with_alias("official"),
870 None,
871 None,
872 r#continue,
873 )?;
874 return Ok(());
875 }
876
877 let config = storage
878 .configurations
879 .get(&alias_name)
880 .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
881 .clone();
882
883 let env_config = EnvironmentConfig::from_config(&config).with_alias(&alias_name);
884 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
885
886 let mut settings =
888 ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
889 settings.switch_to_config_with_mode(
890 &config,
891 storage_mode,
892 storage.get_claude_settings_dir().map(|s| s.as_str()),
893 )?;
894
895 println!("Switched to configuration '{}'", alias_name);
896 println!(" URL: {}", config.url);
897 println!(
898 " Token: {}",
899 crate::cli::display_utils::format_token_for_display(&config.token)
900 );
901
902 let prompt_str = if prompt.is_empty() {
903 None
904 } else {
905 Some(prompt.join(" "))
906 };
907
908 launch_claude_with_env(
909 env_config,
910 prompt_str.as_deref(),
911 resume.as_deref(),
912 r#continue,
913 )?;
914 }
915 Commands::Codex { command } => match command {
916 Some(crate::cli::CodexCommands::Add {
917 alias_name,
918 api_key,
919 force,
920 interactive,
921 from_file,
922 }) => {
923 let resolved_from_file: Option<String> = match from_file {
924 Some(Some(path)) => {
925 if !std::path::Path::new(&path).exists() {
926 anyhow::bail!("Codex auth file not found: {}", path);
927 }
928 Some(path)
929 }
930 Some(None) => {
931 let default_path = crate::codex::default_codex_auth_path()
932 .map(|p| p.to_string_lossy().into_owned())
933 .map_err(|e| {
934 anyhow!("Failed to resolve default Codex auth path: {}", e)
935 })?;
936 if !std::path::Path::new(&default_path).exists() {
937 anyhow::bail!(
938 "Default Codex auth file not found at: {}\n\
939 Run `codex login` first, or pass an explicit path: --from-file <path>",
940 default_path
941 );
942 }
943 Some(default_path)
944 }
945 None => None,
946 };
947
948 handle_codex_add(
949 alias_name,
950 api_key,
951 force,
952 interactive,
953 resolved_from_file,
954 &mut storage,
955 )?;
956 }
957 Some(crate::cli::CodexCommands::List { plain, name }) => {
958 handle_codex_list(plain, name, &storage)?;
959 }
960 Some(crate::cli::CodexCommands::Use {
961 alias_name,
962 r#continue,
963 resume,
964 prompt,
965 }) => {
966 handle_codex_use(alias_name, r#continue, resume, prompt, &mut storage)?;
967 }
968 Some(crate::cli::CodexCommands::Remove { alias_names }) => {
969 handle_codex_remove(alias_names, &mut storage)?;
970 }
971 None => {
972 handle_codex_interactive(&storage)?;
974 }
975 },
976 Commands::Statusline { action } => {
977 let custom_dir = storage.get_claude_settings_dir().map(|s| s.as_str());
978 match action {
979 crate::cli::StatuslineAction::Install => {
980 crate::statusline::install(custom_dir)?;
981 }
982 crate::cli::StatuslineAction::Uninstall => {
983 crate::statusline::uninstall(custom_dir)?;
984 }
985 }
986 }
987 }
988 } else {
989 let storage = ConfigStorage::load()?;
991 handle_interactive_selection(&storage)?;
992 }
993
994 Ok(())
995}