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)> {
57 let file_content = fs::read_to_string(file_path)
59 .map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
60
61 let json: serde_json::Value = serde_json::from_str(&file_content)
63 .map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
64
65 let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
67 anyhow!(
68 "File '{}' does not contain a valid 'env' section",
69 file_path
70 )
71 })?;
72
73 let path = Path::new(file_path);
75 let alias_name = path
76 .file_stem()
77 .and_then(|s| s.to_str())
78 .ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
79 .to_string();
80
81 let token = env
83 .get("ANTHROPIC_AUTH_TOKEN")
84 .and_then(|v| v.as_str())
85 .ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
86 .to_string();
87
88 let url = env
89 .get("ANTHROPIC_BASE_URL")
90 .and_then(|v| v.as_str())
91 .ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
92 .to_string();
93
94 let model = env
95 .get("ANTHROPIC_MODEL")
96 .and_then(|v| v.as_str())
97 .map(|s| s.to_string());
98
99 let small_fast_model = env
100 .get("ANTHROPIC_SMALL_FAST_MODEL")
101 .and_then(|v| v.as_str())
102 .map(|s| s.to_string());
103
104 let max_thinking_tokens = env
105 .get("ANTHROPIC_MAX_THINKING_TOKENS")
106 .and_then(|v| v.as_u64())
107 .map(|u| u as u32);
108
109 let api_timeout_ms = env
110 .get("API_TIMEOUT_MS")
111 .and_then(|v| v.as_u64())
112 .map(|u| u as u32);
113
114 let claude_code_disable_nonessential_traffic = env
115 .get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
116 .and_then(|v| v.as_u64())
117 .map(|u| u as u32);
118
119 let anthropic_default_sonnet_model = env
120 .get("ANTHROPIC_DEFAULT_SONNET_MODEL")
121 .and_then(|v| v.as_str())
122 .map(|s| s.to_string());
123
124 let anthropic_default_opus_model = env
125 .get("ANTHROPIC_DEFAULT_OPUS_MODEL")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string());
128
129 let anthropic_default_haiku_model = env
130 .get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
131 .and_then(|v| v.as_str())
132 .map(|s| s.to_string());
133
134 Ok((
135 alias_name,
136 token,
137 url,
138 model,
139 small_fast_model,
140 max_thinking_tokens,
141 api_timeout_ms,
142 claude_code_disable_nonessential_traffic,
143 anthropic_default_sonnet_model,
144 anthropic_default_opus_model,
145 anthropic_default_haiku_model,
146 ))
147}
148
149fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
158 if let Some(file_path) = ¶ms.from_file {
160 println!("Importing configuration from file: {}", file_path);
161
162 let (
163 file_alias_name,
164 file_token,
165 file_url,
166 file_model,
167 file_small_fast_model,
168 file_max_thinking_tokens,
169 file_api_timeout_ms,
170 file_claude_disable_nonessential_traffic,
171 file_sonnet_model,
172 file_opus_model,
173 file_haiku_model,
174 ) = parse_config_from_file(file_path)?;
175
176 params.alias_name = file_alias_name;
178
179 params.token = Some(file_token);
181 params.url = Some(file_url);
182 params.model = file_model;
183 params.small_fast_model = file_small_fast_model;
184 params.max_thinking_tokens = file_max_thinking_tokens;
185 params.api_timeout_ms = file_api_timeout_ms;
186 params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
187 params.anthropic_default_sonnet_model = file_sonnet_model;
188 params.anthropic_default_opus_model = file_opus_model;
189 params.anthropic_default_haiku_model = file_haiku_model;
190
191 println!(
192 "Configuration '{}' will be imported from file",
193 params.alias_name
194 );
195 }
196
197 validate_alias_name(¶ms.alias_name)?;
199
200 if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
202 eprintln!("Configuration '{}' already exists.", params.alias_name);
203 eprintln!("Use --force to overwrite or choose a different alias name.");
204 return Ok(());
205 }
206
207 if params.interactive && params.from_file.is_some() {
209 anyhow::bail!("Cannot use --interactive mode with --from-file");
210 }
211
212 let final_token = if params.interactive {
214 if params.token.is_some() || params.token_arg.is_some() {
215 eprintln!(
216 "Warning: Token provided via flags/arguments will be ignored in interactive mode"
217 );
218 }
219 read_sensitive_input("Enter API token (sk-ant-xxx): ")?
220 } else {
221 match (¶ms.token, ¶ms.token_arg) {
222 (Some(t), _) => t.clone(),
223 (None, Some(t)) => t.clone(),
224 (None, None) => {
225 anyhow::bail!(
226 "Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
227 );
228 }
229 }
230 };
231
232 let final_url = if params.interactive {
234 if params.url.is_some() || params.url_arg.is_some() {
235 eprintln!(
236 "Warning: URL provided via flags/arguments will be ignored in interactive mode"
237 );
238 }
239 read_input("Enter API URL (default: https://api.anthropic.com): ")?
240 } else {
241 match (¶ms.url, ¶ms.url_arg) {
242 (Some(u), _) => u.clone(),
243 (None, Some(u)) => u.clone(),
244 (None, None) => "https://api.anthropic.com".to_string(),
245 }
246 };
247
248 let final_url = if final_url.is_empty() {
250 "https://api.anthropic.com".to_string()
251 } else {
252 final_url
253 };
254
255 let final_model = if params.interactive {
257 if params.model.is_some() {
258 eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
259 }
260 let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
261 if model_input.is_empty() {
262 None
263 } else {
264 Some(model_input)
265 }
266 } else {
267 params.model
268 };
269
270 let final_small_fast_model = if params.interactive {
272 if params.small_fast_model.is_some() {
273 eprintln!(
274 "Warning: Small fast model provided via flags will be ignored in interactive mode"
275 );
276 }
277 let small_model_input =
278 read_input("Enter small fast model name (optional, press enter to skip): ")?;
279 if small_model_input.is_empty() {
280 None
281 } else {
282 Some(small_model_input)
283 }
284 } else {
285 params.small_fast_model
286 };
287
288 let final_max_thinking_tokens = if params.interactive {
290 if params.max_thinking_tokens.is_some() {
291 eprintln!(
292 "Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
293 );
294 }
295 let tokens_input = read_input(
296 "Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
297 )?;
298 if tokens_input.is_empty() {
299 None
300 } else if let Ok(tokens) = tokens_input.parse::<u32>() {
301 if tokens == 0 { None } else { Some(tokens) }
302 } else {
303 eprintln!("Warning: Invalid max thinking tokens value, skipping");
304 None
305 }
306 } else {
307 params.max_thinking_tokens
308 };
309
310 let final_api_timeout_ms = if params.interactive {
312 if params.api_timeout_ms.is_some() {
313 eprintln!(
314 "Warning: API timeout provided via flags will be ignored in interactive mode"
315 );
316 }
317 let timeout_input = read_input(
318 "Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
319 )?;
320 if timeout_input.is_empty() {
321 None
322 } else if let Ok(timeout) = timeout_input.parse::<u32>() {
323 if timeout == 0 { None } else { Some(timeout) }
324 } else {
325 eprintln!("Warning: Invalid API timeout value, skipping");
326 None
327 }
328 } else {
329 params.api_timeout_ms
330 };
331
332 let final_claude_code_disable_nonessential_traffic = if params.interactive {
334 if params.claude_code_disable_nonessential_traffic.is_some() {
335 eprintln!(
336 "Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
337 );
338 }
339 let flag_input = read_input(
340 "Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
341 )?;
342 if flag_input.is_empty() {
343 None
344 } else if let Ok(flag) = flag_input.parse::<u32>() {
345 if flag == 0 { None } else { Some(flag) }
346 } else {
347 eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
348 None
349 }
350 } else {
351 params.claude_code_disable_nonessential_traffic
352 };
353
354 let final_anthropic_default_sonnet_model = if params.interactive {
356 if params.anthropic_default_sonnet_model.is_some() {
357 eprintln!(
358 "Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
359 );
360 }
361 let model_input =
362 read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
363 if model_input.is_empty() {
364 None
365 } else {
366 Some(model_input)
367 }
368 } else {
369 params.anthropic_default_sonnet_model
370 };
371
372 let final_anthropic_default_opus_model = if params.interactive {
374 if params.anthropic_default_opus_model.is_some() {
375 eprintln!(
376 "Warning: Default Opus model provided via flags will be ignored in interactive mode"
377 );
378 }
379 let model_input =
380 read_input("Enter default Opus model name (optional, press enter to skip): ")?;
381 if model_input.is_empty() {
382 None
383 } else {
384 Some(model_input)
385 }
386 } else {
387 params.anthropic_default_opus_model
388 };
389
390 let final_anthropic_default_haiku_model = if params.interactive {
392 if params.anthropic_default_haiku_model.is_some() {
393 eprintln!(
394 "Warning: Default Haiku model provided via flags will be ignored in interactive mode"
395 );
396 }
397 let model_input =
398 read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
399 if model_input.is_empty() {
400 None
401 } else {
402 Some(model_input)
403 }
404 } else {
405 params.anthropic_default_haiku_model
406 };
407
408 let is_anthropic_official = final_url.contains("api.anthropic.com");
410 if is_anthropic_official {
411 if !final_token.starts_with("sk-ant-api03-") {
412 eprintln!(
413 "Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
414 );
415 }
416 } else {
417 if final_token.starts_with("sk-ant-api03-") {
419 eprintln!("Warning: Using official Claude token format with non-official API endpoint");
420 }
421 }
423
424 let config = Configuration {
426 alias_name: params.alias_name.clone(),
427 token: final_token,
428 url: final_url,
429 model: final_model,
430 small_fast_model: final_small_fast_model,
431 max_thinking_tokens: final_max_thinking_tokens,
432 api_timeout_ms: final_api_timeout_ms,
433 claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
434 anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
435 anthropic_default_opus_model: final_anthropic_default_opus_model,
436 anthropic_default_haiku_model: final_anthropic_default_haiku_model,
437 claude_code_experimental_agent_teams: None,
438 claude_code_disable_1m_context: None,
439 };
440
441 storage.add_configuration(config);
442 storage.save()?;
443
444 println!("Configuration '{}' added successfully", params.alias_name);
445 if params.force {
446 println!("(Overwrote existing configuration)");
447 }
448
449 Ok(())
450}
451
452pub fn run() -> Result<()> {
462 let cli = Cli::parse();
463
464 if cli.migrate {
466 ConfigStorage::migrate_from_old_path()?;
467 return Ok(());
468 }
469
470 if cli.list_aliases {
472 list_aliases_for_completion()?;
473 return Ok(());
474 }
475
476 if let Some(ref store_str) = cli.store
478 && cli.command.is_none()
479 {
480 let mode = match parse_storage_mode(store_str) {
482 Ok(mode) => mode,
483 Err(e) => {
484 eprintln!("Error: {}", e);
485 std::process::exit(1);
486 }
487 };
488
489 let mut storage = ConfigStorage::load()?;
490 storage.default_storage_mode = Some(mode.clone());
491 storage.save()?;
492
493 let mode_str = match mode {
494 StorageMode::Env => "env",
495 StorageMode::Config => "config",
496 };
497
498 println!("Default storage mode set to: {}", mode_str);
499 return Ok(());
500 }
501
502 if let Some(command) = cli.command {
504 let mut storage = ConfigStorage::load()?;
505
506 match command {
507 Commands::Add {
508 alias_name,
509 token,
510 url,
511 model,
512 small_fast_model,
513 max_thinking_tokens,
514 api_timeout_ms,
515 claude_code_disable_nonessential_traffic,
516 anthropic_default_sonnet_model,
517 anthropic_default_opus_model,
518 anthropic_default_haiku_model,
519 force,
520 interactive,
521 token_arg,
522 url_arg,
523 from_file,
524 } => {
525 let final_alias_name = if from_file.is_some() {
528 "placeholder".to_string()
530 } else {
531 alias_name.unwrap_or_else(|| {
532 eprintln!("Error: alias_name is required when not using --from-file");
533 std::process::exit(1);
534 })
535 };
536
537 let params = AddCommandParams {
538 alias_name: final_alias_name,
539 token,
540 url,
541 model,
542 small_fast_model,
543 max_thinking_tokens,
544 api_timeout_ms,
545 claude_code_disable_nonessential_traffic,
546 anthropic_default_sonnet_model,
547 anthropic_default_opus_model,
548 anthropic_default_haiku_model,
549 force,
550 interactive,
551 token_arg,
552 url_arg,
553 from_file,
554 };
555 handle_add_command(params, &mut storage)?;
556 }
557 Commands::Remove { alias_names } => {
558 let mut removed_count = 0;
559 let mut not_found_aliases = Vec::new();
560
561 for alias_name in &alias_names {
562 if storage.remove_configuration(alias_name) {
563 removed_count += 1;
564 println!("Configuration '{alias_name}' removed successfully");
565 } else {
566 not_found_aliases.push(alias_name.clone());
567 println!("Configuration '{alias_name}' not found");
568 }
569 }
570
571 if removed_count > 0 {
572 storage.save()?;
573 }
574
575 if !not_found_aliases.is_empty() {
576 eprintln!(
577 "Warning: The following configurations were not found: {}",
578 not_found_aliases.join(", ")
579 );
580 }
581
582 if removed_count > 0 {
583 println!("Successfully removed {removed_count} configuration(s)");
584 }
585 }
586 Commands::List { plain } => {
587 if plain {
588 if storage.configurations.is_empty() {
590 println!("No configurations stored");
591 } else {
592 println!("Stored configurations:");
593 for (alias_name, config) in &storage.configurations {
594 let mut info = format!("token={}, url={}", config.token, config.url);
595 if let Some(model) = &config.model {
596 info.push_str(&format!(", model={model}"));
597 }
598 if let Some(small_fast_model) = &config.small_fast_model {
599 info.push_str(&format!(", small_fast_model={small_fast_model}"));
600 }
601 if let Some(max_thinking_tokens) = config.max_thinking_tokens {
602 info.push_str(&format!(
603 ", max_thinking_tokens={max_thinking_tokens}"
604 ));
605 }
606 println!(" {alias_name}: {info}");
607 }
608 }
609 } else {
610 println!(
612 "{}",
613 serde_json::to_string_pretty(&storage.configurations)
614 .map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
615 );
616 }
617 }
618 Commands::Completion { shell } => {
619 generate_completion(&shell)?;
620 }
621 Commands::Use {
622 alias_name,
623 resume,
624 r#continue,
625 prompt,
626 } => {
627 let config = storage
628 .configurations
629 .get(&alias_name)
630 .ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
631 .clone();
632
633 let env_config = EnvironmentConfig::from_config(&config);
634 let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
635
636 let mut settings =
638 ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
639 settings.switch_to_config_with_mode(
640 &config,
641 storage_mode,
642 storage.get_claude_settings_dir().map(|s| s.as_str()),
643 )?;
644
645 println!("Switched to configuration '{}'", alias_name);
646 println!(" URL: {}", config.url);
647 println!(
648 " Token: {}",
649 crate::cli::display_utils::format_token_for_display(&config.token)
650 );
651
652 let prompt_str = if prompt.is_empty() {
653 None
654 } else {
655 Some(prompt.join(" "))
656 };
657
658 launch_claude_with_env(
659 env_config,
660 prompt_str.as_deref(),
661 resume.as_deref(),
662 r#continue,
663 )?;
664 }
665 }
666 } else {
667 let storage = ConfigStorage::load()?;
669 handle_interactive_selection(&storage)?;
670 }
671
672 Ok(())
673}