use anyhow::Result; use clap::{Args, Parser, Subcommand}; use goose::config::{Config, ExtensionConfig}; use crate::commands::bench::agent_generator; use crate::commands::configure::handle_configure; use crate::commands::info::handle_info; use crate::commands::mcp::run_server; use crate::commands::recipe::{handle_deeplink, handle_validate}; use crate::commands::session::handle_session_list; use crate::logging::setup_logging; use crate::recipe::load_recipe; use crate::session; use crate::session::{build_session, SessionBuilderConfig}; use goose_bench::bench_config::BenchRunConfig; use goose_bench::runners::bench_runner::BenchRunner; use goose_bench::runners::eval_runner::EvalRunner; use goose_bench::runners::model_runner::ModelRunner; use std::io::Read; use std::path::PathBuf; #[derive(Parser)] #[command(author, version, display_name = "", about, long_about = None)] struct Cli { #[command(subcommand)] command: Option, } #[derive(Args)] #[group(required = false, multiple = false)] struct Identifier { #[arg( short, long, value_name = "NAME", help = "Name for the chat session (e.g., 'project-x')", long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists." )] name: Option, #[arg( short, long, value_name = "PATH", help = "Path for the chat session (e.g., './playground.jsonl')", long_help = "Specify a path for your chat session. When used with --resume, will resume this specific session if it exists." )] path: Option, } fn extract_identifier(identifier: Identifier) -> session::Identifier { if let Some(name) = identifier.name { session::Identifier::Name(name) } else if let Some(path) = identifier.path { session::Identifier::Path(path) } else { unreachable!() } } #[derive(Subcommand)] enum SessionCommand { #[command(about = "List all available sessions")] List { #[arg(short, long, help = "List all available sessions")] verbose: bool, #[arg( short, long, help = "Output format (text, json)", default_value = "text" )] format: String, }, } #[derive(Subcommand)] pub enum BenchCommand { #[command(name = "init-config", about = "Create a new starter-config")] InitConfig { #[arg(short, long, help = "filename with extension for generated config")] name: String, }, #[command(about = "Run all benchmarks from a config")] Run { #[arg( short, long, help = "A config file generated by the config-init command" )] config: PathBuf, }, #[command(about = "List all available selectors")] Selectors { #[arg( short, long, help = "A config file generated by the config-init command" )] config: Option, }, #[command(name = "eval-model", about = "Run an eval of model")] EvalModel { #[arg(short, long, help = "A serialized config file for the model only.")] config: String, }, #[command(name = "exec-eval", about = "run a single eval")] ExecEval { #[arg(short, long, help = "A serialized config file for the eval only.")] config: String, }, } #[derive(Subcommand)] enum RecipeCommand { /// Validate a recipe file #[command(about = "Validate a recipe file")] Validate { /// Path to the recipe file to validate #[arg(help = "Path to the recipe file to validate")] file: String, }, /// Generate a deeplink for a recipe file #[command(about = "Generate a deeplink for a recipe file")] Deeplink { /// Path to the recipe file #[arg(help = "Path to the recipe file")] file: String, }, } #[derive(Subcommand)] enum Command { /// Configure Goose settings #[command(about = "Configure Goose settings")] Configure {}, /// Display Goose configuration information #[command(about = "Display Goose information")] Info { /// Show verbose information including current configuration #[arg(short, long, help = "Show verbose information including config.yaml")] verbose: bool, }, /// Manage system prompts and behaviors #[command(about = "Run one of the mcp servers bundled with goose")] Mcp { name: String }, /// Start or resume interactive chat sessions #[command( about = "Start or resume interactive chat sessions", visible_alias = "s" )] Session { #[command(subcommand)] command: Option, /// Identifier for the chat session #[command(flatten)] identifier: Option, /// Resume a previous session #[arg( short, long, help = "Resume a previous session (last used or specified by --name)", long_help = "Continue from a previous chat session. If --name or --path is provided, resumes that specific session. Otherwise resumes the last used session." )] resume: bool, /// Enable debug output mode #[arg( long, help = "Enable debug output mode with full content and no truncation", long_help = "When enabled, shows complete tool responses without truncation and full paths." )] debug: bool, /// Add stdio extensions with environment variables and commands #[arg( long = "with-extension", value_name = "COMMAND", help = "Add stdio extensions (can be specified multiple times)", long_help = "Add stdio extensions from full commands with environment variables. Can be specified multiple times. Format: 'ENV1=val1 ENV2=val2 command args...'", action = clap::ArgAction::Append )] extensions: Vec, /// Add remote extensions with a URL #[arg( long = "with-remote-extension", value_name = "URL", help = "Add remote extensions (can be specified multiple times)", long_help = "Add remote extensions from a URL. Can be specified multiple times. Format: 'url...'", action = clap::ArgAction::Append )] remote_extensions: Vec, /// Add builtin extensions by name #[arg( long = "with-builtin", value_name = "NAME", help = "Add builtin extensions by name (e.g., 'developer' or multiple: 'developer,github')", long_help = "Add one or more builtin extensions that are bundled with goose by specifying their names, comma-separated", value_delimiter = ',' )] builtins: Vec, }, /// Execute commands from an instruction file #[command(about = "Execute commands from an instruction file or stdin")] Run { /// Path to instruction file containing commands #[arg( short, long, value_name = "FILE", help = "Path to instruction file containing commands. Use - for stdin.", conflicts_with = "input_text", conflicts_with = "recipe" )] instructions: Option, /// Input text containing commands #[arg( short = 't', long = "text", value_name = "TEXT", help = "Input text to provide to Goose directly", long_help = "Input text containing commands for Goose. Use this in lieu of the instructions argument.", conflicts_with = "instructions", conflicts_with = "recipe" )] input_text: Option, /// Path to recipe.yaml file #[arg( short = None, long = "recipe", value_name = "FILE", help = "Path to recipe.yaml file", long_help = "Path to a recipe.yaml file that defines a custom agent configuration", conflicts_with = "instructions", conflicts_with = "input_text" )] recipe: Option, /// Continue in interactive mode after processing input #[arg( short = 's', long = "interactive", help = "Continue in interactive mode after processing initial input" )] interactive: bool, /// Identifier for this run session #[command(flatten)] identifier: Option, /// Resume a previous run #[arg( short, long, action = clap::ArgAction::SetTrue, help = "Resume from a previous run", long_help = "Continue from a previous run, maintaining the execution state and context." )] resume: bool, /// Enable debug output mode #[arg( long, help = "Enable debug output mode with full content and no truncation", long_help = "When enabled, shows complete tool responses without truncation and full paths." )] debug: bool, /// Add stdio extensions with environment variables and commands #[arg( long = "with-extension", value_name = "COMMAND", help = "Add stdio extensions (can be specified multiple times)", long_help = "Add stdio extensions from full commands with environment variables. Can be specified multiple times. Format: 'ENV1=val1 ENV2=val2 command args...'", action = clap::ArgAction::Append )] extensions: Vec, /// Add remote extensions #[arg( long = "with-remote-extension", value_name = "URL", help = "Add remote extensions (can be specified multiple times)", long_help = "Add remote extensions. Can be specified multiple times. Format: 'url...'", action = clap::ArgAction::Append )] remote_extensions: Vec, /// Add builtin extensions by name #[arg( long = "with-builtin", value_name = "NAME", help = "Add builtin extensions by name (e.g., 'developer' or multiple: 'developer,github')", long_help = "Add one or more builtin extensions that are bundled with goose by specifying their names, comma-separated", value_delimiter = ',' )] builtins: Vec, }, /// Recipe utilities for validation and deeplinking #[command(about = "Recipe utilities for validation and deeplinking")] Recipe { #[command(subcommand)] command: RecipeCommand, }, /// Update the Goose CLI version #[command(about = "Update the goose CLI version")] Update { /// Update to canary version #[arg( short, long, help = "Update to canary version", long_help = "Update to the latest canary version of the goose CLI, otherwise updates to the latest stable version." )] canary: bool, /// Enforce to re-configure Goose during update #[arg(short, long, help = "Enforce to re-configure goose during update")] reconfigure: bool, }, Bench { #[command(subcommand)] cmd: BenchCommand, }, } #[derive(clap::ValueEnum, Clone, Debug)] enum CliProviderVariant { OpenAi, Databricks, Ollama, } struct InputConfig { contents: Option, extensions_override: Option>, additional_system_prompt: Option, } pub async fn cli() -> Result<()> { let cli = Cli::parse(); match cli.command { Some(Command::Configure {}) => { let _ = handle_configure().await; return Ok(()); } Some(Command::Info { verbose }) => { handle_info(verbose)?; return Ok(()); } Some(Command::Mcp { name }) => { let _ = run_server(&name).await; } Some(Command::Session { command, identifier, resume, debug, extensions, remote_extensions, builtins, }) => { return match command { Some(SessionCommand::List { verbose, format }) => { handle_session_list(verbose, format)?; Ok(()) } None => { // Run session command by default let mut session = build_session(SessionBuilderConfig { identifier: identifier.map(extract_identifier), resume, extensions, remote_extensions, builtins, extensions_override: None, additional_system_prompt: None, debug, }) .await; setup_logging( session.session_file().file_stem().and_then(|s| s.to_str()), None, )?; let _ = session.interactive(None).await; Ok(()) } }; } Some(Command::Run { instructions, input_text, recipe, interactive, identifier, resume, debug, extensions, remote_extensions, builtins, }) => { let input_config = match (instructions, input_text, recipe) { (Some(file), _, _) if file == "-" => { let mut input = String::new(); std::io::stdin() .read_to_string(&mut input) .expect("Failed to read from stdin"); InputConfig { contents: Some(input), extensions_override: None, additional_system_prompt: None, } } (Some(file), _, _) => { let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| { eprintln!( "Instruction file not found — did you mean to use goose run --text?\n{}", err ); std::process::exit(1); }); InputConfig { contents: Some(contents), extensions_override: None, additional_system_prompt: None, } } (_, Some(text), _) => InputConfig { contents: Some(text), extensions_override: None, additional_system_prompt: None, }, (_, _, Some(file)) => { let recipe = load_recipe(&file, true).unwrap_or_else(|err| { eprintln!("{}: {}", console::style("Error").red().bold(), err); std::process::exit(1); }); InputConfig { contents: recipe.prompt, extensions_override: recipe.extensions, additional_system_prompt: Some(recipe.instructions), } } (None, None, None) => { eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin."); std::process::exit(1); } }; let mut session = build_session(SessionBuilderConfig { identifier: identifier.map(extract_identifier), resume, extensions, remote_extensions, builtins, extensions_override: input_config.extensions_override, additional_system_prompt: input_config.additional_system_prompt, debug, }) .await; setup_logging( session.session_file().file_stem().and_then(|s| s.to_str()), None, )?; if interactive { let _ = session.interactive(input_config.contents).await; } else if let Some(contents) = input_config.contents { let _ = session.headless(contents).await; } else { eprintln!("Error: no text provided for prompt in headless mode"); std::process::exit(1); } return Ok(()); } Some(Command::Update { canary, reconfigure, }) => { crate::commands::update::update(canary, reconfigure)?; return Ok(()); } Some(Command::Bench { cmd }) => { match cmd { BenchCommand::Selectors { config } => BenchRunner::list_selectors(config)?, BenchCommand::InitConfig { name } => BenchRunConfig::default().save(name), BenchCommand::Run { config } => BenchRunner::new(config)?.run()?, BenchCommand::EvalModel { config } => ModelRunner::from(config)?.run()?, BenchCommand::ExecEval { config } => { EvalRunner::from(config)?.run(agent_generator).await? } } return Ok(()); } Some(Command::Recipe { command }) => { match command { RecipeCommand::Validate { file } => { handle_validate(file)?; } RecipeCommand::Deeplink { file } => { handle_deeplink(file)?; } } return Ok(()); } None => { return if !Config::global().exists() { let _ = handle_configure().await; Ok(()) } else { // Run session command by default let mut session = build_session(SessionBuilderConfig::default()).await; setup_logging( session.session_file().file_stem().and_then(|s| s.to_str()), None, )?; if let Err(e) = session.interactive(None).await { eprintln!("Session ended with error: {}", e); } Ok(()) }; } } Ok(()) }