1use crate::recipes::github_recipe::ASTER_RECIPE_GITHUB_REPO_CONFIG_KEY;
2use aster::agents::extension::ToolInfo;
3use aster::agents::extension_manager::get_parameter_names;
4use aster::agents::Agent;
5use aster::agents::{extension::Envs, ExtensionConfig};
6use aster::config::declarative_providers::{create_custom_provider, remove_custom_provider};
7use aster::config::extensions::{
8 get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
9 name_to_key, remove_extension, set_extension, set_extension_enabled,
10};
11use aster::config::paths::Paths;
12use aster::config::permission::PermissionLevel;
13use aster::config::signup_tetrate::TetrateAuth;
14use aster::config::{
15 configure_tetrate, AsterMode, Config, ConfigError, ExperimentManager, ExtensionEntry,
16 PermissionManager,
17};
18use aster::conversation::message::Message;
19use aster::model::ModelConfig;
20use aster::providers::provider_test::test_provider_configuration;
21use aster::providers::{create, providers, retry_operation, RetryConfig};
22use aster::session::{SessionManager, SessionType};
23use cliclack::spinner;
24use console::style;
25use serde_json::Value;
26use std::collections::HashMap;
27
28const MULTISELECT_VISIBILITY_HINT: &str = "<";
31
32pub async fn handle_configure() -> anyhow::Result<()> {
33 let config = Config::global();
34
35 if !config.exists() {
36 handle_first_time_setup(config).await
37 } else {
38 handle_existing_config().await
39 }
40}
41
42async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> {
43 println!();
44 println!(
45 "{}",
46 style("Welcome to aster! Let's get you set up with a provider.").dim()
47 );
48 println!(
49 "{}",
50 style(" you can rerun this command later to update your configuration").dim()
51 );
52 println!();
53 cliclack::intro(style(" aster-configure ").on_cyan().black())?;
54
55 let setup_method = cliclack::select("How would you like to set up your provider?")
56 .item(
57 "openrouter",
58 "OpenRouter Login (Recommended)",
59 "Sign in with OpenRouter to automatically configure models",
60 )
61 .item(
62 "tetrate",
63 "Tetrate Agent Router Service Login",
64 "Sign in with Tetrate Agent Router Service to automatically configure models",
65 )
66 .item(
67 "manual",
68 "Manual Configuration",
69 "Choose a provider and enter credentials manually",
70 )
71 .interact()?;
72
73 match setup_method {
74 "openrouter" => {
75 if let Err(e) = handle_openrouter_auth().await {
76 let _ = config.clear();
77 println!(
78 "\n {} OpenRouter authentication failed: {} \n Please try again or use manual configuration",
79 style("Error").red().italic(),
80 e,
81 );
82 }
83 }
84 "tetrate" => {
85 if let Err(e) = handle_tetrate_auth().await {
86 let _ = config.clear();
87 println!(
88 "\n {} Tetrate Agent Router Service authentication failed: {} \n Please try again or use manual configuration",
89 style("Error").red().italic(),
90 e,
91 );
92 }
93 }
94 "manual" => handle_manual_provider_setup(config).await,
95 _ => unreachable!(),
96 }
97 Ok(())
98}
99
100async fn handle_manual_provider_setup(config: &Config) {
101 match configure_provider_dialog().await {
102 Ok(true) => {
103 println!(
104 "\n {}: Run '{}' again to adjust your config or add extensions",
105 style("Tip").green().italic(),
106 style("aster configure").cyan()
107 );
108 set_extension(ExtensionEntry {
109 enabled: true,
110 config: ExtensionConfig::default(),
111 });
112 }
113 Ok(false) => {
114 let _ = config.clear();
115 println!(
116 "\n {}: We did not save your config, inspect your credentials\n and run '{}' again to ensure aster can connect",
117 style("Warning").yellow().italic(),
118 style("aster configure").cyan()
119 );
120 }
121 Err(e) => {
122 let _ = config.clear();
123 print_manual_config_error(&e);
124 }
125 }
126}
127
128fn print_manual_config_error(e: &anyhow::Error) {
129 match e.downcast_ref::<ConfigError>() {
130 Some(ConfigError::NotFound(key)) => {
131 println!(
132 "\n {} Required configuration key '{}' not found \n Please provide this value and run '{}' again",
133 style("Error").red().italic(),
134 key,
135 style("aster configure").cyan()
136 );
137 }
138 Some(ConfigError::KeyringError(msg)) => {
139 print_keyring_error(msg);
140 }
141 Some(ConfigError::DeserializeError(msg)) => {
142 println!(
143 "\n {} Invalid configuration value: {} \n Please check your input and run '{}' again",
144 style("Error").red().italic(),
145 msg,
146 style("aster configure").cyan()
147 );
148 }
149 Some(ConfigError::FileError(err)) => {
150 println!(
151 "\n {} Failed to access config file: {} \n Please check file permissions and run '{}' again",
152 style("Error").red().italic(),
153 err,
154 style("aster configure").cyan()
155 );
156 }
157 Some(ConfigError::DirectoryError(msg)) => {
158 println!(
159 "\n {} Failed to access config directory: {} \n Please check directory permissions and run '{}' again",
160 style("Error").red().italic(),
161 msg,
162 style("aster configure").cyan()
163 );
164 }
165 _ => {
166 println!(
167 "\n {} {} \n We did not save your config, inspect your credentials\n and run '{}' again to ensure aster can connect",
168 style("Error").red().italic(),
169 e,
170 style("aster configure").cyan()
171 );
172 }
173 }
174}
175
176#[cfg(target_os = "macos")]
177fn print_keyring_error(msg: &str) {
178 println!(
179 "\n {} Failed to access secure storage (keyring): {} \n Please check your system keychain and run '{}' again. \n If your system is unable to use the keyring, please try setting secret key(s) via environment variables.",
180 style("Error").red().italic(),
181 msg,
182 style("aster configure").cyan()
183 );
184}
185
186#[cfg(target_os = "windows")]
187fn print_keyring_error(msg: &str) {
188 println!(
189 "\n {} Failed to access Windows Credential Manager: {} \n Please check Windows Credential Manager and run '{}' again. \n If your system is unable to use the Credential Manager, please try setting secret key(s) via environment variables.",
190 style("Error").red().italic(),
191 msg,
192 style("aster configure").cyan()
193 );
194}
195
196#[cfg(not(any(target_os = "macos", target_os = "windows")))]
197fn print_keyring_error(msg: &str) {
198 println!(
199 "\n {} Failed to access secure storage: {} \n Please check your system's secure storage and run '{}' again. \n If your system is unable to use secure storage, please try setting secret key(s) via environment variables.",
200 style("Error").red().italic(),
201 msg,
202 style("aster configure").cyan()
203 );
204}
205
206async fn handle_existing_config() -> anyhow::Result<()> {
207 let config_dir = Paths::config_dir().display().to_string();
208
209 println!();
210 println!(
211 "{}",
212 style("This will update your existing config files").dim()
213 );
214 println!(
215 "{} {}",
216 style(" if you prefer, you can edit them directly at").dim(),
217 config_dir
218 );
219 println!();
220
221 cliclack::intro(style(" aster-configure ").on_cyan().black())?;
222 let action = cliclack::select("What would you like to configure?")
223 .item(
224 "providers",
225 "Configure Providers",
226 "Change provider or update credentials",
227 )
228 .item(
229 "custom_providers",
230 "Custom Providers",
231 "Add custom provider with compatible API",
232 )
233 .item("add", "Add Extension", "Connect to a new extension")
234 .item(
235 "toggle",
236 "Toggle Extensions",
237 "Enable or disable connected extensions",
238 )
239 .item("remove", "Remove Extension", "Remove an extension")
240 .item(
241 "settings",
242 "aster settings",
243 "Set the aster mode, Tool Output, Tool Permissions, Experiment, aster recipe github repo and more",
244 )
245 .interact()?;
246
247 match action {
248 "toggle" => toggle_extensions_dialog(),
249 "add" => configure_extensions_dialog(),
250 "remove" => remove_extension_dialog(),
251 "settings" => configure_settings_dialog().await,
252 "providers" => configure_provider_dialog().await.map(|_| ()),
253 "custom_providers" => configure_custom_provider_dialog(),
254 _ => unreachable!(),
255 }
256}
257
258async fn handle_oauth_configuration(provider_name: &str, key_name: &str) -> anyhow::Result<()> {
260 let _ = cliclack::log::info(format!(
261 "Configuring {} using OAuth device code flow...",
262 key_name
263 ));
264
265 let temp_model = ModelConfig::new("temp")?;
267 match create(provider_name, temp_model).await {
268 Ok(provider) => match provider.configure_oauth().await {
269 Ok(_) => {
270 let _ = cliclack::log::success("OAuth authentication completed successfully!");
271 Ok(())
272 }
273 Err(e) => {
274 let _ = cliclack::log::error(format!("Failed to authenticate: {}", e));
275 Err(anyhow::anyhow!(
276 "OAuth authentication failed for {}: {}",
277 key_name,
278 e
279 ))
280 }
281 },
282 Err(e) => {
283 let _ = cliclack::log::error(format!("Failed to create provider for OAuth: {}", e));
284 Err(anyhow::anyhow!(
285 "Failed to create provider for OAuth: {}",
286 e
287 ))
288 }
289 }
290}
291
292fn interactive_model_search(models: &[String]) -> anyhow::Result<String> {
293 const MAX_VISIBLE: usize = 30;
294 let mut query = String::new();
295
296 loop {
297 let _ = cliclack::clear_screen();
298
299 let _ = cliclack::log::info(format!(
300 "🔍 {} models available. Type to filter.",
301 models.len()
302 ));
303
304 let input: String = cliclack::input("Filtering models, press Enter to search")
305 .placeholder("e.g., gpt, sonnet, llama, qwen")
306 .default_input(&query)
307 .interact::<String>()?;
308 query = input.trim().to_string();
309
310 let filtered: Vec<String> = if query.is_empty() {
311 models.to_vec()
312 } else {
313 let q = query.to_lowercase();
314 models
315 .iter()
316 .filter(|m| m.to_lowercase().contains(&q))
317 .cloned()
318 .collect()
319 };
320
321 if filtered.is_empty() {
322 let _ = cliclack::log::warning("No matching models. Try a different search.");
323 continue;
324 }
325
326 let mut items: Vec<(String, String, &str)> = filtered
327 .iter()
328 .take(MAX_VISIBLE)
329 .map(|m| (m.clone(), m.clone(), ""))
330 .collect();
331
332 if filtered.len() > MAX_VISIBLE {
333 items.insert(
334 0,
335 (
336 "__refine__".to_string(),
337 format!(
338 "Refine search to see more (showing {} of {} results)",
339 MAX_VISIBLE,
340 filtered.len()
341 ),
342 "Too many matches",
343 ),
344 );
345 } else {
346 items.insert(
347 0,
348 (
349 "__new_search__".to_string(),
350 "Start a new search...".to_string(),
351 "Enter a different search term",
352 ),
353 );
354 }
355
356 let selection = cliclack::select("Select a model:")
357 .items(&items)
358 .interact()?;
359
360 if selection == "__refine__" {
361 continue;
362 } else if selection == "__new_search__" {
363 query.clear();
364 continue;
365 } else {
366 return Ok(selection);
367 }
368 }
369}
370
371fn select_model_from_list(
372 models: &[String],
373 provider_meta: &aster::providers::base::ProviderMetadata,
374) -> anyhow::Result<String> {
375 const MAX_MODELS: usize = 10;
376 if models.len() > MAX_MODELS {
381 let recommended_models: Vec<String> = provider_meta
383 .known_models
384 .iter()
385 .map(|m| m.name.clone())
386 .filter(|name| models.contains(name))
387 .collect();
388
389 if !recommended_models.is_empty() {
390 let mut model_items: Vec<(String, String, &str)> = recommended_models
391 .iter()
392 .map(|m| (m.clone(), m.clone(), "Recommended"))
393 .collect();
394
395 model_items.insert(
396 0,
397 (
398 "search_all".to_string(),
399 "Search all models...".to_string(),
400 "Search complete model list",
401 ),
402 );
403
404 let selection = cliclack::select("Select a model:")
405 .items(&model_items)
406 .interact()?;
407
408 if selection == "search_all" {
409 Ok(interactive_model_search(models)?)
410 } else {
411 Ok(selection)
412 }
413 } else {
414 Ok(interactive_model_search(models)?)
415 }
416 } else {
417 Ok(cliclack::select("Select a model:")
419 .items(
420 &models
421 .iter()
422 .map(|m| (m, m.as_str(), ""))
423 .collect::<Vec<_>>(),
424 )
425 .interact()?
426 .to_string())
427 }
428}
429
430fn try_store_secret(config: &Config, key_name: &str, value: String) -> anyhow::Result<bool> {
431 match config.set_secret(key_name, &value) {
432 Ok(_) => Ok(true),
433 Err(e) => {
434 cliclack::outro(style(format!(
435 "Failed to store {} securely: {}. Please ensure your system's secure storage is accessible. Alternatively you can run with ASTER_DISABLE_KEYRING=true or set the key in your environment variables",
436 key_name, e
437 )).on_red().white())?;
438 Ok(false)
439 }
440 }
441}
442
443pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
444 let config = Config::global();
446
447 let mut available_providers = providers().await;
449
450 available_providers.sort_by(|a, b| a.0.display_name.cmp(&b.0.display_name));
452
453 let provider_items: Vec<(&String, &str, &str)> = available_providers
455 .iter()
456 .map(|(p, _)| (&p.name, p.display_name.as_str(), p.description.as_str()))
457 .collect();
458
459 let current_provider: Option<String> = config.get_aster_provider().ok();
461 let default_provider = current_provider.unwrap_or_default();
462
463 let provider_name = cliclack::select("Which model provider should we use?")
465 .initial_value(&default_provider)
466 .items(&provider_items)
467 .interact()?;
468
469 let (provider_meta, _) = available_providers
471 .iter()
472 .find(|(p, _)| &p.name == provider_name)
473 .expect("Selected provider must exist in metadata");
474
475 for key in &provider_meta.config_keys {
477 if !key.required {
478 continue;
479 }
480
481 let from_env = std::env::var(&key.name).ok();
483
484 match from_env {
485 Some(env_value) => {
486 let _ =
487 cliclack::log::info(format!("{} is set via environment variable", key.name));
488 if cliclack::confirm("Would you like to save this value to your keyring?")
489 .initial_value(true)
490 .interact()?
491 {
492 if key.secret {
493 if !try_store_secret(config, &key.name, env_value)? {
494 return Ok(false);
495 }
496 } else {
497 config.set_param(&key.name, &env_value)?;
498 }
499 let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path()));
500 }
501 }
502 None => {
503 let existing: Result<String, _> = if key.secret {
505 config.get_secret(&key.name)
506 } else {
507 config.get_param(&key.name)
508 };
509
510 match existing {
511 Ok(_) => {
512 let _ = cliclack::log::info(format!("{} is already configured", key.name));
513 if cliclack::confirm("Would you like to update this value?").interact()? {
514 if key.oauth_flow {
516 handle_oauth_configuration(provider_name, &key.name).await?;
517 } else {
518 let value: String = if key.secret {
520 cliclack::password(format!("Enter new value for {}", key.name))
521 .mask('▪')
522 .interact()?
523 } else {
524 let mut input = cliclack::input(format!(
525 "Enter new value for {}",
526 key.name
527 ));
528 if key.default.is_some() {
529 input = input.default_input(&key.default.clone().unwrap());
530 }
531 input.interact()?
532 };
533
534 if key.secret {
535 if !try_store_secret(config, &key.name, value)? {
536 return Ok(false);
537 }
538 } else {
539 config.set_param(&key.name, &value)?;
540 }
541 }
542 }
543 }
544 Err(_) => {
545 if key.oauth_flow {
546 handle_oauth_configuration(provider_name, &key.name).await?;
547 } else {
548 let value: String = if key.secret {
550 cliclack::password(format!(
551 "Provider {} requires {}, please enter a value",
552 provider_meta.display_name, key.name
553 ))
554 .mask('▪')
555 .interact()?
556 } else {
557 let mut input = cliclack::input(format!(
558 "Provider {} requires {}, please enter a value",
559 provider_meta.display_name, key.name
560 ));
561 if key.default.is_some() {
562 input = input.default_input(&key.default.clone().unwrap());
563 }
564 input.interact()?
565 };
566
567 if key.secret {
568 config.set_secret(&key.name, &value)?;
569 } else {
570 config.set_param(&key.name, &value)?;
571 }
572 }
573 }
574 }
575 }
576 }
577 }
578
579 let spin = spinner();
580 spin.start("Attempting to fetch supported models...");
581 let models_res = {
582 let temp_model_config = ModelConfig::new(&provider_meta.default_model)?;
583 let temp_provider = create(provider_name, temp_model_config).await?;
584 retry_operation(&RetryConfig::default(), || async {
585 temp_provider.fetch_recommended_models().await
586 })
587 .await
588 };
589 spin.stop(style("Model fetch complete").green());
590
591 let model: String = match models_res {
593 Err(e) => {
594 cliclack::outro(style(e.to_string()).on_red().white())?;
596 return Ok(false);
597 }
598 Ok(Some(models)) => select_model_from_list(&models, provider_meta)?,
599 Ok(None) => {
600 let default_model =
601 std::env::var("ASTER_MODEL").unwrap_or(provider_meta.default_model.clone());
602 cliclack::input("Enter a model from that provider:")
603 .default_input(&default_model)
604 .interact()?
605 }
606 };
607
608 let spin = spinner();
610 spin.start("Checking your configuration...");
611
612 let toolshim_enabled = std::env::var("ASTER_TOOLSHIM")
613 .map(|val| val == "1" || val.to_lowercase() == "true")
614 .unwrap_or(false);
615 let toolshim_model = std::env::var("ASTER_TOOLSHIM_OLLAMA_MODEL").ok();
616
617 match test_provider_configuration(provider_name, &model, toolshim_enabled, toolshim_model).await
618 {
619 Ok(()) => {
620 config.set_aster_provider(provider_name)?;
621 config.set_aster_model(&model)?;
622 print_config_file_saved()?;
623 Ok(true)
624 }
625 Err(e) => {
626 spin.stop(style(e.to_string()).red());
627 cliclack::outro(style("Failed to configure provider: init chat completion request with tool did not succeed.").on_red().white())?;
628 Ok(false)
629 }
630 }
631}
632
633pub fn toggle_extensions_dialog() -> anyhow::Result<()> {
636 for warning in aster::config::get_warnings() {
637 eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
638 }
639
640 let extensions = get_all_extensions();
641
642 if extensions.is_empty() {
643 cliclack::outro(
644 "No extensions configured yet. Run configure and add some extensions first.",
645 )?;
646 return Ok(());
647 }
648
649 let mut extension_status: Vec<(String, bool)> = extensions
651 .iter()
652 .map(|entry| (entry.config.name().to_string(), entry.enabled))
653 .collect();
654
655 extension_status.sort_by(|a, b| a.0.cmp(&b.0));
657
658 let enabled_extensions: Vec<&String> = extension_status
660 .iter()
661 .filter(|(_, enabled)| *enabled)
662 .map(|(name, _)| name)
663 .collect();
664
665 let selected = cliclack::multiselect(
667 "enable extensions: (use \"space\" to toggle and \"enter\" to submit)",
668 )
669 .required(false)
670 .items(
671 &extension_status
672 .iter()
673 .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT))
674 .collect::<Vec<_>>(),
675 )
676 .initial_values(enabled_extensions)
677 .interact()?;
678
679 for name in extension_status.iter().map(|(name, _)| name) {
681 set_extension_enabled(
682 &name_to_key(name),
683 selected.iter().any(|s| s.as_str() == name),
684 );
685 }
686
687 let config = Config::global();
688 cliclack::outro(format!(
689 "Extension settings saved successfully to {}",
690 config.path()
691 ))?;
692 Ok(())
693}
694
695fn prompt_extension_timeout() -> anyhow::Result<u64> {
696 Ok(
697 cliclack::input("Please set the timeout for this tool (in secs):")
698 .placeholder(&aster::config::DEFAULT_EXTENSION_TIMEOUT.to_string())
699 .validate(|input: &String| match input.parse::<u64>() {
700 Ok(_) => Ok(()),
701 Err(_) => Err("Please enter a valid timeout"),
702 })
703 .interact()?,
704 )
705}
706
707fn prompt_extension_description() -> anyhow::Result<String> {
708 Ok(cliclack::input("Enter a description for this extension:")
709 .placeholder("Description")
710 .validate(|input: &String| {
711 if input.trim().is_empty() {
712 Err("Please enter a valid description")
713 } else {
714 Ok(())
715 }
716 })
717 .interact()?)
718}
719
720fn prompt_extension_name(placeholder: &str) -> anyhow::Result<String> {
721 let extensions = get_all_extension_names();
722 Ok(
723 cliclack::input("What would you like to call this extension?")
724 .placeholder(placeholder)
725 .validate(move |input: &String| {
726 if input.is_empty() {
727 Err("Please enter a name")
728 } else if extensions.contains(input) {
729 Err("An extension with this name already exists")
730 } else {
731 Ok(())
732 }
733 })
734 .interact()?,
735 )
736}
737
738fn collect_env_vars() -> anyhow::Result<(HashMap<String, String>, Vec<String>)> {
739 let mut envs = HashMap::new();
740 let mut env_keys = Vec::new();
741 let config = Config::global();
742
743 if !cliclack::confirm("Would you like to add environment variables?").interact()? {
744 return Ok((envs, env_keys));
745 }
746
747 loop {
748 let key: String = cliclack::input("Environment variable name:")
749 .placeholder("API_KEY")
750 .interact()?;
751
752 let value: String = cliclack::password("Environment variable value:")
753 .mask('▪')
754 .interact()?;
755
756 match config.set_secret(&key, &value) {
757 Ok(_) => env_keys.push(key),
758 Err(_) => {
759 envs.insert(key, value);
760 }
761 }
762
763 if !cliclack::confirm("Add another environment variable?").interact()? {
764 break;
765 }
766 }
767
768 Ok((envs, env_keys))
769}
770
771fn collect_headers() -> anyhow::Result<HashMap<String, String>> {
772 let mut headers = HashMap::new();
773
774 if !cliclack::confirm("Would you like to add custom headers?").interact()? {
775 return Ok(headers);
776 }
777
778 loop {
779 let key: String = cliclack::input("Header name:")
780 .placeholder("Authorization")
781 .interact()?;
782
783 let value: String = cliclack::input("Header value:")
784 .placeholder("Bearer token123")
785 .interact()?;
786
787 headers.insert(key, value);
788
789 if !cliclack::confirm("Add another header?").interact()? {
790 break;
791 }
792 }
793
794 Ok(headers)
795}
796
797fn configure_builtin_extension() -> anyhow::Result<()> {
798 let extensions = vec![
799 (
800 "autovisualiser",
801 "Auto Visualiser",
802 "Data visualisation and UI generation tools",
803 ),
804 (
805 "computercontroller",
806 "Computer Controller",
807 "controls for webscraping, file caching, and automations",
808 ),
809 (
810 "developer",
811 "Developer Tools",
812 "Code editing and shell access",
813 ),
814 (
815 "memory",
816 "Memory",
817 "Tools to save and retrieve durable memories",
818 ),
819 (
820 "tutorial",
821 "Tutorial",
822 "Access interactive tutorials and guides",
823 ),
824 ];
825
826 let mut select = cliclack::select("Which built-in extension would you like to enable?");
827 for (id, name, desc) in &extensions {
828 select = select.item(id, name, desc);
829 }
830 let extension = select.interact()?.to_string();
831 let timeout = prompt_extension_timeout()?;
832
833 let (display_name, description) = extensions
834 .iter()
835 .find(|(id, _, _)| id == &extension)
836 .map(|(_, name, desc)| (name.to_string(), desc.to_string()))
837 .unwrap_or_else(|| (extension.clone(), extension.clone()));
838
839 set_extension(ExtensionEntry {
840 enabled: true,
841 config: ExtensionConfig::Builtin {
842 name: extension.clone(),
843 display_name: Some(display_name),
844 timeout: Some(timeout),
845 bundled: Some(true),
846 description,
847 available_tools: Vec::new(),
848 },
849 });
850
851 cliclack::outro(format!("Enabled {} extension", style(extension).green()))?;
852 Ok(())
853}
854
855fn configure_stdio_extension() -> anyhow::Result<()> {
856 let name = prompt_extension_name("my-extension")?;
857
858 let command_str: String = cliclack::input("What command should be run?")
859 .placeholder("npx -y @block/gdrive")
860 .validate(|input: &String| {
861 if input.is_empty() {
862 Err("Please enter a command")
863 } else {
864 Ok(())
865 }
866 })
867 .interact()?;
868
869 let timeout = prompt_extension_timeout()?;
870
871 let mut parts = command_str.split_whitespace();
872 let cmd = parts.next().unwrap_or("").to_string();
873 let args: Vec<String> = parts.map(String::from).collect();
874
875 let description = prompt_extension_description()?;
876 let (envs, env_keys) = collect_env_vars()?;
877
878 set_extension(ExtensionEntry {
879 enabled: true,
880 config: ExtensionConfig::Stdio {
881 name: name.clone(),
882 cmd,
883 args,
884 envs: Envs::new(envs),
885 env_keys,
886 description,
887 timeout: Some(timeout),
888 bundled: None,
889 available_tools: Vec::new(),
890 },
891 });
892
893 cliclack::outro(format!("Added {} extension", style(name).green()))?;
894 Ok(())
895}
896
897fn configure_streamable_http_extension() -> anyhow::Result<()> {
898 let name = prompt_extension_name("my-remote-extension")?;
899
900 let uri: String = cliclack::input("What is the Streaming HTTP endpoint URI?")
901 .placeholder("http://localhost:8000/messages")
902 .validate(|input: &String| {
903 if input.is_empty() {
904 Err("Please enter a URI")
905 } else if !(input.starts_with("http://") || input.starts_with("https://")) {
906 Err("URI should start with http:// or https://")
907 } else {
908 Ok(())
909 }
910 })
911 .interact()?;
912
913 let timeout = prompt_extension_timeout()?;
914 let description = prompt_extension_description()?;
915 let headers = collect_headers()?;
916
917 let envs = HashMap::new();
919 let env_keys = Vec::new();
920
921 set_extension(ExtensionEntry {
922 enabled: true,
923 config: ExtensionConfig::StreamableHttp {
924 name: name.clone(),
925 uri,
926 envs: Envs::new(envs),
927 env_keys,
928 headers,
929 description,
930 timeout: Some(timeout),
931 bundled: None,
932 available_tools: Vec::new(),
933 },
934 });
935
936 cliclack::outro(format!("Added {} extension", style(name).green()))?;
937 Ok(())
938}
939
940pub fn configure_extensions_dialog() -> anyhow::Result<()> {
941 let extension_type = cliclack::select("What type of extension would you like to add?")
942 .item(
943 "built-in",
944 "Built-in Extension",
945 "Use an extension that comes with aster",
946 )
947 .item(
948 "stdio",
949 "Command-line Extension",
950 "Run a local command or script",
951 )
952 .item(
953 "streamable_http",
954 "Remote Extension (Streamable HTTP)",
955 "Connect to a remote extension via MCP Streamable HTTP",
956 )
957 .interact()?;
958
959 match extension_type {
960 "built-in" => configure_builtin_extension()?,
961 "stdio" => configure_stdio_extension()?,
962 "streamable_http" => configure_streamable_http_extension()?,
963 _ => unreachable!(),
964 };
965
966 print_config_file_saved()?;
967 Ok(())
968}
969
970pub fn remove_extension_dialog() -> anyhow::Result<()> {
971 for warning in aster::config::get_warnings() {
972 eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
973 }
974
975 let extensions = get_all_extensions();
976
977 let mut extension_status: Vec<(String, bool)> = extensions
979 .iter()
980 .map(|entry| (entry.config.name().to_string(), entry.enabled))
981 .collect();
982
983 extension_status.sort_by(|a, b| a.0.cmp(&b.0));
985
986 if extensions.is_empty() {
987 cliclack::outro(
988 "No extensions configured yet. Run configure and add some extensions first.",
989 )?;
990 return Ok(());
991 }
992
993 if extension_status.iter().all(|(_, enabled)| *enabled) {
995 cliclack::outro(
996 "All extensions are currently enabled. You must first disable extensions before removing them.",
997 )?;
998 return Ok(());
999 }
1000
1001 let disabled_extensions: Vec<_> = extensions
1003 .iter()
1004 .filter(|entry| !entry.enabled)
1005 .map(|entry| (entry.config.name().to_string(), entry.enabled))
1006 .collect();
1007
1008 let selected = cliclack::multiselect("Select extensions to remove (note: you can only remove disabled extensions - use \"space\" to toggle and \"enter\" to submit)")
1009 .required(false)
1010 .items(
1011 &disabled_extensions
1012 .iter()
1013 .filter(|(_, enabled)| !enabled)
1014 .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT))
1015 .collect::<Vec<_>>(),
1016 )
1017 .interact()?;
1018
1019 for name in selected {
1020 remove_extension(&name_to_key(name));
1021 let mut permission_manager = PermissionManager::default();
1022 permission_manager.remove_extension(&name_to_key(name));
1023 cliclack::outro(format!("Removed {} extension", style(name).green()))?;
1024 }
1025
1026 print_config_file_saved()?;
1027
1028 Ok(())
1029}
1030
1031pub async fn configure_settings_dialog() -> anyhow::Result<()> {
1032 let setting_type = cliclack::select("What setting would you like to configure?")
1033 .item("aster_mode", "aster mode", "Configure aster mode")
1034 .item(
1035 "tool_permission",
1036 "Tool Permission",
1037 "Set permission for individual tool of enabled extensions",
1038 )
1039 .item(
1040 "tool_output",
1041 "Tool Output",
1042 "Show more or less tool output",
1043 )
1044 .item(
1045 "max_turns",
1046 "Max Turns",
1047 "Set maximum number of turns without user input",
1048 )
1049 .item(
1050 "keyring",
1051 "Secret Storage",
1052 "Configure how secrets are stored (keyring vs file)",
1053 )
1054 .item(
1055 "experiment",
1056 "Toggle Experiment",
1057 "Enable or disable an experiment feature",
1058 )
1059 .item(
1060 "recipe",
1061 "aster recipe github repo",
1062 "aster will pull recipes from this repo if not found locally.",
1063 )
1064 .interact()?;
1065
1066 let mut should_print_config_path = true;
1067
1068 match setting_type {
1069 "aster_mode" => {
1070 configure_aster_mode_dialog()?;
1071 }
1072 "tool_permission" => {
1073 configure_tool_permissions_dialog().await.and(Ok(()))?;
1074 should_print_config_path = false;
1076 }
1077 "tool_output" => {
1078 configure_tool_output_dialog()?;
1079 }
1080 "max_turns" => {
1081 configure_max_turns_dialog()?;
1082 }
1083 "keyring" => {
1084 configure_keyring_dialog()?;
1085 }
1086 "experiment" => {
1087 toggle_experiments_dialog()?;
1088 }
1089 "recipe" => {
1090 configure_recipe_dialog()?;
1091 }
1092 _ => unreachable!(),
1093 };
1094
1095 if should_print_config_path {
1096 print_config_file_saved()?;
1097 }
1098
1099 Ok(())
1100}
1101
1102pub fn configure_aster_mode_dialog() -> anyhow::Result<()> {
1103 let config = Config::global();
1104
1105 if std::env::var("ASTER_MODE").is_ok() {
1106 let _ = cliclack::log::info("Notice: ASTER_MODE environment variable is set and will override the configuration here.");
1107 }
1108
1109 let mode = cliclack::select("Which aster mode would you like to configure?")
1110 .item(
1111 AsterMode::Auto,
1112 "Auto Mode",
1113 "Full file modification, extension usage, edit, create and delete files freely"
1114 )
1115 .item(
1116 AsterMode::Approve,
1117 "Approve Mode",
1118 "All tools, extensions and file modifications will require human approval"
1119 )
1120 .item(
1121 AsterMode::SmartApprove,
1122 "Smart Approve Mode",
1123 "Editing, creating, deleting files and using extensions will require human approval"
1124 )
1125 .item(
1126 AsterMode::Chat,
1127 "Chat Mode",
1128 "Engage with the selected provider without using tools, extensions, or file modification"
1129 )
1130 .interact()?;
1131
1132 config.set_aster_mode(mode)?;
1133 let msg = match mode {
1134 AsterMode::Auto => "Set to Auto Mode - full file modification enabled",
1135 AsterMode::Approve => "Set to Approve Mode - all tools and modifications require approval",
1136 AsterMode::SmartApprove => "Set to Smart Approve Mode - modifications require approval",
1137 AsterMode::Chat => "Set to Chat Mode - no tools or modifications enabled",
1138 };
1139 cliclack::outro(msg)?;
1140 Ok(())
1141}
1142
1143pub fn configure_tool_output_dialog() -> anyhow::Result<()> {
1144 let config = Config::global();
1145
1146 if std::env::var("ASTER_CLI_MIN_PRIORITY").is_ok() {
1147 let _ = cliclack::log::info("Notice: ASTER_CLI_MIN_PRIORITY environment variable is set and will override the configuration here.");
1148 }
1149 let tool_log_level = cliclack::select("Which tool output would you like to show?")
1150 .item("high", "High Importance", "")
1151 .item("medium", "Medium Importance", "Ex. results of file-writes")
1152 .item("all", "All (default)", "Ex. shell command output")
1153 .interact()?;
1154
1155 match tool_log_level {
1156 "high" => {
1157 config.set_param("ASTER_CLI_MIN_PRIORITY", 0.8)?;
1158 cliclack::outro("Showing tool output of high importance only.")?;
1159 }
1160 "medium" => {
1161 config.set_param("ASTER_CLI_MIN_PRIORITY", 0.2)?;
1162 cliclack::outro("Showing tool output of medium importance.")?;
1163 }
1164 "all" => {
1165 config.set_param("ASTER_CLI_MIN_PRIORITY", 0.0)?;
1166 cliclack::outro("Showing all tool output.")?;
1167 }
1168 _ => unreachable!(),
1169 };
1170
1171 Ok(())
1172}
1173
1174pub fn configure_keyring_dialog() -> anyhow::Result<()> {
1175 let config = Config::global();
1176
1177 if std::env::var("ASTER_DISABLE_KEYRING").is_ok() {
1178 let _ = cliclack::log::info("Notice: ASTER_DISABLE_KEYRING environment variable is set and will override the configuration here.");
1179 }
1180
1181 let currently_disabled = config.get_param::<String>("ASTER_DISABLE_KEYRING").is_ok();
1182
1183 let current_status = if currently_disabled {
1184 "Disabled (using file-based storage)"
1185 } else {
1186 "Enabled (using system keyring)"
1187 };
1188
1189 let _ = cliclack::log::info(format!("Current secret storage: {}", current_status));
1190 let _ = cliclack::log::warning("Note: Disabling the keyring stores secrets in a plain text file (~/.config/aster/secrets.yaml)");
1191
1192 let storage_option = cliclack::select("How would you like to store secrets?")
1193 .item(
1194 "keyring",
1195 "System Keyring (recommended)",
1196 "Use secure system keyring for storing API keys and secrets",
1197 )
1198 .item(
1199 "file",
1200 "File-based Storage",
1201 "Store secrets in a local file (useful when keyring access is restricted)",
1202 )
1203 .interact()?;
1204
1205 match storage_option {
1206 "keyring" => {
1207 config.set_param("ASTER_DISABLE_KEYRING", Value::String("".to_string()))?;
1209 cliclack::outro("Secret storage set to system keyring (secure)")?;
1210 let _ =
1211 cliclack::log::info("You may need to restart aster for this change to take effect");
1212 }
1213 "file" => {
1214 config.set_param("ASTER_DISABLE_KEYRING", Value::String("true".to_string()))?;
1216 cliclack::outro(
1217 "Secret storage set to file (~/.config/aster/secrets.yaml). Keep this file secure!",
1218 )?;
1219 let _ =
1220 cliclack::log::info("You may need to restart aster for this change to take effect");
1221 }
1222 _ => unreachable!(),
1223 };
1224
1225 Ok(())
1226}
1227
1228pub fn toggle_experiments_dialog() -> anyhow::Result<()> {
1231 let experiments = ExperimentManager::get_all()?;
1232
1233 if experiments.is_empty() {
1234 cliclack::outro("No experiments supported yet.")?;
1235 return Ok(());
1236 }
1237
1238 let enabled_experiments: Vec<&String> = experiments
1240 .iter()
1241 .filter(|(_, enabled)| *enabled)
1242 .map(|(name, _)| name)
1243 .collect();
1244
1245 let selected = cliclack::multiselect(
1247 "enable experiments: (use \"space\" to toggle and \"enter\" to submit)",
1248 )
1249 .required(false)
1250 .items(
1251 &experiments
1252 .iter()
1253 .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT))
1254 .collect::<Vec<_>>(),
1255 )
1256 .initial_values(enabled_experiments)
1257 .interact()?;
1258
1259 for name in experiments.iter().map(|(name, _)| name) {
1261 ExperimentManager::set_enabled(name, selected.iter().any(|&s| s.as_str() == name))?;
1262 }
1263
1264 cliclack::outro("Experiments settings updated successfully")?;
1265 Ok(())
1266}
1267
1268pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
1269 let mut extensions: Vec<String> = get_enabled_extensions()
1270 .into_iter()
1271 .map(|ext| ext.name().clone())
1272 .collect();
1273 extensions.push("platform".to_string());
1274
1275 extensions.sort();
1276
1277 let selected_extension_name = cliclack::select("Choose an extension to configure tools")
1278 .items(
1279 &extensions
1280 .iter()
1281 .map(|ext| (ext.clone(), ext.clone(), ""))
1282 .collect::<Vec<_>>(),
1283 )
1284 .interact()?;
1285
1286 let config = Config::global();
1287
1288 let provider_name: String = config
1289 .get_aster_provider()
1290 .expect("No provider configured. Please set model provider first");
1291
1292 let model: String = config
1293 .get_aster_model()
1294 .expect("No model configured. Please set model first");
1295 let model_config = ModelConfig::new(&model)?;
1296
1297 let session = SessionManager::create_session(
1298 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
1299 "Tool Permission Configuration".to_string(),
1300 SessionType::Hidden,
1301 )
1302 .await?;
1303
1304 let agent = Agent::new();
1305 let new_provider = create(&provider_name, model_config).await?;
1306 agent.update_provider(new_provider, &session.id).await?;
1307 if let Some(config) = get_extension_by_name(&selected_extension_name) {
1308 agent
1309 .add_extension(config.clone())
1310 .await
1311 .unwrap_or_else(|_| {
1312 println!(
1313 "{} Failed to check extension: {}",
1314 style("Error").red().italic(),
1315 config.name()
1316 );
1317 });
1318 } else {
1319 println!(
1320 "{} Configuration not found for extension: {}",
1321 style("Warning").yellow().italic(),
1322 selected_extension_name
1323 );
1324 return Ok(());
1325 }
1326
1327 let mut permission_manager = PermissionManager::default();
1328 let selected_tools = agent
1329 .list_tools(Some(selected_extension_name.clone()))
1330 .await
1331 .into_iter()
1332 .map(|tool| {
1333 ToolInfo::new(
1334 &tool.name,
1335 tool.description
1336 .as_ref()
1337 .map(|d| d.as_ref())
1338 .unwrap_or_default(),
1339 get_parameter_names(&tool),
1340 permission_manager.get_user_permission(&tool.name),
1341 )
1342 })
1343 .collect::<Vec<ToolInfo>>();
1344
1345 let tool_name = cliclack::select("Choose a tool to update permission")
1346 .items(
1347 &selected_tools
1348 .iter()
1349 .map(|tool| {
1350 let first_description = tool
1351 .description
1352 .split('.')
1353 .next()
1354 .unwrap_or("No description available")
1355 .trim();
1356 (tool.name.clone(), tool.name.clone(), first_description)
1357 })
1358 .collect::<Vec<_>>(),
1359 )
1360 .interact()?;
1361
1362 let tool = selected_tools
1364 .iter()
1365 .find(|tool| tool.name == tool_name)
1366 .unwrap();
1367
1368 let current_permission = match tool.permission {
1370 Some(PermissionLevel::AlwaysAllow) => "Always Allow",
1371 Some(PermissionLevel::AskBefore) => "Ask Before",
1372 Some(PermissionLevel::NeverAllow) => "Never Allow",
1373 None => "Not Set",
1374 };
1375
1376 let permission = cliclack::select(format!(
1378 "Set permission level for tool {}, current permission level: {}",
1379 tool.name, current_permission
1380 ))
1381 .item(
1382 "always_allow",
1383 "Always Allow",
1384 "Allow this tool to execute without asking",
1385 )
1386 .item(
1387 "ask_before",
1388 "Ask Before",
1389 "Prompt before executing this tool",
1390 )
1391 .item(
1392 "never_allow",
1393 "Never Allow",
1394 "Prevent this tool from executing",
1395 )
1396 .interact()?;
1397
1398 let permission_label = match permission {
1399 "always_allow" => "Always Allow",
1400 "ask_before" => "Ask Before",
1401 "never_allow" => "Never Allow",
1402 _ => unreachable!(),
1403 };
1404
1405 let new_permission = match permission {
1407 "always_allow" => PermissionLevel::AlwaysAllow,
1408 "ask_before" => PermissionLevel::AskBefore,
1409 "never_allow" => PermissionLevel::NeverAllow,
1410 _ => unreachable!(),
1411 };
1412
1413 permission_manager.update_user_permission(&tool.name, new_permission);
1414
1415 cliclack::outro(format!(
1416 "Updated permission level for tool {} to {}.",
1417 tool.name, permission_label
1418 ))?;
1419
1420 cliclack::outro(format!(
1421 "Changes saved to {}",
1422 permission_manager.get_config_path().display()
1423 ))?;
1424
1425 Ok(())
1426}
1427
1428fn configure_recipe_dialog() -> anyhow::Result<()> {
1429 let key_name = ASTER_RECIPE_GITHUB_REPO_CONFIG_KEY;
1430 let config = Config::global();
1431 let default_recipe_repo = std::env::var(key_name)
1432 .ok()
1433 .or_else(|| config.get_param(key_name).unwrap_or(None));
1434 let mut recipe_repo_input = cliclack::input(
1435 "Enter your aster recipe Github repo (owner/repo): eg: my_org/aster-recipes",
1436 )
1437 .required(false);
1438 if let Some(recipe_repo) = default_recipe_repo {
1439 recipe_repo_input = recipe_repo_input.default_input(&recipe_repo);
1440 }
1441 let input_value: String = recipe_repo_input.interact()?;
1442 if input_value.clone().trim().is_empty() {
1443 config.delete(key_name)?;
1444 } else {
1445 config.set_param(key_name, &input_value)?;
1446 }
1447 Ok(())
1448}
1449
1450pub fn configure_max_turns_dialog() -> anyhow::Result<()> {
1451 let config = Config::global();
1452
1453 let current_max_turns: u32 = config.get_param("ASTER_MAX_TURNS").unwrap_or(1000);
1454
1455 let max_turns_input: String =
1456 cliclack::input("Set maximum number of agent turns without user input:")
1457 .placeholder(¤t_max_turns.to_string())
1458 .default_input(¤t_max_turns.to_string())
1459 .validate(|input: &String| match input.parse::<u32>() {
1460 Ok(value) => {
1461 if value < 1 {
1462 Err("Value must be at least 1")
1463 } else {
1464 Ok(())
1465 }
1466 }
1467 Err(_) => Err("Please enter a valid number"),
1468 })
1469 .interact()?;
1470
1471 let max_turns: u32 = max_turns_input.parse()?;
1472 config.set_param("ASTER_MAX_TURNS", max_turns)?;
1473
1474 cliclack::outro(format!(
1475 "Set maximum turns to {} - aster will ask for input after {} consecutive actions",
1476 max_turns, max_turns
1477 ))?;
1478
1479 Ok(())
1480}
1481
1482pub async fn handle_openrouter_auth() -> anyhow::Result<()> {
1484 use aster::config::{configure_openrouter, signup_openrouter::OpenRouterAuth};
1485 use aster::conversation::message::Message;
1486 use aster::providers::create;
1487
1488 let mut auth_flow = OpenRouterAuth::new()?;
1490 let api_key = auth_flow.complete_flow().await?;
1491 println!("\nAuthentication complete!");
1492
1493 let config = Config::global();
1495
1496 println!("\nConfiguring OpenRouter...");
1498 configure_openrouter(config, api_key)?;
1499
1500 println!("✓ OpenRouter configuration complete");
1501 println!("✓ Models configured successfully");
1502
1503 println!("\nTesting configuration...");
1505 let configured_model: String = config.get_aster_model()?;
1506 let model_config = match aster::model::ModelConfig::new(&configured_model) {
1507 Ok(config) => config,
1508 Err(e) => {
1509 eprintln!("⚠️ Invalid model configuration: {}", e);
1510 eprintln!("Your settings have been saved. Please check your model configuration.");
1511 return Ok(());
1512 }
1513 };
1514
1515 match create("openrouter", model_config).await {
1516 Ok(provider) => {
1517 let test_result = provider
1519 .complete(
1520 "You are aster, an AI assistant.",
1521 &[Message::user().with_text("Say 'Configuration test successful!'")],
1522 &[],
1523 )
1524 .await;
1525
1526 match test_result {
1527 Ok(_) => {
1528 println!("✓ Configuration test passed!");
1529
1530 let entries = get_all_extensions();
1532 let has_developer = entries
1533 .iter()
1534 .any(|e| e.config.name() == "developer" && e.enabled);
1535
1536 if !has_developer {
1537 set_extension(ExtensionEntry {
1538 enabled: true,
1539 config: ExtensionConfig::Builtin {
1540 name: "developer".to_string(),
1541 display_name: Some(aster::config::DEFAULT_DISPLAY_NAME.to_string()),
1542 timeout: Some(aster::config::DEFAULT_EXTENSION_TIMEOUT),
1543 bundled: Some(true),
1544 description: "Developer extension".to_string(),
1545 available_tools: Vec::new(),
1546 },
1547 });
1548 println!("✓ Developer extension enabled");
1549 }
1550
1551 cliclack::outro("OpenRouter setup complete! You can now use aster.")?;
1552 }
1553 Err(e) => {
1554 eprintln!("⚠️ Configuration test failed: {}", e);
1555 eprintln!("Your settings have been saved, but there may be an issue with the connection.");
1556 }
1557 }
1558 }
1559 Err(e) => {
1560 eprintln!("⚠️ Failed to create provider for testing: {}", e);
1561 eprintln!("Your settings have been saved. Please check your configuration.");
1562 }
1563 }
1564 Ok(())
1565}
1566
1567pub async fn handle_tetrate_auth() -> anyhow::Result<()> {
1568 let mut auth_flow = TetrateAuth::new()?;
1569 let api_key = auth_flow.complete_flow().await?;
1570
1571 println!("\nAuthentication complete!");
1572
1573 let config = Config::global();
1574
1575 println!("\nConfiguring Tetrate Agent Router Service...");
1576 configure_tetrate(config, api_key)?;
1577
1578 println!("✓ Tetrate Agent Router Service configuration complete");
1579 println!("✓ Models configured successfully");
1580
1581 println!("\nTesting configuration...");
1583 let configured_model: String = config.get_aster_model()?;
1584 let model_config = match aster::model::ModelConfig::new(&configured_model) {
1585 Ok(config) => config,
1586 Err(e) => {
1587 eprintln!("⚠️ Invalid model configuration: {}", e);
1588 eprintln!("Your settings have been saved. Please check your model configuration.");
1589 return Ok(());
1590 }
1591 };
1592
1593 match create("tetrate", model_config).await {
1594 Ok(provider) => {
1595 let test_result = provider
1596 .complete(
1597 "You are aster, an AI assistant.",
1598 &[Message::user().with_text("Say 'Configuration test successful!'")],
1599 &[],
1600 )
1601 .await;
1602
1603 match test_result {
1604 Ok(_) => {
1605 println!("✓ Configuration test passed!");
1606
1607 let entries = get_all_extensions();
1608 let has_developer = entries
1609 .iter()
1610 .any(|e| e.config.name() == "developer" && e.enabled);
1611
1612 if !has_developer {
1613 set_extension(ExtensionEntry {
1614 enabled: true,
1615 config: ExtensionConfig::Builtin {
1616 name: "developer".to_string(),
1617 display_name: Some(aster::config::DEFAULT_DISPLAY_NAME.to_string()),
1618 timeout: Some(aster::config::DEFAULT_EXTENSION_TIMEOUT),
1619 bundled: Some(true),
1620 description: "Developer extension".to_string(),
1621 available_tools: Vec::new(),
1622 },
1623 });
1624 println!("✓ Developer extension enabled");
1625 }
1626
1627 cliclack::outro(
1628 "Tetrate Agent Router Service setup complete! You can now use aster.",
1629 )?;
1630 }
1631 Err(e) => {
1632 eprintln!("⚠️ Configuration test failed: {}", e);
1633 eprintln!("Your settings have been saved, but there may be an issue with the connection.");
1634 }
1635 }
1636 }
1637 Err(e) => {
1638 eprintln!("⚠️ Failed to create provider for testing: {}", e);
1639 eprintln!("Your settings have been saved. Please check your configuration.");
1640 }
1641 }
1642
1643 Ok(())
1644}
1645
1646fn collect_custom_headers() -> anyhow::Result<Option<std::collections::HashMap<String, String>>> {
1648 let use_custom_headers = cliclack::confirm("Does this provider require custom headers?")
1649 .initial_value(false)
1650 .interact()?;
1651
1652 if !use_custom_headers {
1653 return Ok(None);
1654 }
1655
1656 let mut custom_headers = std::collections::HashMap::new();
1657
1658 loop {
1659 let header_name: String = cliclack::input("Header name:")
1660 .placeholder("e.g., x-origin-client-id")
1661 .required(false)
1662 .interact()?;
1663
1664 if header_name.is_empty() {
1665 break;
1666 }
1667
1668 let header_value: String = cliclack::password(format!("Value for '{}':", header_name))
1669 .mask('▪')
1670 .interact()?;
1671
1672 custom_headers.insert(header_name, header_value);
1673
1674 let add_more = cliclack::confirm("Add another header?")
1675 .initial_value(false)
1676 .interact()?;
1677
1678 if !add_more {
1679 break;
1680 }
1681 }
1682
1683 if custom_headers.is_empty() {
1684 Ok(None)
1685 } else {
1686 Ok(Some(custom_headers))
1687 }
1688}
1689
1690fn add_provider() -> anyhow::Result<()> {
1691 let provider_type = cliclack::select("What type of API is this?")
1692 .item(
1693 "openai_compatible",
1694 "OpenAI Compatible",
1695 "Uses OpenAI API format",
1696 )
1697 .item(
1698 "anthropic_compatible",
1699 "Anthropic Compatible",
1700 "Uses Anthropic API format",
1701 )
1702 .item(
1703 "ollama_compatible",
1704 "Ollama Compatible",
1705 "Uses Ollama API format",
1706 )
1707 .interact()?;
1708
1709 let display_name: String = cliclack::input("What should we call this provider?")
1710 .placeholder("Your Provider Name")
1711 .validate(|input: &String| {
1712 if input.is_empty() {
1713 Err("Please enter a name")
1714 } else {
1715 Ok(())
1716 }
1717 })
1718 .interact()?;
1719
1720 let api_url: String = cliclack::input("Provider API URL:")
1721 .placeholder("https://api.example.com/v1/messages")
1722 .validate(|input: &String| {
1723 if !input.starts_with("http://") && !input.starts_with("https://") {
1724 Err("URL must start with either http:// or https://")
1725 } else {
1726 Ok(())
1727 }
1728 })
1729 .interact()?;
1730
1731 let api_key: String = cliclack::password("API key:")
1732 .allow_empty()
1733 .mask('▪')
1734 .interact()?;
1735
1736 let models_input: String = cliclack::input("Available models (separate with commas):")
1737 .placeholder("model-a, model-b, model-c")
1738 .validate(|input: &String| {
1739 if input.trim().is_empty() {
1740 Err("Please enter at least one model name")
1741 } else {
1742 Ok(())
1743 }
1744 })
1745 .interact()?;
1746
1747 let models: Vec<String> = models_input
1748 .split(',')
1749 .map(|s| s.trim().to_string())
1750 .filter(|s| !s.is_empty())
1751 .collect();
1752
1753 let supports_streaming = cliclack::confirm("Does this provider support streaming responses?")
1754 .initial_value(true)
1755 .interact()?;
1756
1757 let headers = if provider_type == "openai_compatible" {
1759 collect_custom_headers()?
1760 } else {
1761 None
1762 };
1763
1764 create_custom_provider(
1765 provider_type,
1766 display_name.clone(),
1767 api_url,
1768 api_key,
1769 models,
1770 Some(supports_streaming),
1771 headers,
1772 )?;
1773
1774 cliclack::outro(format!("Custom provider added: {}", display_name))?;
1775 Ok(())
1776}
1777
1778fn remove_provider() -> anyhow::Result<()> {
1779 let custom_providers_dir = aster::config::declarative_providers::custom_providers_dir();
1780 let custom_providers = if custom_providers_dir.exists() {
1781 aster::config::declarative_providers::load_custom_providers(&custom_providers_dir)?
1782 } else {
1783 Vec::new()
1784 };
1785
1786 if custom_providers.is_empty() {
1787 cliclack::outro("No custom providers added just yet.")?;
1788 return Ok(());
1789 }
1790
1791 let provider_items: Vec<_> = custom_providers
1792 .iter()
1793 .map(|p| (p.name.as_str(), p.display_name.as_str(), "Custom provider"))
1794 .collect();
1795
1796 let selected_id = cliclack::select("Which custom provider would you like to remove?")
1797 .items(&provider_items)
1798 .interact()?;
1799
1800 remove_custom_provider(selected_id)?;
1801 cliclack::outro(format!("Removed custom provider: {}", selected_id))?;
1802 Ok(())
1803}
1804
1805pub fn configure_custom_provider_dialog() -> anyhow::Result<()> {
1806 let action = cliclack::select("What would you like to do?")
1807 .item(
1808 "add",
1809 "Add A Custom Provider",
1810 "Add a new OpenAI/Anthropic/Ollama compatible Provider",
1811 )
1812 .item(
1813 "remove",
1814 "Remove Custom Provider",
1815 "Remove an existing custom provider",
1816 )
1817 .interact()?;
1818
1819 match action {
1820 "add" => add_provider(),
1821 "remove" => remove_provider(),
1822 _ => unreachable!(),
1823 }?;
1824
1825 print_config_file_saved()?;
1826
1827 Ok(())
1828}
1829
1830fn print_config_file_saved() -> anyhow::Result<()> {
1831 let config = Config::global();
1832 cliclack::outro(format!(
1833 "Configuration saved successfully to {}",
1834 config.path()
1835 ))?;
1836 Ok(())
1837}