use cliclack::spinner; use console::style; use goose::agents::extension::ToolInfo; use goose::agents::extension_manager::get_parameter_names; use goose::agents::platform_tools::{ PLATFORM_LIST_RESOURCES_TOOL_NAME, PLATFORM_READ_RESOURCE_TOOL_NAME, }; use goose::agents::Agent; use goose::agents::{extension::Envs, ExtensionConfig}; use goose::config::extensions::name_to_key; use goose::config::permission::PermissionLevel; use goose::config::{ Config, ConfigError, ExperimentManager, ExtensionConfigManager, ExtensionEntry, PermissionManager, }; use goose::message::Message; use goose::providers::{create, providers}; use mcp_core::tool::ToolAnnotations; use mcp_core::Tool; use serde_json::{json, Value}; use std::collections::HashMap; use std::error::Error; use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY; // useful for light themes where there is no dicernible colour contrast between // cursor-selected and cursor-unselected items. const MULTISELECT_VISIBILITY_HINT: &str = "<"; fn get_display_name(extension_id: &str) -> String { match extension_id { "developer" => "Developer Tools".to_string(), "computercontroller" => "Computer Controller".to_string(), "googledrive" => "Google Drive".to_string(), "memory" => "Memory".to_string(), "tutorial" => "Tutorial".to_string(), "jetbrains" => "JetBrains".to_string(), // Add other extensions as needed _ => { extension_id .chars() .next() .unwrap_or_default() .to_uppercase() .collect::() + &extension_id[1..] } } } pub async fn handle_configure() -> Result<(), Box> { let config = Config::global(); if !config.exists() { // First time setup flow println!(); println!( "{}", style("Welcome to goose! Let's get you set up with a provider.").dim() ); println!( "{}", style(" you can rerun this command later to update your configuration").dim() ); println!(); cliclack::intro(style(" goose-configure ").on_cyan().black())?; match configure_provider_dialog().await { Ok(true) => { println!( "\n {}: Run '{}' again to adjust your config or add extensions", style("Tip").green().italic(), style("goose configure").cyan() ); // Since we are setting up for the first time, we'll also enable the developer system // This operation is best-effort and errors are ignored ExtensionConfigManager::set(ExtensionEntry { enabled: true, config: ExtensionConfig::Builtin { name: "developer".to_string(), display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()), timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: Some(true), }, })?; } Ok(false) => { let _ = config.clear(); println!( "\n {}: We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", style("Warning").yellow().italic(), style("goose configure").cyan() ); } Err(e) => { let _ = config.clear(); match e.downcast_ref::() { Some(ConfigError::NotFound(key)) => { println!( "\n {} Required configuration key '{}' not found \n Please provide this value and run '{}' again", style("Error").red().italic(), key, style("goose configure").cyan() ); } Some(ConfigError::KeyringError(msg)) => { #[cfg(target_os = "macos")] println!( "\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.", style("Error").red().italic(), msg, style("goose configure").cyan() ); #[cfg(target_os = "windows")] println!( "\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.", style("Error").red().italic(), msg, style("goose configure").cyan() ); #[cfg(not(any(target_os = "macos", target_os = "windows")))] println!( "\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.", style("Error").red().italic(), msg, style("goose configure").cyan() ); } Some(ConfigError::DeserializeError(msg)) => { println!( "\n {} Invalid configuration value: {} \n Please check your input and run '{}' again", style("Error").red().italic(), msg, style("goose configure").cyan() ); } Some(ConfigError::FileError(e)) => { println!( "\n {} Failed to access config file: {} \n Please check file permissions and run '{}' again", style("Error").red().italic(), e, style("goose configure").cyan() ); } Some(ConfigError::DirectoryError(msg)) => { println!( "\n {} Failed to access config directory: {} \n Please check directory permissions and run '{}' again", style("Error").red().italic(), msg, style("goose configure").cyan() ); } // handle all other nonspecific errors _ => { println!( "\n {} {} \n We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", style("Error").red().italic(), e, style("goose configure").cyan() ); } } } } Ok(()) } else { println!(); println!( "{}", style("This will update your existing config file").dim() ); println!( "{} {}", style(" if you prefer, you can edit it directly at").dim(), config.path() ); println!(); cliclack::intro(style(" goose-configure ").on_cyan().black())?; let action = cliclack::select("What would you like to configure?") .item( "providers", "Configure Providers", "Change provider or update credentials", ) .item("add", "Add Extension", "Connect to a new extension") .item( "toggle", "Toggle Extensions", "Enable or disable connected extensions", ) .item("remove", "Remove Extension", "Remove an extension") .item( "settings", "Goose Settings", "Set the Goose Mode, Tool Output, Tool Permissions, Experiment, Goose recipe github repo and more", ) .interact()?; match action { "toggle" => toggle_extensions_dialog(), "add" => configure_extensions_dialog(), "remove" => remove_extension_dialog(), "settings" => configure_settings_dialog().await.and(Ok(())), "providers" => configure_provider_dialog().await.and(Ok(())), _ => unreachable!(), } } } /// Dialog for configuring the AI provider and model pub async fn configure_provider_dialog() -> Result> { // Get global config instance let config = Config::global(); // Get all available providers and their metadata let available_providers = providers(); // Create selection items from provider metadata let provider_items: Vec<(&String, &str, &str)> = available_providers .iter() .map(|p| (&p.name, p.display_name.as_str(), p.description.as_str())) .collect(); // Get current default provider if it exists let current_provider: Option = config.get_param("GOOSE_PROVIDER").ok(); let default_provider = current_provider.unwrap_or_default(); // Select provider let provider_name = cliclack::select("Which model provider should we use?") .initial_value(&default_provider) .items(&provider_items) .interact()?; // Get the selected provider's metadata let provider_meta = available_providers .iter() .find(|p| &p.name == provider_name) .expect("Selected provider must exist in metadata"); // Configure required provider keys for key in &provider_meta.config_keys { if !key.required { continue; } // First check if the value is set via environment variable let from_env = std::env::var(&key.name).ok(); match from_env { Some(env_value) => { let _ = cliclack::log::info(format!("{} is set via environment variable", key.name)); if cliclack::confirm("Would you like to save this value to your keyring?") .initial_value(true) .interact()? { if key.secret { config.set_secret(&key.name, Value::String(env_value))?; } else { config.set_param(&key.name, Value::String(env_value))?; } let _ = cliclack::log::info(format!("Saved {} to config file", key.name)); } } None => { // No env var, check config/secret storage let existing: Result = if key.secret { config.get_secret(&key.name) } else { config.get_param(&key.name) }; match existing { Ok(_) => { let _ = cliclack::log::info(format!("{} is already configured", key.name)); if cliclack::confirm("Would you like to update this value?").interact()? { let new_value: String = if key.secret { cliclack::password(format!("Enter new value for {}", key.name)) .mask('▪') .interact()? } else { let mut input = cliclack::input(format!("Enter new value for {}", key.name)); if key.default.is_some() { input = input.default_input(&key.default.clone().unwrap()); } input.interact()? }; if key.secret { config.set_secret(&key.name, Value::String(new_value))?; } else { config.set_param(&key.name, Value::String(new_value))?; } } } Err(_) => { let value: String = if key.secret { cliclack::password(format!( "Provider {} requires {}, please enter a value", provider_meta.display_name, key.name )) .mask('▪') .interact()? } else { let mut input = cliclack::input(format!( "Provider {} requires {}, please enter a value", provider_meta.display_name, key.name )); if key.default.is_some() { input = input.default_input(&key.default.clone().unwrap()); } input.interact()? }; if key.secret { config.set_secret(&key.name, Value::String(value))?; } else { config.set_param(&key.name, Value::String(value))?; } } } } } } // Attempt to fetch supported models for this provider let spin = spinner(); spin.start("Attempting to fetch supported models..."); let models_res = { let temp_model_config = goose::model::ModelConfig::new(provider_meta.default_model.clone()); let temp_provider = create(provider_name, temp_model_config)?; temp_provider.fetch_supported_models_async().await }; spin.stop(style("Model fetch complete").green()); // Select a model: on fetch error show styled error and abort; if Some(models), show list; if None, free-text input let model: String = match models_res { Err(e) => { // Provider hook error cliclack::outro(style(e.to_string()).on_red().white())?; return Ok(false); } Ok(Some(models)) => cliclack::select("Select a model:") .items( &models .iter() .map(|m| (m, m.as_str(), "")) .collect::>(), ) .interact()? .to_string(), Ok(None) => { let default_model = std::env::var("GOOSE_MODEL").unwrap_or(provider_meta.default_model.clone()); cliclack::input("Enter a model from that provider:") .default_input(&default_model) .interact()? } }; // Test the configuration let spin = spinner(); spin.start("Checking your configuration..."); // Create model config with env var settings let toolshim_enabled = std::env::var("GOOSE_TOOLSHIM") .map(|val| val == "1" || val.to_lowercase() == "true") .unwrap_or(false); let model_config = goose::model::ModelConfig::new(model.clone()) .with_max_tokens(Some(50)) .with_toolshim(toolshim_enabled) .with_toolshim_model(std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL").ok()); let provider = create(provider_name, model_config)?; let messages = vec![Message::user().with_text("What is the weather like in San Francisco today?")]; // Only add the sample tool if toolshim is not enabled let tools = if !toolshim_enabled { let sample_tool = Tool::new( "get_weather".to_string(), "Get current temperature for a given location.".to_string(), json!({ "type": "object", "required": ["location"], "properties": { "location": {"type": "string"} } }), Some(ToolAnnotations { title: Some("Get weather".to_string()), read_only_hint: true, destructive_hint: false, idempotent_hint: false, open_world_hint: false, }), ); vec![sample_tool] } else { vec![] }; let result = provider .complete( "You are an AI agent called Goose. You use tools of connected extensions to solve problems.", &messages, &tools ) .await; match result { Ok((_message, _usage)) => { // Update config with new values only if the test succeeds config.set_param("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?; config.set_param("GOOSE_MODEL", Value::String(model.clone()))?; cliclack::outro("Configuration saved successfully")?; Ok(true) } Err(e) => { spin.stop(style(e.to_string()).red()); cliclack::outro(style("Failed to configure provider: init chat completion request with tool did not succeed.").on_red().white())?; Ok(false) } } } /// Configure extensions that can be used with goose /// Dialog for toggling which extensions are enabled/disabled pub fn toggle_extensions_dialog() -> Result<(), Box> { let extensions = ExtensionConfigManager::get_all()?; if extensions.is_empty() { cliclack::outro( "No extensions configured yet. Run configure and add some extensions first.", )?; return Ok(()); } // Create a list of extension names and their enabled status let mut extension_status: Vec<(String, bool)> = extensions .iter() .map(|entry| (entry.config.name().to_string(), entry.enabled)) .collect(); // Sort extensions alphabetically by name extension_status.sort_by(|a, b| a.0.cmp(&b.0)); // Get currently enabled extensions for the selection let enabled_extensions: Vec<&String> = extension_status .iter() .filter(|(_, enabled)| *enabled) .map(|(name, _)| name) .collect(); // Let user toggle extensions let selected = cliclack::multiselect( "enable extensions: (use \"space\" to toggle and \"enter\" to submit)", ) .required(false) .items( &extension_status .iter() .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT)) .collect::>(), ) .initial_values(enabled_extensions) .interact()?; // Update enabled status for each extension for name in extension_status.iter().map(|(name, _)| name) { ExtensionConfigManager::set_enabled( &name_to_key(name), selected.iter().any(|s| s.as_str() == name), )?; } cliclack::outro("Extension settings updated successfully")?; Ok(()) } pub fn configure_extensions_dialog() -> Result<(), Box> { let extension_type = cliclack::select("What type of extension would you like to add?") .item( "built-in", "Built-in Extension", "Use an extension that comes with Goose", ) .item( "stdio", "Command-line Extension", "Run a local command or script", ) .item( "sse", "Remote Extension", "Connect to a remote extension via SSE", ) .interact()?; match extension_type { // TODO we'll want a place to collect all these options, maybe just an enum in goose-mcp "built-in" => { let extension = cliclack::select("Which built-in extension would you like to enable?") .item( "computercontroller", "Computer Controller", "controls for webscraping, file caching, and automations", ) .item( "developer", "Developer Tools", "Code editing and shell access", ) .item( "googledrive", "Google Drive", "Search and read content from google drive - additional config required", ) .item("jetbrains", "JetBrains", "Connect to jetbrains IDEs") .item( "memory", "Memory", "Tools to save and retrieve durable memories", ) .item( "tutorial", "Tutorial", "Access interactive tutorials and guides", ) .interact()? .to_string(); let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):") .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string()) .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), Err(_) => Err("Please enter a valid timeout"), }) .interact()?; let display_name = get_display_name(&extension); ExtensionConfigManager::set(ExtensionEntry { enabled: true, config: ExtensionConfig::Builtin { name: extension.clone(), display_name: Some(display_name), timeout: Some(timeout), bundled: Some(true), }, })?; cliclack::outro(format!("Enabled {} extension", style(extension).green()))?; } "stdio" => { let extensions = ExtensionConfigManager::get_all_names()?; let name: String = cliclack::input("What would you like to call this extension?") .placeholder("my-extension") .validate(move |input: &String| { if input.is_empty() { Err("Please enter a name") } else if extensions.contains(input) { Err("An extension with this name already exists") } else { Ok(()) } }) .interact()?; let command_str: String = cliclack::input("What command should be run?") .placeholder("npx -y @block/gdrive") .validate(|input: &String| { if input.is_empty() { Err("Please enter a command") } else { Ok(()) } }) .interact()?; let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):") .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string()) .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), Err(_) => Err("Please enter a valid timeout"), }) .interact()?; // Split the command string into command and args // TODO: find a way to expose this to the frontend so we dont need to re-write code let mut parts = command_str.split_whitespace(); let cmd = parts.next().unwrap_or("").to_string(); let args: Vec = parts.map(String::from).collect(); let add_desc = cliclack::confirm("Would you like to add a description?").interact()?; let description = if add_desc { let desc = cliclack::input("Enter a description for this extension:") .placeholder("Description") .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), Err(_) => Err("Please enter a valid description"), }) .interact()?; Some(desc) } else { None }; let add_env = cliclack::confirm("Would you like to add environment variables?").interact()?; let mut envs = HashMap::new(); let mut env_keys = Vec::new(); let config = Config::global(); if add_env { loop { let key: String = cliclack::input("Environment variable name:") .placeholder("API_KEY") .interact()?; let value: String = cliclack::password("Environment variable value:") .mask('▪') .interact()?; // Try to store in keychain let keychain_key = key.to_string(); match config.set_secret(&keychain_key, Value::String(value.clone())) { Ok(_) => { // Successfully stored in keychain, add to env_keys env_keys.push(keychain_key); } Err(_) => { // Failed to store in keychain, store directly in envs envs.insert(key, value); } } if !cliclack::confirm("Add another environment variable?").interact()? { break; } } } ExtensionConfigManager::set(ExtensionEntry { enabled: true, config: ExtensionConfig::Stdio { name: name.clone(), cmd, args, envs: Envs::new(envs), env_keys, description, timeout: Some(timeout), bundled: None, }, })?; cliclack::outro(format!("Added {} extension", style(name).green()))?; } "sse" => { let extensions = ExtensionConfigManager::get_all_names()?; let name: String = cliclack::input("What would you like to call this extension?") .placeholder("my-remote-extension") .validate(move |input: &String| { if input.is_empty() { Err("Please enter a name") } else if extensions.contains(input) { Err("An extension with this name already exists") } else { Ok(()) } }) .interact()?; let uri: String = cliclack::input("What is the SSE endpoint URI?") .placeholder("http://localhost:8000/events") .validate(|input: &String| { if input.is_empty() { Err("Please enter a URI") } else if !input.starts_with("http") { Err("URI should start with http:// or https://") } else { Ok(()) } }) .interact()?; let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):") .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string()) .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), Err(_) => Err("Please enter a valid timeout"), }) .interact()?; let add_desc = cliclack::confirm("Would you like to add a description?").interact()?; let description = if add_desc { let desc = cliclack::input("Enter a description for this extension:") .placeholder("Description") .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), Err(_) => Err("Please enter a valid description"), }) .interact()?; Some(desc) } else { None }; let add_env = cliclack::confirm("Would you like to add environment variables?").interact()?; let mut envs = HashMap::new(); let mut env_keys = Vec::new(); let config = Config::global(); if add_env { loop { let key: String = cliclack::input("Environment variable name:") .placeholder("API_KEY") .interact()?; let value: String = cliclack::password("Environment variable value:") .mask('▪') .interact()?; // Try to store in keychain let keychain_key = key.to_string(); match config.set_secret(&keychain_key, Value::String(value.clone())) { Ok(_) => { // Successfully stored in keychain, add to env_keys env_keys.push(keychain_key); } Err(_) => { // Failed to store in keychain, store directly in envs envs.insert(key, value); } } if !cliclack::confirm("Add another environment variable?").interact()? { break; } } } ExtensionConfigManager::set(ExtensionEntry { enabled: true, config: ExtensionConfig::Sse { name: name.clone(), uri, envs: Envs::new(envs), env_keys, description, timeout: Some(timeout), bundled: None, }, })?; cliclack::outro(format!("Added {} extension", style(name).green()))?; } _ => unreachable!(), }; Ok(()) } pub fn remove_extension_dialog() -> Result<(), Box> { let extensions = ExtensionConfigManager::get_all()?; // Create a list of extension names and their enabled status let mut extension_status: Vec<(String, bool)> = extensions .iter() .map(|entry| (entry.config.name().to_string(), entry.enabled)) .collect(); // Sort extensions alphabetically by name extension_status.sort_by(|a, b| a.0.cmp(&b.0)); if extensions.is_empty() { cliclack::outro( "No extensions configured yet. Run configure and add some extensions first.", )?; return Ok(()); } // Check if all extensions are enabled if extension_status.iter().all(|(_, enabled)| *enabled) { cliclack::outro( "All extensions are currently enabled. You must first disable extensions before removing them.", )?; return Ok(()); } // Filter out only disabled extensions let disabled_extensions: Vec<_> = extensions .iter() .filter(|entry| !entry.enabled) .map(|entry| (entry.config.name().to_string(), entry.enabled)) .collect(); let selected = cliclack::multiselect("Select extensions to remove (note: you can only remove disabled extensions - use \"space\" to toggle and \"enter\" to submit)") .required(false) .items( &disabled_extensions .iter() .filter(|(_, enabled)| !enabled) .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT)) .collect::>(), ) .interact()?; for name in selected { ExtensionConfigManager::remove(&name_to_key(name))?; let mut permission_manager = PermissionManager::default(); permission_manager.remove_extension(&name_to_key(name)); cliclack::outro(format!("Removed {} extension", style(name).green()))?; } Ok(()) } pub async fn configure_settings_dialog() -> Result<(), Box> { let setting_type = cliclack::select("What setting would you like to configure?") .item("goose_mode", "Goose Mode", "Configure Goose mode") .item( "goose_router_strategy", "Router Tool Selection Strategy", "Configure the strategy for selecting tools to use", ) .item( "tool_permission", "Tool Permission", "Set permission for individual tool of enabled extensions", ) .item( "tool_output", "Tool Output", "Show more or less tool output", ) .item( "experiment", "Toggle Experiment", "Enable or disable an experiment feature", ) .item( "recipe", "Goose recipe github repo", "Goose will pull recipes from this repo if not found locally.", ) .interact()?; match setting_type { "goose_mode" => { configure_goose_mode_dialog()?; } "goose_router_strategy" => { configure_goose_router_strategy_dialog()?; } "tool_permission" => { configure_tool_permissions_dialog().await.and(Ok(()))?; } "tool_output" => { configure_tool_output_dialog()?; } "experiment" => { toggle_experiments_dialog()?; } "recipe" => { configure_recipe_dialog()?; } _ => unreachable!(), }; Ok(()) } pub fn configure_goose_mode_dialog() -> Result<(), Box> { let config = Config::global(); // Check if GOOSE_MODE is set as an environment variable if std::env::var("GOOSE_MODE").is_ok() { let _ = cliclack::log::info("Notice: GOOSE_MODE environment variable is set and will override the configuration here."); } let mode = cliclack::select("Which Goose mode would you like to configure?") .item( "auto", "Auto Mode", "Full file modification, extension usage, edit, create and delete files freely" ) .item( "approve", "Approve Mode", "All tools, extensions and file modifications will require human approval" ) .item( "smart_approve", "Smart Approve Mode", "Editing, creating, deleting files and using extensions will require human approval" ) .item( "chat", "Chat Mode", "Engage with the selected provider without using tools, extensions, or file modification" ) .interact()?; match mode { "auto" => { config.set_param("GOOSE_MODE", Value::String("auto".to_string()))?; cliclack::outro("Set to Auto Mode - full file modification enabled")?; } "approve" => { config.set_param("GOOSE_MODE", Value::String("approve".to_string()))?; cliclack::outro("Set to Approve Mode - all tools and modifications require approval")?; } "smart_approve" => { config.set_param("GOOSE_MODE", Value::String("smart_approve".to_string()))?; cliclack::outro("Set to Smart Approve Mode - modifications require approval")?; } "chat" => { config.set_param("GOOSE_MODE", Value::String("chat".to_string()))?; cliclack::outro("Set to Chat Mode - no tools or modifications enabled")?; } _ => unreachable!(), }; Ok(()) } pub fn configure_goose_router_strategy_dialog() -> Result<(), Box> { let config = Config::global(); // Check if GOOSE_ROUTER_STRATEGY is set as an environment variable if std::env::var("GOOSE_ROUTER_TOOL_SELECTION_STRATEGY").is_ok() { let _ = cliclack::log::info("Notice: GOOSE_ROUTER_TOOL_SELECTION_STRATEGY environment variable is set. Configuration will override this."); } let strategy = cliclack::select("Which router strategy would you like to use?") .item( "vector", "Vector Strategy", "Use vector-based similarity to select tools", ) .item( "default", "Default Strategy", "Use the default tool selection strategy", ) .interact()?; match strategy { "vector" => { config.set_param( "GOOSE_ROUTER_TOOL_SELECTION_STRATEGY", Value::String("vector".to_string()), )?; cliclack::outro( "Set to Vector Strategy - using vector-based similarity for tool selection", )?; } "default" => { config.set_param( "GOOSE_ROUTER_TOOL_SELECTION_STRATEGY", Value::String("default".to_string()), )?; cliclack::outro("Set to Default Strategy - using default tool selection")?; } _ => unreachable!(), }; Ok(()) } pub fn configure_tool_output_dialog() -> Result<(), Box> { let config = Config::global(); // Check if GOOSE_CLI_MIN_PRIORITY is set as an environment variable if std::env::var("GOOSE_CLI_MIN_PRIORITY").is_ok() { let _ = cliclack::log::info("Notice: GOOSE_CLI_MIN_PRIORITY environment variable is set and will override the configuration here."); } let tool_log_level = cliclack::select("Which tool output would you like to show?") .item("high", "High Importance", "") .item("medium", "Medium Importance", "Ex. results of file-writes") .item("all", "All (default)", "Ex. shell command output") .interact()?; match tool_log_level { "high" => { config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.8))?; cliclack::outro("Showing tool output of high importance only.")?; } "medium" => { config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.2))?; cliclack::outro("Showing tool output of medium importance.")?; } "all" => { config.set_param("GOOSE_CLI_MIN_PRIORITY", Value::from(0.0))?; cliclack::outro("Showing all tool output.")?; } _ => unreachable!(), }; Ok(()) } /// Configure experiment features that can be used with goose /// Dialog for toggling which experiments are enabled/disabled pub fn toggle_experiments_dialog() -> Result<(), Box> { let experiments = ExperimentManager::get_all()?; if experiments.is_empty() { cliclack::outro("No experiments supported yet.")?; return Ok(()); } // Get currently enabled experiments for the selection let enabled_experiments: Vec<&String> = experiments .iter() .filter(|(_, enabled)| *enabled) .map(|(name, _)| name) .collect(); // Let user toggle experiments let selected = cliclack::multiselect( "enable experiments: (use \"space\" to toggle and \"enter\" to submit)", ) .required(false) .items( &experiments .iter() .map(|(name, _)| (name, name.as_str(), MULTISELECT_VISIBILITY_HINT)) .collect::>(), ) .initial_values(enabled_experiments) .interact()?; // Update enabled status for each experiments for name in experiments.iter().map(|(name, _)| name) { ExperimentManager::set_enabled(name, selected.iter().any(|&s| s.as_str() == name))?; } cliclack::outro("Experiments settings updated successfully")?; Ok(()) } pub async fn configure_tool_permissions_dialog() -> Result<(), Box> { let mut extensions: Vec = ExtensionConfigManager::get_all() .unwrap_or_default() .into_iter() .filter(|ext| ext.enabled) .map(|ext| ext.config.name().clone()) .collect(); extensions.push("platform".to_string()); // Sort extensions alphabetically by name extensions.sort(); let selected_extension_name = cliclack::select("Choose an extension to configure tools") .items( &extensions .iter() .map(|ext| (ext.clone(), ext.clone(), "")) .collect::>(), ) .interact()?; // Fetch tools for the selected extension // Load config and get provider/model let config = Config::global(); let provider_name: String = config .get_param("GOOSE_PROVIDER") .expect("No provider configured. Please set model provider first"); let model: String = config .get_param("GOOSE_MODEL") .expect("No model configured. Please set model first"); let model_config = goose::model::ModelConfig::new(model.clone()); // Create the agent let agent = Agent::new(); let new_provider = create(&provider_name, model_config)?; agent.update_provider(new_provider).await?; if let Ok(Some(config)) = ExtensionConfigManager::get_config_by_name(&selected_extension_name) { agent .add_extension(config.clone()) .await .unwrap_or_else(|_| { println!( "{} Failed to check extension: {}", style("Error").red().italic(), config.name() ); }); } else { println!( "{} Configuration not found for extension: {}", style("Warning").yellow().italic(), selected_extension_name ); return Ok(()); } let mut permission_manager = PermissionManager::default(); let selected_tools = agent .list_tools(Some(selected_extension_name.clone())) .await .into_iter() .filter(|tool| { tool.name != PLATFORM_LIST_RESOURCES_TOOL_NAME && tool.name != PLATFORM_READ_RESOURCE_TOOL_NAME }) .map(|tool| { ToolInfo::new( &tool.name, &tool.description, get_parameter_names(&tool), permission_manager.get_user_permission(&tool.name), ) }) .collect::>(); let tool_name = cliclack::select("Choose a tool to update permission") .items( &selected_tools .iter() .map(|tool| { let first_description = tool .description .split('.') .next() .unwrap_or("No description available") .trim(); (tool.name.clone(), tool.name.clone(), first_description) }) .collect::>(), ) .interact()?; // Find the selected tool let tool = selected_tools .iter() .find(|tool| tool.name == tool_name) .unwrap(); // Display tool description and current permission level let current_permission = match tool.permission { Some(PermissionLevel::AlwaysAllow) => "Always Allow", Some(PermissionLevel::AskBefore) => "Ask Before", Some(PermissionLevel::NeverAllow) => "Never Allow", None => "Not Set", }; // Allow user to set the permission level let permission = cliclack::select(format!( "Set permission level for tool {}, current permission level: {}", tool.name, current_permission )) .item( "always_allow", "Always Allow", "Allow this tool to execute without asking", ) .item( "ask_before", "Ask Before", "Prompt before executing this tool", ) .item( "never_allow", "Never Allow", "Prevent this tool from executing", ) .interact()?; let permission_label = match permission { "always_allow" => "Always Allow", "ask_before" => "Ask Before", "never_allow" => "Never Allow", _ => unreachable!(), }; // Update the permission level in the configuration let new_permission = match permission { "always_allow" => PermissionLevel::AlwaysAllow, "ask_before" => PermissionLevel::AskBefore, "never_allow" => PermissionLevel::NeverAllow, _ => unreachable!(), }; permission_manager.update_user_permission(&tool.name, new_permission); cliclack::outro(format!( "Updated permission level for tool {} to {}.", tool.name, permission_label ))?; Ok(()) } fn configure_recipe_dialog() -> Result<(), Box> { let key_name = GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY; let config = Config::global(); let default_recipe_repo = std::env::var(key_name) .ok() .or_else(|| config.get_param(key_name).unwrap_or(None)); let mut recipe_repo_input = cliclack::input( "Enter your Goose Recipe Github repo (owner/repo): eg: my_org/goose-recipes", ) .required(false); if let Some(recipe_repo) = default_recipe_repo { recipe_repo_input = recipe_repo_input.default_input(&recipe_repo); } let input_value: String = recipe_repo_input.interact()?; // if input is blank, it clears the recipe github repo settings in the config file if input_value.clone().trim().is_empty() { config.delete(key_name)?; } else { config.set_param(key_name, Value::String(input_value))?; } Ok(()) }