Skip to main content

aster_cli/commands/
configure.rs

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
28// useful for light themes where there is no dicernible colour contrast between
29// cursor-selected and cursor-unselected items.
30const 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
258/// Helper function to handle OAuth configuration for a provider
259async 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    // Create a temporary provider instance to handle OAuth
266    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    // Smart model selection:
377    // If we have more than MAX_MODELS models, show the recommended models with additional search option.
378    // Otherwise, show all models without search.
379
380    if models.len() > MAX_MODELS {
381        // Get recommended models from provider metadata
382        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        // just a few models, show all without search for better UX
418        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    // Get global config instance
445    let config = Config::global();
446
447    // Get all available providers and their metadata
448    let mut available_providers = providers().await;
449
450    // Sort providers alphabetically by display name
451    available_providers.sort_by(|a, b| a.0.display_name.cmp(&b.0.display_name));
452
453    // Create selection items from provider metadata
454    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    // Get current default provider if it exists
460    let current_provider: Option<String> = config.get_aster_provider().ok();
461    let default_provider = current_provider.unwrap_or_default();
462
463    // Select provider
464    let provider_name = cliclack::select("Which model provider should we use?")
465        .initial_value(&default_provider)
466        .items(&provider_items)
467        .interact()?;
468
469    // Get the selected provider's metadata
470    let (provider_meta, _) = available_providers
471        .iter()
472        .find(|(p, _)| &p.name == provider_name)
473        .expect("Selected provider must exist in metadata");
474
475    // Configure required provider keys
476    for key in &provider_meta.config_keys {
477        if !key.required {
478            continue;
479        }
480
481        // First check if the value is set via environment variable
482        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                // No env var, check config/secret storage
504                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                            // Check if this key uses OAuth flow
515                            if key.oauth_flow {
516                                handle_oauth_configuration(provider_name, &key.name).await?;
517                            } else {
518                                // Non-OAuth key, use manual entry
519                                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                            // Non-OAuth key, use manual entry
549                            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    // Select a model: on fetch error show styled error and abort; if Some(models), show list; if None, free-text input
592    let model: String = match models_res {
593        Err(e) => {
594            // Provider hook error
595            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    // Test the configuration
609    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
633/// Configure extensions that can be used with aster
634/// Dialog for toggling which extensions are enabled/disabled
635pub 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    // Create a list of extension names and their enabled status
650    let mut extension_status: Vec<(String, bool)> = extensions
651        .iter()
652        .map(|entry| (entry.config.name().to_string(), entry.enabled))
653        .collect();
654
655    // Sort extensions alphabetically by name
656    extension_status.sort_by(|a, b| a.0.cmp(&b.0));
657
658    // Get currently enabled extensions for the selection
659    let enabled_extensions: Vec<&String> = extension_status
660        .iter()
661        .filter(|(_, enabled)| *enabled)
662        .map(|(name, _)| name)
663        .collect();
664
665    // Let user toggle extensions
666    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    // Update enabled status for each extension
680    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    // Original behavior: no env var collection for Streamable HTTP
918    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    // Create a list of extension names and their enabled status
978    let mut extension_status: Vec<(String, bool)> = extensions
979        .iter()
980        .map(|entry| (entry.config.name().to_string(), entry.enabled))
981        .collect();
982
983    // Sort extensions alphabetically by name
984    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    // Check if all extensions are enabled
994    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    // Filter out only disabled extensions
1002    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            // No need to print config file path since it's already handled.
1075            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            // Set to empty string to enable keyring (absence or empty = enabled)
1208            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            // Set the disable flag to use file storage
1215            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
1228/// Configure experiment features that can be used with aster
1229/// Dialog for toggling which experiments are enabled/disabled
1230pub 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    // Get currently enabled experiments for the selection
1239    let enabled_experiments: Vec<&String> = experiments
1240        .iter()
1241        .filter(|(_, enabled)| *enabled)
1242        .map(|(name, _)| name)
1243        .collect();
1244
1245    // Let user toggle experiments
1246    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    // Update enabled status for each experiments
1260    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    // Find the selected tool
1363    let tool = selected_tools
1364        .iter()
1365        .find(|tool| tool.name == tool_name)
1366        .unwrap();
1367
1368    // Display tool description and current permission level
1369    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    // Allow user to set the permission level
1377    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    // Update the permission level in the configuration
1406    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(&current_max_turns.to_string())
1458            .default_input(&current_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
1482/// Handle OpenRouter authentication
1483pub 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    // Use the OpenRouter authentication flow
1489    let mut auth_flow = OpenRouterAuth::new()?;
1490    let api_key = auth_flow.complete_flow().await?;
1491    println!("\nAuthentication complete!");
1492
1493    // Get config instance
1494    let config = Config::global();
1495
1496    // Use the existing configure_openrouter function to set everything up
1497    println!("\nConfiguring OpenRouter...");
1498    configure_openrouter(config, api_key)?;
1499
1500    println!("✓ OpenRouter configuration complete");
1501    println!("✓ Models configured successfully");
1502
1503    // Test configuration - get the model that was configured
1504    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            // Simple test request
1518            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                    // Enable the developer extension by default if not already enabled
1531                    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    // Test configuration
1582    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
1646/// Prompts the user to collect custom HTTP headers for a provider.
1647fn 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    // Ask about custom headers for OpenAI compatible providers
1758    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}