mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 15:14:21 +01:00
feat: V1.0 (#734)
Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Wendy Tang <wendytang@squareup.com> Co-authored-by: Jarrod Sibbison <72240382+jsibbison-square@users.noreply.github.com> Co-authored-by: Alex Hancock <alex.hancock@example.com> Co-authored-by: Alex Hancock <alexhancock@block.xyz> Co-authored-by: Lifei Zhou <lifei@squareup.com> Co-authored-by: Wes <141185334+wesrblock@users.noreply.github.com> Co-authored-by: Max Novich <maksymstepanenko1990@gmail.com> Co-authored-by: Zaki Ali <zaki@squareup.com> Co-authored-by: Salman Mohammed <smohammed@squareup.com> Co-authored-by: Kalvin C <kalvinnchau@users.noreply.github.com> Co-authored-by: Alec Thomas <alec@swapoff.org> Co-authored-by: lily-de <119957291+lily-de@users.noreply.github.com> Co-authored-by: kalvinnchau <kalvin@block.xyz> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Rizel Scarlett <rizel@squareup.com> Co-authored-by: bwrage <bwrage@squareup.com> Co-authored-by: Kalvin Chau <kalvin@squareup.com> Co-authored-by: Alice Hau <110418948+ahau-square@users.noreply.github.com> Co-authored-by: Alistair Gray <ajgray@stripe.com> Co-authored-by: Nahiyan Khan <nahiyan.khan@gmail.com> Co-authored-by: Alex Hancock <alexhancock@squareup.com> Co-authored-by: Nahiyan Khan <nahiyan@squareup.com> Co-authored-by: marcelle <1852848+laanak08@users.noreply.github.com> Co-authored-by: Yingjie He <yingjiehe@block.xyz> Co-authored-by: Yingjie He <yingjiehe@squareup.com> Co-authored-by: Lily Delalande <ldelalande@block.xyz> Co-authored-by: Adewale Abati <acekyd01@gmail.com> Co-authored-by: Ebony Louis <ebony774@gmail.com> Co-authored-by: Angie Jones <jones.angie@gmail.com> Co-authored-by: Ebony Louis <55366651+EbonyLouis@users.noreply.github.com>
This commit is contained in:
486
crates/goose-cli/src/commands/configure.rs
Normal file
486
crates/goose-cli/src/commands/configure.rs
Normal file
@@ -0,0 +1,486 @@
|
||||
use cliclack::spinner;
|
||||
use console::style;
|
||||
use goose::agents::{extension::Envs, ExtensionConfig};
|
||||
use goose::config::{Config, ExtensionEntry, ExtensionManager};
|
||||
use goose::message::Message;
|
||||
use goose::providers::{create, providers};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
||||
pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
|
||||
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())?;
|
||||
if configure_provider_dialog().await? {
|
||||
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
|
||||
ExtensionManager::set(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
name: "developer".to_string(),
|
||||
},
|
||||
})?;
|
||||
} else {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
"toggle",
|
||||
"Toggle Extensions",
|
||||
"Enable or disable connected extensions",
|
||||
)
|
||||
.item("add", "Add Extension", "Connect to a new extension")
|
||||
.interact()?;
|
||||
|
||||
match action {
|
||||
"toggle" => toggle_extensions_dialog(),
|
||||
"add" => configure_extensions_dialog(),
|
||||
"providers" => configure_provider_dialog().await.and(Ok(())),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog for configuring the AI provider and model
|
||||
pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
|
||||
// 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<String> = config.get("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 config file?")
|
||||
.initial_value(true)
|
||||
.interact()?
|
||||
{
|
||||
if key.secret {
|
||||
config.set_secret(&key.name, Value::String(env_value))?;
|
||||
} else {
|
||||
config.set(&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<String, _> = if key.secret {
|
||||
config.get_secret(&key.name)
|
||||
} else {
|
||||
config.get(&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 {
|
||||
cliclack::input(format!("Enter new value for {}", key.name))
|
||||
.interact()?
|
||||
};
|
||||
|
||||
if key.secret {
|
||||
config.set_secret(&key.name, Value::String(new_value))?;
|
||||
} else {
|
||||
config.set(&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 {
|
||||
cliclack::input(format!(
|
||||
"Provider {} requires {}, please enter a value",
|
||||
provider_meta.display_name, key.name
|
||||
))
|
||||
.interact()?
|
||||
};
|
||||
|
||||
if key.secret {
|
||||
config.set_secret(&key.name, Value::String(value))?;
|
||||
} else {
|
||||
config.set(&key.name, Value::String(value))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select model, defaulting to the provider's recommended model
|
||||
let default_model = config
|
||||
.get("GOOSE_MODEL")
|
||||
.unwrap_or(provider_meta.default_model.clone());
|
||||
let model: String = cliclack::input("Enter a model from that provider:")
|
||||
.default_input(&default_model)
|
||||
.interact()?;
|
||||
|
||||
// Update config with new values
|
||||
config.set("GOOSE_PROVIDER", Value::String(provider_name.to_string()))?;
|
||||
config.set("GOOSE_MODEL", Value::String(model.clone()))?;
|
||||
|
||||
// Test the configuration
|
||||
let spin = spinner();
|
||||
spin.start("Checking your configuration...");
|
||||
|
||||
let model_config = goose::model::ModelConfig::new(model.clone());
|
||||
let provider = create(provider_name, model_config)?;
|
||||
|
||||
let message = Message::user().with_text(
|
||||
"Please give a nice welcome message (one sentence) and let them know they are all set to use this agent"
|
||||
);
|
||||
|
||||
let result = provider
|
||||
.complete(
|
||||
"You are an AI agent called Goose. You use tools of connected extensions to solve problems.",
|
||||
&[message],
|
||||
&[]
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok((message, _usage)) => {
|
||||
if let Some(content) = message.content.first() {
|
||||
if let Some(text) = content.as_text() {
|
||||
spin.stop(text);
|
||||
} else {
|
||||
spin.stop("No response text available");
|
||||
}
|
||||
} else {
|
||||
spin.stop("No response content available");
|
||||
}
|
||||
|
||||
cliclack::outro("Configuration saved successfully")?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{:?}", e);
|
||||
spin.stop("We could not connect!");
|
||||
let _ = cliclack::outro("The provider configuration was invalid");
|
||||
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<dyn Error>> {
|
||||
let extensions = ExtensionManager::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 extension_status: Vec<(String, bool)> = extensions
|
||||
.iter()
|
||||
.map(|entry| (entry.config.name().to_string(), entry.enabled))
|
||||
.collect();
|
||||
|
||||
// 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(), ""))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.initial_values(enabled_extensions)
|
||||
.interact()?;
|
||||
|
||||
// Update enabled status for each extension
|
||||
for name in extension_status.iter().map(|(name, _)| name) {
|
||||
ExtensionManager::set_enabled(name, selected.iter().any(|s| s.as_str() == name))?;
|
||||
}
|
||||
|
||||
cliclack::outro("Extension settings updated successfully")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
||||
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(
|
||||
"developer",
|
||||
"Developer Tools",
|
||||
"Code editing and shell access",
|
||||
)
|
||||
.item(
|
||||
"nondeveloper",
|
||||
"Non Developer",
|
||||
"AI driven scripting for non developers",
|
||||
)
|
||||
.item(
|
||||
"google_drive",
|
||||
"Google Drive",
|
||||
"Search and read content from google drive - additional config required",
|
||||
)
|
||||
.item(
|
||||
"memory",
|
||||
"Memory",
|
||||
"Tools to save and retrieve durable memories",
|
||||
)
|
||||
.item("jetbrains", "JetBrains", "Connect to jetbrains IDEs")
|
||||
.interact()?
|
||||
.to_string();
|
||||
|
||||
ExtensionManager::set(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
name: extension.clone(),
|
||||
},
|
||||
})?;
|
||||
|
||||
cliclack::outro(format!("Enabled {} extension", style(extension).green()))?;
|
||||
}
|
||||
"stdio" => {
|
||||
let extensions = ExtensionManager::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()?;
|
||||
|
||||
// Split the command string into command and args
|
||||
let mut parts = command_str.split_whitespace();
|
||||
let cmd = parts.next().unwrap_or("").to_string();
|
||||
let args: Vec<String> = parts.map(String::from).collect();
|
||||
|
||||
let add_env =
|
||||
cliclack::confirm("Would you like to add environment variables?").interact()?;
|
||||
|
||||
let mut envs = HashMap::new();
|
||||
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()?;
|
||||
|
||||
envs.insert(key, value);
|
||||
|
||||
if !cliclack::confirm("Add another environment variable?").interact()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionManager::set(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Stdio {
|
||||
name: name.clone(),
|
||||
cmd,
|
||||
args,
|
||||
envs: Envs::new(envs),
|
||||
},
|
||||
})?;
|
||||
|
||||
cliclack::outro(format!("Added {} extension", style(name).green()))?;
|
||||
}
|
||||
"sse" => {
|
||||
let extensions = ExtensionManager::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 add_env =
|
||||
cliclack::confirm("Would you like to add environment variables?").interact()?;
|
||||
|
||||
let mut envs = HashMap::new();
|
||||
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()?;
|
||||
|
||||
envs.insert(key, value);
|
||||
|
||||
if !cliclack::confirm("Add another environment variable?").interact()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionManager::set(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Sse {
|
||||
name: name.clone(),
|
||||
uri,
|
||||
envs: Envs::new(envs),
|
||||
},
|
||||
})?;
|
||||
|
||||
cliclack::outro(format!("Added {} extension", style(name).green()))?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user