use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as ClapShell}; use goose::config::{Config, ExtensionConfig}; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{ AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, }; use crate::commands::acp::run_acp_agent; use crate::commands::bench::agent_generator; use crate::commands::configure::handle_configure; use crate::commands::info::handle_info; use crate::commands::project::{handle_project_default, handle_projects_interactive}; use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate}; use crate::commands::term::{ handle_term_info, handle_term_init, handle_term_log, handle_term_run, Shell, }; use crate::commands::schedule::{ handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove, handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop, handle_schedule_sessions, }; use crate::commands::session::{handle_session_list, handle_session_remove}; use crate::recipes::extract_from_cli::extract_recipe_info_from_cli; use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml}; use crate::session::{build_session, SessionBuilderConfig, SessionSettings}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose_bench::bench_config::BenchRunConfig; use goose_bench::runners::bench_runner::BenchRunner; use goose_bench::runners::eval_runner::EvalRunner; use goose_bench::runners::metric_aggregator::MetricAggregator; use goose_bench::runners::model_runner::ModelRunner; use std::io::Read; use std::path::PathBuf; use tracing::warn; #[derive(Parser)] #[command(author, version, display_name = "", about, long_about = None)] struct Cli { #[command(subcommand)] command: Option, } #[derive(Args, Debug, Clone)] #[group(required = false, multiple = false)] pub struct Identifier { #[arg( short = 'n', 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." )] pub name: Option, #[arg( long = "session-id", alias = "id", value_name = "SESSION_ID", help = "Session ID (e.g., '20250921_143022')", long_help = "Specify a session ID directly. When used with --resume, will resume this specific session if it exists." )] pub session_id: Option, #[arg( long, value_name = "PATH", help = "Legacy: Path for the chat session", long_help = "Legacy parameter for backward compatibility. Extracts session ID from the file path (e.g., '/path/to/20250325_200615. jsonl' -> '20250325_200615')." )] pub path: Option, } /// Session behavior options shared between Session and Run commands #[derive(Args, Debug, Clone, Default)] pub struct SessionOptions { #[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." )] pub debug: bool, #[arg( long = "max-tool-repetitions", value_name = "NUMBER", help = "Maximum number of consecutive identical tool calls allowed", long_help = "Set a limit on how many times the same tool can be called consecutively with identical parameters. Helps prevent infinite loops." )] pub max_tool_repetitions: Option, #[arg( long = "max-turns", value_name = "NUMBER", help = "Maximum number of turns allowed without user input (default: 1000)", long_help = "Set a limit on how many turns (iterations) the agent can take without asking for user input to continue." )] pub max_turns: Option, } /// Extension configuration options shared between Session and Run commands #[derive(Args, Debug, Clone, Default)] pub struct ExtensionOptions { #[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 )] pub extensions: Vec, #[arg( long = "with-streamable-http-extension", value_name = "URL", help = "Add streamable HTTP extensions (can be specified multiple times)", long_help = "Add streamable HTTP extensions from a URL. Can be specified multiple times. Format: 'url...'", action = clap::ArgAction::Append )] pub streamable_http_extensions: Vec, #[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 = ',' )] pub builtins: Vec, } /// Input source and recipe options for the run command #[derive(Args, Debug, Clone, Default)] pub struct InputOptions { /// 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" )] pub 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" )] pub input_text: Option, /// Recipe name or full path to the recipe file #[arg( short = None, long = "recipe", value_name = "RECIPE_NAME or FULL_PATH_TO_RECIPE_FILE", help = "Recipe name to get recipe file or the full path of the recipe file (use --explain to see recipe details)", long_help = "Recipe name to get recipe file or the full path of the recipe file that defines a custom agent configuration. Use --explain to see the recipe's title, description, and parameters.", conflicts_with = "instructions", conflicts_with = "input_text" )] pub recipe: Option, /// Additional system prompt to customize agent behavior #[arg( long = "system", value_name = "TEXT", help = "Additional system prompt to customize agent behavior", long_help = "Provide additional system instructions to customize the agent's behavior", conflicts_with = "recipe" )] pub system: Option, #[arg( long, value_name = "KEY=VALUE", help = "Dynamic parameters (e.g., --params username=alice --params channel_name=goose-channel)", long_help = "Key-value parameters to pass to the recipe file. Can be specified multiple times.", action = clap::ArgAction::Append, value_parser = parse_key_val, )] pub params: Vec<(String, String)>, /// Additional sub-recipe file paths #[arg( long = "sub-recipe", value_name = "RECIPE", help = "Sub-recipe name or file path (can be specified multiple times)", long_help = "Specify sub-recipes to include alongside the main recipe. Can be:\n - Recipe names from GitHub (if GOOSE_RECIPE_GITHUB_REPO is configured)\n - Local file paths to YAML files\nCan be specified multiple times to include multiple sub-recipes.", action = clap::ArgAction::Append )] pub additional_sub_recipes: Vec, /// Show the recipe title, description, and parameters #[arg( long = "explain", help = "Show the recipe title, description, and parameters" )] pub explain: bool, /// Print the rendered recipe instead of running it #[arg( long = "render-recipe", help = "Print the rendered recipe instead of running it." )] pub render_recipe: bool, } /// Output configuration options for the run command #[derive(Args, Debug, Clone)] pub struct OutputOptions { /// Quiet mode - suppress non-response output #[arg( short = 'q', long = "quiet", help = "Quiet mode. Suppress non-response output, printing only the model response to stdout" )] pub quiet: bool, /// Output format (text, json, stream-json) #[arg( long = "output-format", value_name = "FORMAT", help = "Output format (text, json, stream-json)", default_value = "text", value_parser = clap::builder::PossibleValuesParser::new(["text", "json", "stream-json"]) )] pub output_format: String, } impl Default for OutputOptions { fn default() -> Self { Self { quiet: false, output_format: "text".to_string(), } } } /// Model/provider override options for the run command #[derive(Args, Debug, Clone, Default)] pub struct ModelOptions { /// Provider to use for this run (overrides environment variable) #[arg( long = "provider", value_name = "PROVIDER", help = "Specify the LLM provider to use (e.g., 'openai', 'anthropic')", long_help = "Override the GOOSE_PROVIDER environment variable for this run. Available providers include openai, anthropic, ollama, databricks, gemini-cli, claude-code, and others." )] pub provider: Option, /// Model to use for this run (overrides environment variable) #[arg( long = "model", value_name = "MODEL", help = "Specify the model to use (e.g., 'gpt-4o', 'claude-sonnet-4-20250514')", long_help = "Override the GOOSE_MODEL environment variable for this run. The model must be supported by the specified provider." )] pub model: Option, } /// Run execution behavior options #[derive(Args, Debug, Clone, Default)] pub struct RunBehavior { /// Continue in interactive mode after processing input #[arg( short = 's', long = "interactive", help = "Continue in interactive mode after processing initial input" )] pub interactive: bool, /// Run without storing a session file #[arg( long = "no-session", help = "Run without storing a session file", long_help = "Execute commands without creating or using a session file. Useful for automated runs.", conflicts_with_all = ["resume", "name", "path"] )] pub no_session: bool, /// 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." )] pub resume: bool, /// Scheduled job ID (used internally for scheduled executions) #[arg( long = "scheduled-job-id", value_name = "ID", help = "ID of the scheduled job that triggered this execution (internal use)", long_help = "Internal parameter used when this run command is executed by a scheduled job. This associates the session with the schedule for tracking purposes.", hide = true )] pub scheduled_job_id: Option, } async fn get_or_create_session_id( identifier: Option, resume: bool, no_session: bool, ) -> Result> { if no_session { return Ok(None); } let Some(id) = identifier else { return if resume { let sessions = SessionManager::list_sessions().await?; let session_id = sessions .first() .map(|s| s.id.clone()) .ok_or_else(|| anyhow::anyhow!("No session found to resume"))?; Ok(Some(session_id)) } else { let session = SessionManager::create_session( std::env::current_dir()?, "CLI Session".to_string(), SessionType::User, ) .await?; Ok(Some(session.id)) }; }; if let Some(session_id) = id.session_id { Ok(Some(session_id)) } else if let Some(name) = id.name { if resume { let sessions = SessionManager::list_sessions().await?; let session_id = sessions .into_iter() .find(|s| s.name == name || s.id == name) .map(|s| s.id) .ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name))?; Ok(Some(session_id)) } else { let session = SessionManager::create_session( std::env::current_dir()?, name.clone(), SessionType::User, ) .await?; SessionManager::update_session(&session.id) .user_provided_name(name) .apply() .await?; Ok(Some(session.id)) } } else if let Some(path) = id.path { let session_id = path .file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) .ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path))?; Ok(Some(session_id)) } else { let session = SessionManager::create_session( std::env::current_dir()?, "CLI Session".to_string(), SessionType::User, ) .await?; Ok(Some(session.id)) } } async fn lookup_session_id(identifier: Identifier) -> Result { if let Some(session_id) = identifier.session_id { Ok(session_id) } else if let Some(name) = identifier.name { let sessions = SessionManager::list_sessions().await?; sessions .into_iter() .find(|s| s.name == name || s.id == name) .map(|s| s.id) .ok_or_else(|| anyhow::anyhow!("No session found with name '{}'", name)) } else if let Some(path) = identifier.path { path.file_stem() .and_then(|s| s.to_str()) .map(|s| s.to_string()) .ok_or_else(|| anyhow::anyhow!("Could not extract session ID from path: {:?}", path)) } else { Err(anyhow::anyhow!("No identifier provided")) } } fn parse_key_val(s: &str) -> Result<(String, String), String> { match s.split_once('=') { Some((key, value)) => Ok((key.to_string(), value.to_string())), None => Err(format!("invalid KEY=VALUE: {}", s)), } } #[derive(Subcommand)] enum SessionCommand { #[command(about = "List all available sessions")] List { #[arg( short, long, help = "Output format (text, json)", default_value = "text" )] format: String, #[arg( long = "ascending", help = "Sort by date in ascending order (oldest first)", long_help = "Sort sessions by date in ascending order (oldest first). Default is descending order (newest first)." )] ascending: bool, #[arg( short = 'w', short_alias = 'p', long = "working_dir", help = "Filter sessions by working directory" )] working_dir: Option, #[arg(short = 'l', long = "limit", help = "Limit the number of results")] limit: Option, }, #[command(about = "Remove sessions. Runs interactively if no ID, name, or regex is provided.")] Remove { #[command(flatten)] identifier: Option, #[arg( short = 'r', long, help = "Regex for removing matched sessions (optional)" )] regex: Option, }, #[command(about = "Export a session")] Export { #[command(flatten)] identifier: Option, #[arg( short, long, help = "Output file path (default: stdout)", long_help = "Path to save the exported Markdown. If not provided, output will be sent to stdout" )] output: Option, #[arg( long = "format", value_name = "FORMAT", help = "Output format (markdown, json, yaml)", default_value = "markdown" )] format: String, }, #[command(name = "diagnostics")] Diagnostics { /// Session identifier for generating diagnostics #[command(flatten)] identifier: Option, /// Output path for the diagnostics zip file (optional, defaults to current directory) #[arg(short = 'o', long)] output: Option, }, } #[derive(Subcommand, Debug)] enum SchedulerCommand { #[command(about = "Add a new scheduled job")] Add { #[arg( long = "schedule-id", alias = "id", help = "Unique ID for the recurring scheduled job" )] schedule_id: String, #[arg( long, help = "Cron expression for the schedule", long_help = "Cron expression for when to run the job. Examples:\n '0 * * * *' - Every hour at minute 0\n '0 */2 * * *' - Every 2 hours\n '@hourly' - Every hour (shorthand)\n '0 9 * * *' - Every day at 9:00 AM\n '0 9 * * 1' - Every Monday at 9:00 AM\n '0 0 1 * *' - First day of every month at midnight" )] cron: String, #[arg( long, help = "Recipe source (path to file, or base64 encoded recipe string)" )] recipe_source: String, }, #[command(about = "List all scheduled jobs")] List {}, #[command(about = "Remove a scheduled job by ID")] Remove { #[arg( long = "schedule-id", alias = "id", help = "ID of the scheduled job to remove (removes the recurring schedule)" )] schedule_id: String, }, /// List sessions created by a specific schedule #[command(about = "List sessions created by a specific schedule")] Sessions { /// ID of the schedule #[arg(long = "schedule-id", alias = "id", help = "ID of the schedule")] schedule_id: String, #[arg(short = 'l', long, help = "Maximum number of sessions to return")] limit: Option, }, #[command(about = "Run a scheduled job immediately")] RunNow { /// ID of the schedule to run #[arg(long = "schedule-id", alias = "id", help = "ID of the schedule to run")] schedule_id: String, }, /// Check status of scheduler services (deprecated - no external services needed) #[command(about = "[Deprecated] Check status of scheduler services")] ServicesStatus {}, /// Stop scheduler services (deprecated - no external services needed) #[command(about = "[Deprecated] Stop scheduler services")] ServicesStop {}, /// Show cron expression examples and help #[command(about = "Show cron expression examples and help")] CronHelp {}, } #[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, }, #[command( name = "generate-leaderboard", about = "Generate a leaderboard CSV from benchmark results" )] GenerateLeaderboard { #[arg( short, long, help = "Path to the benchmark directory containing model evaluation results" )] benchmark_dir: PathBuf, }, } #[derive(Subcommand)] enum RecipeCommand { /// Validate a recipe file #[command(about = "Validate a recipe")] Validate { /// Recipe name to get recipe file to validate #[arg(help = "recipe name to get recipe file or full path to the recipe file to validate")] recipe_name: String, }, /// Generate a deeplink for a recipe file #[command(about = "Generate a deeplink for a recipe")] Deeplink { /// Recipe name to get recipe file to generate deeplink #[arg( help = "recipe name to get recipe file or full path to the recipe file to generate deeplink" )] recipe_name: String, /// Recipe parameters in key=value format (can be specified multiple times) #[arg( short = 'p', long = "param", value_name = "KEY=VALUE", help = "Recipe parameter in key=value format (can be specified multiple times)" )] params: Vec, }, /// Open a recipe in Goose Desktop #[command(about = "Open a recipe in Goose Desktop")] Open { /// Recipe name to get recipe file to open #[arg(help = "recipe name or full path to the recipe file")] recipe_name: String, /// Recipe parameters in key=value format (can be specified multiple times) #[arg( short = 'p', long = "param", value_name = "KEY=VALUE", help = "Recipe parameter in key=value format (can be specified multiple times)" )] params: Vec, }, /// List available recipes #[command(about = "List available recipes")] List { /// Output format (text, json) #[arg( long = "format", value_name = "FORMAT", help = "Output format (text, json)", default_value = "text" )] format: String, /// Show verbose information including recipe descriptions #[arg( short, long, help = "Show verbose information including recipe descriptions" )] verbose: bool, }, } #[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 { #[arg(value_parser = clap::value_parser!(McpCommand))] server: McpCommand, }, /// Run goose as an ACP (Agent Client Protocol) agent #[command(about = "Run goose as an ACP agent server on stdio")] Acp { /// 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, }, /// Start or resume interactive chat sessions #[command( about = "Start or resume interactive chat sessions", visible_alias = "s" )] Session { #[command(subcommand)] command: Option, #[command(flatten)] identifier: Option, /// Resume a previous session #[arg( short, long, help = "Resume a previous session (last used or specified by --name/--session-id)", long_help = "Continue from a previous session. If --name or --session-id is provided, resumes that specific session. Otherwise resumes the most recently used session." )] resume: bool, /// Show message history when resuming #[arg( long, help = "Show previous messages when resuming a session", requires = "resume" )] history: bool, #[command(flatten)] session_opts: SessionOptions, #[command(flatten)] extension_opts: ExtensionOptions, }, /// Open the last project directory #[command(about = "Open the last project directory", visible_alias = "p")] Project {}, /// List recent project directories #[command(about = "List recent project directories", visible_alias = "ps")] Projects, /// Execute commands from an instruction file #[command(about = "Execute commands from an instruction file or stdin")] Run { #[command(flatten)] input_opts: InputOptions, #[command(flatten)] identifier: Option, #[command(flatten)] run_behavior: RunBehavior, #[command(flatten)] session_opts: SessionOptions, #[command(flatten)] extension_opts: ExtensionOptions, #[command(flatten)] output_opts: OutputOptions, #[command(flatten)] model_opts: ModelOptions, }, /// Recipe utilities for validation and deeplinking #[command(about = "Recipe utilities for validation and deeplinking")] Recipe { #[command(subcommand)] command: RecipeCommand, }, /// Manage scheduled jobs #[command(about = "Manage scheduled jobs", visible_alias = "sched")] Schedule { #[command(subcommand)] command: SchedulerCommand, }, /// 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, }, /// Evaluate system configuration across a range of practical tasks #[command(about = "Evaluate system configuration across a range of practical tasks")] Bench { #[command(subcommand)] cmd: BenchCommand, }, /// Start a web server with a chat interface #[command(about = "Experimental: Start a web server with a chat interface")] Web { /// Port to run the web server on #[arg( short, long, default_value = "3000", help = "Port to run the web server on" )] port: u16, /// Host to bind the web server to #[arg( long, default_value = "127.0.0.1", help = "Host to bind the web server to" )] host: String, /// Open browser automatically #[arg(long, help = "Open browser automatically when server starts")] open: bool, /// Authentication token for both Basic Auth (password) and Bearer token #[arg(long, help = "Authentication token to secure the web interface")] auth_token: Option, }, /// Terminal-integrated session (one session per terminal) #[command( about = "Terminal-integrated goose session", long_about = "Runs a goose session tied to your terminal window.\n\ Each terminal maintains its own persistent session that resumes automatically.\n\n\ Setup:\n \ eval \"$(goose term init zsh)\" # Add to ~/.zshrc\n\n\ Usage:\n \ goose term run \"list files in this directory\"\n \ @goose \"create a python script\" # using alias\n \ @g \"quick question\" # short alias" )] Term { #[command(subcommand)] command: TermCommand, }, /// Generate completions for various shells #[command(about = "Generate the autocompletion script for the specified shell")] Completion { #[arg(value_enum)] shell: ClapShell, }, } #[derive(Subcommand)] enum TermCommand { /// Print shell initialization script #[command( about = "Print shell initialization script", long_about = "Prints shell configuration to set up terminal-integrated sessions.\n\ Each terminal gets a persistent goose session that automatically resumes.\n\n\ Setup:\n \ echo 'eval \"$(goose term init zsh)\"' >> ~/.zshrc\n \ source ~/.zshrc\n\n\ With --default (anything typed that isn't a command goes to goose):\n \ echo 'eval \"$(goose term init zsh --default)\"' >> ~/.zshrc" )] Init { /// Shell type (bash, zsh, fish, powershell) #[arg(value_enum)] shell: Shell, #[arg(short, long, help = "Name for the terminal session")] name: Option, /// Make goose the default handler for unknown commands #[arg( long = "default", help = "Make goose the default handler for unknown commands", long_help = "When enabled, anything you type that isn't a valid command will be sent to goose. Only supported for zsh and bash." )] default: bool, }, /// Log a shell command (called by shell hook) #[command(about = "Log a shell command to the session", hide = true)] Log { /// The command that was executed command: String, }, /// Run a prompt in the terminal session #[command( about = "Run a prompt in the terminal session", long_about = "Run a prompt in the terminal-integrated session.\n\n\ Examples:\n \ goose term run list files in this directory\n \ @goose list files # using alias\n \ @g why did that fail # short alias" )] Run { /// The prompt to send to goose (multiple words allowed without quotes) #[arg(required = true, num_args = 1..)] prompt: Vec, }, /// Print session info for prompt integration #[command( about = "Print session info for prompt integration", long_about = "Prints compact session info (token usage, model) for shell prompt integration.\n\ Example output: ●○○○○ sonnet" )] Info, } #[derive(clap::ValueEnum, Clone, Debug)] enum CliProviderVariant { OpenAi, Databricks, Ollama, } #[derive(Debug)] pub struct InputConfig { pub contents: Option, pub extensions_override: Option>, pub additional_system_prompt: Option, } #[derive(Debug)] pub struct RecipeInfo { pub session_settings: Option, pub sub_recipes: Option>, pub final_output_response: Option, pub retry_config: Option, } fn get_command_name(command: &Option) -> &'static str { match command { Some(Command::Configure {}) => "configure", Some(Command::Info { .. }) => "info", Some(Command::Mcp { .. }) => "mcp", Some(Command::Acp { .. }) => "acp", Some(Command::Session { .. }) => "session", Some(Command::Project {}) => "project", Some(Command::Projects) => "projects", Some(Command::Run { .. }) => "run", Some(Command::Schedule { .. }) => "schedule", Some(Command::Update { .. }) => "update", Some(Command::Bench { .. }) => "bench", Some(Command::Recipe { .. }) => "recipe", Some(Command::Web { .. }) => "web", Some(Command::Term { .. }) => "term", Some(Command::Completion { .. }) => "completion", None => "default_session", } } async fn handle_mcp_command(server: McpCommand) -> Result<()> { let name = server.name(); crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; match server { McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?, McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?, McpCommand::Memory => serve(MemoryServer::new()).await?, McpCommand::Tutorial => serve(TutorialServer::new()).await?, McpCommand::Developer => serve(DeveloperServer::new()).await?, } Ok(()) } async fn handle_session_subcommand(command: SessionCommand) -> Result<()> { match command { SessionCommand::List { format, ascending, working_dir, limit, } => { handle_session_list(format, ascending, working_dir, limit).await?; } SessionCommand::Remove { identifier, regex } => { let (session_id, name) = if let Some(id) = identifier { (id.session_id, id.name) } else { (None, None) }; handle_session_remove(session_id, name, regex).await?; } SessionCommand::Export { identifier, output, format, } => { let session_identifier = if let Some(id) = identifier { lookup_session_id(id).await? } else { match crate::commands::session::prompt_interactive_session_selection().await { Ok(id) => id, Err(e) => { eprintln!("Error: {}", e); return Ok(()); } } }; crate::commands::session::handle_session_export(session_identifier, output, format) .await?; } SessionCommand::Diagnostics { identifier, output } => { let session_id = if let Some(id) = identifier { lookup_session_id(id).await? } else { match crate::commands::session::prompt_interactive_session_selection().await { Ok(id) => id, Err(e) => { eprintln!("Error: {}", e); return Ok(()); } } }; crate::commands::session::handle_diagnostics(&session_id, output).await?; } } Ok(()) } async fn handle_interactive_session( identifier: Option, resume: bool, history: bool, session_opts: SessionOptions, extension_opts: ExtensionOptions, ) -> Result<()> { let session_start = std::time::Instant::now(); let session_type = if resume { "resumed" } else { "new" }; tracing::info!( counter.goose.session_starts = 1, session_type, interactive = true, "Session started" ); if let Some(Identifier { session_id: Some(_), .. }) = &identifier { if !resume { eprintln!("Error: --session-id can only be used with --resume flag"); std::process::exit(1); } } let session_id = get_or_create_session_id(identifier, resume, false).await?; let mut session: crate::CliSession = build_session(SessionBuilderConfig { session_id, resume, no_session: false, extensions: extension_opts.extensions, streamable_http_extensions: extension_opts.streamable_http_extensions, builtins: extension_opts.builtins, extensions_override: None, additional_system_prompt: None, settings: None, provider: None, model: None, debug: session_opts.debug, max_tool_repetitions: session_opts.max_tool_repetitions, max_turns: session_opts.max_turns, scheduled_job_id: None, interactive: true, quiet: false, sub_recipes: None, final_output_response: None, retry_config: None, output_format: "text".to_string(), }) .await; if resume && history { session.render_message_history(); } let result = session.interactive(None).await; log_session_completion(&session, session_start, session_type, result.is_ok()).await; result } async fn log_session_completion( session: &crate::CliSession, session_start: std::time::Instant, session_type: &str, success: bool, ) { let session_duration = session_start.elapsed(); let exit_type = if success { "normal" } else { "error" }; let (total_tokens, message_count) = session .get_session() .await .map(|m| (m.total_tokens.unwrap_or(0), m.message_count)) .unwrap_or((0, 0)); tracing::info!( counter.goose.session_completions = 1, session_type, exit_type, duration_ms = session_duration.as_millis() as u64, total_tokens, message_count, "Session completed" ); tracing::info!( counter.goose.session_duration_ms = session_duration.as_millis() as u64, session_type, "Session duration" ); if total_tokens > 0 { tracing::info!( counter.goose.session_tokens = total_tokens, session_type, "Session tokens" ); } } fn parse_run_input( input_opts: &InputOptions, quiet: bool, ) -> Result)>> { match ( &input_opts.instructions, &input_opts.input_text, &input_opts.recipe, ) { (Some(file), _, _) if file == "-" => { let mut contents = String::new(); std::io::stdin() .read_to_string(&mut contents) .expect("Failed to read from stdin"); Ok(Some(( InputConfig { contents: Some(contents), extensions_override: None, additional_system_prompt: input_opts.system.clone(), }, 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); }); Ok(Some(( InputConfig { contents: Some(contents), extensions_override: None, additional_system_prompt: None, }, None, ))) } (_, Some(text), _) => Ok(Some(( InputConfig { contents: Some(text.clone()), extensions_override: None, additional_system_prompt: input_opts.system.clone(), }, None, ))), (_, _, Some(recipe_name)) => { let recipe_display_name = std::path::Path::new(recipe_name) .file_name() .and_then(|name| name.to_str()) .unwrap_or(recipe_name); let recipe_version = crate::recipes::search_recipe::load_recipe_file(recipe_name) .ok() .and_then(|rf| { goose::recipe::template_recipe::parse_recipe_content( &rf.content, Some(rf.parent_dir.display().to_string()), ) .ok() .map(|(r, _)| r.version) }) .unwrap_or_else(|| "unknown".to_string()); if input_opts.explain { explain_recipe(recipe_name, input_opts.params.clone())?; return Ok(None); } if input_opts.render_recipe { if let Err(err) = render_recipe_as_yaml(recipe_name, input_opts.params.clone()) { eprintln!("{}: {}", console::style("Error").red().bold(), err); std::process::exit(1); } return Ok(None); } tracing::info!( counter.goose.recipe_runs = 1, recipe_name = %recipe_display_name, recipe_version = %recipe_version, session_type = "recipe", interface = "cli", "Recipe execution started" ); let (input_config, recipe_info) = extract_recipe_info_from_cli( recipe_name.clone(), input_opts.params.clone(), input_opts.additional_sub_recipes.clone(), quiet, )?; Ok(Some((input_config, Some(recipe_info)))) } (None, None, None) => { eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin."); std::process::exit(1); } } } async fn handle_run_command( input_opts: InputOptions, identifier: Option, run_behavior: RunBehavior, session_opts: SessionOptions, extension_opts: ExtensionOptions, output_opts: OutputOptions, model_opts: ModelOptions, ) -> Result<()> { let parsed = parse_run_input(&input_opts, output_opts.quiet)?; let Some((input_config, recipe_info)) = parsed else { return Ok(()); }; if let Some(Identifier { session_id: Some(_), .. }) = &identifier { if !run_behavior.resume { eprintln!("Error: --session-id can only be used with --resume flag"); std::process::exit(1); } } let session_id = get_or_create_session_id(identifier, run_behavior.resume, run_behavior.no_session).await?; let mut session = build_session(SessionBuilderConfig { session_id, resume: run_behavior.resume, no_session: run_behavior.no_session, extensions: extension_opts.extensions, streamable_http_extensions: extension_opts.streamable_http_extensions, builtins: extension_opts.builtins, extensions_override: input_config.extensions_override, additional_system_prompt: input_config.additional_system_prompt, settings: recipe_info .as_ref() .and_then(|r| r.session_settings.clone()), provider: model_opts.provider, model: model_opts.model, debug: session_opts.debug, max_tool_repetitions: session_opts.max_tool_repetitions, max_turns: session_opts.max_turns, scheduled_job_id: run_behavior.scheduled_job_id, interactive: run_behavior.interactive, quiet: output_opts.quiet, sub_recipes: recipe_info.as_ref().and_then(|r| r.sub_recipes.clone()), final_output_response: recipe_info .as_ref() .and_then(|r| r.final_output_response.clone()), retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()), output_format: output_opts.output_format, }) .await; if run_behavior.interactive { session.interactive(input_config.contents).await } else if let Some(contents) = input_config.contents { let session_start = std::time::Instant::now(); let session_type = if recipe_info.is_some() { "recipe" } else { "run" }; tracing::info!( counter.goose.session_starts = 1, session_type, interactive = false, "Headless session started" ); let result = session.headless(contents).await; log_session_completion(&session, session_start, session_type, result.is_ok()).await; result } else { Err(anyhow::anyhow!( "no text provided for prompt in headless mode" )) } } async fn handle_schedule_command(command: SchedulerCommand) -> Result<()> { match command { SchedulerCommand::Add { schedule_id, cron, recipe_source, } => handle_schedule_add(schedule_id, cron, recipe_source).await, SchedulerCommand::List {} => handle_schedule_list().await, SchedulerCommand::Remove { schedule_id } => handle_schedule_remove(schedule_id).await, SchedulerCommand::Sessions { schedule_id, limit } => { handle_schedule_sessions(schedule_id, limit).await } SchedulerCommand::RunNow { schedule_id } => handle_schedule_run_now(schedule_id).await, SchedulerCommand::ServicesStatus {} => handle_schedule_services_status().await, SchedulerCommand::ServicesStop {} => handle_schedule_services_stop().await, SchedulerCommand::CronHelp {} => handle_schedule_cron_help().await, } } async fn handle_bench_command(cmd: BenchCommand) -> Result<()> { match cmd { BenchCommand::Selectors { config } => BenchRunner::list_selectors(config)?, BenchCommand::InitConfig { name } => { let mut config = BenchRunConfig::default(); let cwd = std::env::current_dir()?; config.output_dir = Some(cwd); config.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?, BenchCommand::GenerateLeaderboard { benchmark_dir } => { MetricAggregator::generate_csv_from_benchmark_dir(&benchmark_dir)? } } Ok(()) } fn handle_recipe_subcommand(command: RecipeCommand) -> Result<()> { match command { RecipeCommand::Validate { recipe_name } => handle_validate(&recipe_name), RecipeCommand::Deeplink { recipe_name, params, } => { handle_deeplink(&recipe_name, ¶ms)?; Ok(()) } RecipeCommand::Open { recipe_name, params, } => handle_open(&recipe_name, ¶ms), RecipeCommand::List { format, verbose } => handle_list(&format, verbose), } } async fn handle_term_subcommand(command: TermCommand) -> Result<()> { match command { TermCommand::Init { shell, name, default, } => handle_term_init(shell, name, default).await, TermCommand::Log { command } => handle_term_log(command).await, TermCommand::Run { prompt } => handle_term_run(prompt).await, TermCommand::Info => handle_term_info().await, } } async fn handle_default_session() -> Result<()> { if !Config::global().exists() { return handle_configure().await; } let session_id = get_or_create_session_id(None, false, false).await?; let mut session = build_session(SessionBuilderConfig { session_id, resume: false, no_session: false, extensions: Vec::new(), streamable_http_extensions: Vec::new(), builtins: Vec::new(), extensions_override: None, additional_system_prompt: None, settings: None::, provider: None, model: None, debug: false, max_tool_repetitions: None, max_turns: None, scheduled_job_id: None, interactive: true, quiet: false, sub_recipes: None, final_output_response: None, retry_config: None, output_format: "text".to_string(), }) .await; session.interactive(None).await } pub async fn cli() -> anyhow::Result<()> { let cli = Cli::parse(); if let Err(e) = crate::project_tracker::update_project_tracker(None, None) { warn!("Warning: Failed to update project tracker: {}", e); } let command_name = get_command_name(&cli.command); tracing::info!( counter.goose.cli_commands = 1, command = command_name, "CLI command executed" ); match cli.command { Some(Command::Completion { shell }) => { let mut cmd = Cli::command(); let bin_name = cmd.get_name().to_string(); generate(shell, &mut cmd, bin_name, &mut std::io::stdout()); Ok(()) } Some(Command::Configure {}) => handle_configure().await, Some(Command::Info { verbose }) => handle_info(verbose), Some(Command::Mcp { server }) => handle_mcp_command(server).await, Some(Command::Acp { builtins }) => run_acp_agent(builtins).await, Some(Command::Session { command: Some(cmd), .. }) => handle_session_subcommand(cmd).await, Some(Command::Session { command: None, identifier, resume, history, session_opts, extension_opts, }) => { handle_interactive_session(identifier, resume, history, session_opts, extension_opts) .await } Some(Command::Project {}) => { handle_project_default()?; Ok(()) } Some(Command::Projects) => { handle_projects_interactive()?; Ok(()) } Some(Command::Run { input_opts, identifier, run_behavior, session_opts, extension_opts, output_opts, model_opts, }) => { handle_run_command( input_opts, identifier, run_behavior, session_opts, extension_opts, output_opts, model_opts, ) .await } Some(Command::Schedule { command }) => handle_schedule_command(command).await, Some(Command::Update { canary, reconfigure, }) => { crate::commands::update::update(canary, reconfigure)?; Ok(()) } Some(Command::Bench { cmd }) => handle_bench_command(cmd).await, Some(Command::Recipe { command }) => handle_recipe_subcommand(command), Some(Command::Web { port, host, open, auth_token, }) => crate::commands::web::handle_web(port, host, open, auth_token).await, Some(Command::Term { command }) => handle_term_subcommand(command).await, None => handle_default_session().await, } }