diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 093fe234..a112f18f 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -690,6 +690,251 @@ pub fn toggle_extensions_dialog() -> anyhow::Result<()> { Ok(()) } +fn prompt_extension_timeout() -> anyhow::Result { + Ok( + 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()?, + ) +} + +fn prompt_extension_description() -> anyhow::Result { + Ok(cliclack::input("Enter a description for this extension:") + .placeholder("Description") + .validate(|input: &String| { + if input.trim().is_empty() { + Err("Please enter a valid description") + } else { + Ok(()) + } + }) + .interact()?) +} + +fn prompt_extension_name(placeholder: &str) -> anyhow::Result { + let extensions = get_all_extension_names(); + Ok( + cliclack::input("What would you like to call this extension?") + .placeholder(placeholder) + .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()?, + ) +} + +fn collect_env_vars() -> anyhow::Result<(HashMap, Vec)> { + let mut envs = HashMap::new(); + let mut env_keys = Vec::new(); + let config = Config::global(); + + if !cliclack::confirm("Would you like to add environment variables?").interact()? { + return Ok((envs, env_keys)); + } + + loop { + let key: String = cliclack::input("Environment variable name:") + .placeholder("API_KEY") + .interact()?; + + let value: String = cliclack::password("Environment variable value:") + .mask('▪') + .interact()?; + + match config.set_secret(&key, &value) { + Ok(_) => env_keys.push(key), + Err(_) => { + envs.insert(key, value); + } + } + + if !cliclack::confirm("Add another environment variable?").interact()? { + break; + } + } + + Ok((envs, env_keys)) +} + +fn collect_headers() -> anyhow::Result> { + let mut headers = HashMap::new(); + + if !cliclack::confirm("Would you like to add custom headers?").interact()? { + return Ok(headers); + } + + loop { + let key: String = cliclack::input("Header name:") + .placeholder("Authorization") + .interact()?; + + let value: String = cliclack::input("Header value:") + .placeholder("Bearer token123") + .interact()?; + + headers.insert(key, value); + + if !cliclack::confirm("Add another header?").interact()? { + break; + } + } + + Ok(headers) +} + +fn configure_builtin_extension() -> anyhow::Result<()> { + let extensions = vec![ + ( + "autovisualiser", + "Auto Visualiser", + "Data visualisation and UI generation tools", + ), + ( + "computercontroller", + "Computer Controller", + "controls for webscraping, file caching, and automations", + ), + ( + "developer", + "Developer Tools", + "Code editing and shell access", + ), + ( + "memory", + "Memory", + "Tools to save and retrieve durable memories", + ), + ( + "tutorial", + "Tutorial", + "Access interactive tutorials and guides", + ), + ]; + + let mut select = cliclack::select("Which built-in extension would you like to enable?"); + for (id, name, desc) in &extensions { + select = select.item(id, name, desc); + } + let extension = select.interact()?.to_string(); + let timeout = prompt_extension_timeout()?; + + let (display_name, description) = extensions + .iter() + .find(|(id, _, _)| id == &extension) + .map(|(_, name, desc)| (name.to_string(), desc.to_string())) + .unwrap_or_else(|| (extension.clone(), extension.clone())); + + set_extension(ExtensionEntry { + enabled: true, + config: ExtensionConfig::Builtin { + name: extension.clone(), + display_name: Some(display_name), + timeout: Some(timeout), + bundled: Some(true), + description, + available_tools: Vec::new(), + }, + }); + + cliclack::outro(format!("Enabled {} extension", style(extension).green()))?; + Ok(()) +} + +fn configure_stdio_extension() -> anyhow::Result<()> { + let name = prompt_extension_name("my-extension")?; + + 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 = prompt_extension_timeout()?; + + let mut parts = command_str.split_whitespace(); + let cmd = parts.next().unwrap_or("").to_string(); + let args: Vec = parts.map(String::from).collect(); + + let description = prompt_extension_description()?; + let (envs, env_keys) = collect_env_vars()?; + + set_extension(ExtensionEntry { + enabled: true, + config: ExtensionConfig::Stdio { + name: name.clone(), + cmd, + args, + envs: Envs::new(envs), + env_keys, + description, + timeout: Some(timeout), + bundled: None, + available_tools: Vec::new(), + }, + }); + + cliclack::outro(format!("Added {} extension", style(name).green()))?; + Ok(()) +} + +fn configure_streamable_http_extension() -> anyhow::Result<()> { + let name = prompt_extension_name("my-remote-extension")?; + + let uri: String = cliclack::input("What is the Streaming HTTP endpoint URI?") + .placeholder("http://localhost:8000/messages") + .validate(|input: &String| { + if input.is_empty() { + Err("Please enter a URI") + } else if !(input.starts_with("http://") || input.starts_with("https://")) { + Err("URI should start with http:// or https://") + } else { + Ok(()) + } + }) + .interact()?; + + let timeout = prompt_extension_timeout()?; + let description = prompt_extension_description()?; + let headers = collect_headers()?; + + // Original behavior: no env var collection for Streamable HTTP + let envs = HashMap::new(); + let env_keys = Vec::new(); + + set_extension(ExtensionEntry { + enabled: true, + config: ExtensionConfig::StreamableHttp { + name: name.clone(), + uri, + envs: Envs::new(envs), + env_keys, + headers, + description, + timeout: Some(timeout), + bundled: None, + available_tools: Vec::new(), + }, + }); + + cliclack::outro(format!("Added {} extension", style(name).green()))?; + Ok(()) +} + pub fn configure_extensions_dialog() -> anyhow::Result<()> { let extension_type = cliclack::select("What type of extension would you like to add?") .item( @@ -710,297 +955,13 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { .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 extensions = vec![ - ( - "autovisualiser", - "Auto Visualiser", - "Data visualisation and UI generation tools", - ), - ( - "computercontroller", - "Computer Controller", - "controls for webscraping, file caching, and automations", - ), - ( - "developer", - "Developer Tools", - "Code editing and shell access", - ), - ( - "memory", - "Memory", - "Tools to save and retrieve durable memories", - ), - ( - "tutorial", - "Tutorial", - "Access interactive tutorials and guides", - ), - ]; - - let mut select = cliclack::select("Which built-in extension would you like to enable?"); - for (id, name, desc) in &extensions { - select = select.item(id, name, desc); - } - let extension = select.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, description) = extensions - .iter() - .find(|(id, _, _)| id == &extension) - .map(|(_, name, desc)| (name.to_string(), desc.to_string())) - .unwrap_or_else(|| (extension.clone(), extension.clone())); - - set_extension(ExtensionEntry { - enabled: true, - config: ExtensionConfig::Builtin { - name: extension.clone(), - display_name: Some(display_name), - timeout: Some(timeout), - bundled: Some(true), - description, - available_tools: Vec::new(), - }, - }); - - cliclack::outro(format!("Enabled {} extension", style(extension).green()))?; - } - "stdio" => { - let extensions = get_all_extension_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 description = 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()?; - - 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) { - 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; - } - } - } - - set_extension(ExtensionEntry { - enabled: true, - config: ExtensionConfig::Stdio { - name: name.clone(), - cmd, - args, - envs: Envs::new(envs), - env_keys, - description, - timeout: Some(timeout), - bundled: None, - available_tools: Vec::new(), - }, - }); - - cliclack::outro(format!("Added {} extension", style(name).green()))?; - } - "streamable_http" => { - let extensions = get_all_extension_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 Streamable HTTP endpoint URI?") - .placeholder("http://localhost:8000/messages") - .validate(|input: &String| { - if input.is_empty() { - Err("Please enter a URI") - } else if !(input.starts_with("http://") || input.starts_with("https://")) { - 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 description = cliclack::input("Enter a description for this extension:") - .placeholder("Description") - .validate(|input: &String| { - if input.trim().is_empty() { - Err("Please enter a valid description") - } else { - Ok(()) - } - }) - .interact()?; - - let add_headers = - cliclack::confirm("Would you like to add custom headers?").interact()?; - - let mut headers = HashMap::new(); - if add_headers { - loop { - let key: String = cliclack::input("Header name:") - .placeholder("Authorization") - .interact()?; - - let value: String = cliclack::input("Header value:") - .placeholder("Bearer token123") - .interact()?; - - headers.insert(key, value); - - if !cliclack::confirm("Add another header?").interact()? { - break; - } - } - } - - let add_env = false; // No env prompt for Streamable HTTP - - 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; - } - } - } - - set_extension(ExtensionEntry { - enabled: true, - config: ExtensionConfig::StreamableHttp { - name: name.clone(), - uri, - envs: Envs::new(envs), - env_keys, - headers, - description, - timeout: Some(timeout), - bundled: None, - available_tools: Vec::new(), - }, - }); - - cliclack::outro(format!("Added {} extension", style(name).green()))?; - } + "built-in" => configure_builtin_extension()?, + "stdio" => configure_stdio_extension()?, + "streamable_http" => configure_streamable_http_extension()?, _ => unreachable!(), }; print_config_file_saved()?; - Ok(()) }