2025-03-26 17:27:54 -07:00
use anyhow ::Result ;
use clap ::{ Args , Parser , Subcommand } ;
2025-04-09 18:57:24 -07:00
use goose ::config ::{ Config , ExtensionConfig } ;
2025-03-26 17:27:54 -07:00
2025-04-08 14:43:43 -04:00
use crate ::commands ::bench ::agent_generator ;
2025-03-26 17:27:54 -07:00
use crate ::commands ::configure ::handle_configure ;
use crate ::commands ::info ::handle_info ;
use crate ::commands ::mcp ::run_server ;
2025-04-09 18:57:24 -07:00
use crate ::commands ::recipe ::{ handle_deeplink , handle_validate } ;
2025-04-22 15:51:33 -07:00
use crate ::commands ::session ::{ handle_session_list , handle_session_remove } ;
2025-03-26 17:27:54 -07:00
use crate ::logging ::setup_logging ;
2025-04-09 18:57:24 -07:00
use crate ::recipe ::load_recipe ;
2025-03-26 17:27:54 -07:00
use crate ::session ;
2025-04-09 18:57:24 -07:00
use crate ::session ::{ build_session , SessionBuilderConfig } ;
2025-04-08 14:43:43 -04:00
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 ;
2025-03-26 17:27:54 -07:00
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 < Command > ,
}
#[ 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 < String > ,
#[ 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 < PathBuf > ,
}
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! ( )
}
}
2025-05-02 08:47:45 +10:00
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 ) ) ,
}
}
2025-03-26 17:27:54 -07:00
#[ 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 ,
2025-04-23 17:21:27 +02:00
#[ 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 ,
2025-03-26 17:27:54 -07:00
} ,
2025-04-22 15:51:33 -07:00
#[ command(about = " Remove sessions " ) ]
Remove {
#[ arg(short, long, help = " session id to be removed " , default_value = " " ) ]
id : String ,
#[ arg(
short,
long,
help = " regex for removing matched session " ,
default_value = " "
) ]
regex : String ,
} ,
2025-03-26 17:27:54 -07:00
}
2025-04-08 14:43:43 -04:00
#[ 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 < PathBuf > ,
} ,
#[ 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 ,
} ,
}
2025-04-09 18:57:24 -07:00
#[ derive(Subcommand) ]
enum RecipeCommand {
/// Validate a recipe file
2025-05-06 09:54:35 +10:00
#[ command(about = " Validate a recipe " ) ]
2025-04-09 18:57:24 -07:00
Validate {
2025-05-06 09:54:35 +10:00
/// 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 ,
2025-04-09 18:57:24 -07:00
} ,
/// Generate a deeplink for a recipe file
2025-05-06 09:54:35 +10:00
#[ command(about = " Generate a deeplink for a recipe " ) ]
2025-04-09 18:57:24 -07:00
Deeplink {
2025-05-06 09:54:35 +10:00
/// 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 ,
2025-04-09 18:57:24 -07:00
} ,
}
2025-03-26 17:27:54 -07:00
#[ 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 < SessionCommand > ,
/// Identifier for the chat session
#[ command(flatten) ]
identifier : Option < Identifier > ,
/// 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
) ]
2025-04-09 18:57:24 -07:00
extensions : Vec < String > ,
2025-03-26 17:27:54 -07:00
2025-04-07 13:42:38 -07:00
/// 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
) ]
2025-04-09 18:57:24 -07:00
remote_extensions : Vec < String > ,
2025-04-07 13:42:38 -07:00
2025-03-26 17:27:54 -07:00
/// 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 = ','
) ]
2025-04-09 18:57:24 -07:00
builtins : Vec < String > ,
2025-03-26 17:27:54 -07:00
} ,
/// 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. " ,
2025-04-09 18:57:24 -07:00
conflicts_with = " input_text " ,
conflicts_with = " recipe "
2025-03-26 17:27:54 -07:00
) ]
instructions : Option < String > ,
/// 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. " ,
2025-04-09 18:57:24 -07:00
conflicts_with = " instructions " ,
conflicts_with = " recipe "
2025-03-26 17:27:54 -07:00
) ]
input_text : Option < String > ,
2025-05-06 09:54:35 +10:00
/// Recipe name or full path to the recipe file
2025-04-09 18:57:24 -07:00
#[ arg(
short = None,
long = " recipe " ,
2025-05-06 09:54:35 +10:00
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 " ,
long_help = " Recipe name to get recipe file or the full path of the recipe file that defines a custom agent configuration " ,
2025-04-09 18:57:24 -07:00
conflicts_with = " instructions " ,
conflicts_with = " input_text "
) ]
recipe : Option < String > ,
2025-05-02 08:47:45 +10:00
#[ 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,
) ]
params : Vec < ( String , String ) > ,
2025-03-26 17:27:54 -07:00
/// 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 < Identifier > ,
/// 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
) ]
2025-04-09 18:57:24 -07:00
extensions : Vec < String > ,
2025-03-26 17:27:54 -07:00
2025-04-07 13:42:38 -07:00
/// 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
) ]
2025-04-09 18:57:24 -07:00
remote_extensions : Vec < String > ,
2025-04-07 13:42:38 -07:00
2025-03-26 17:27:54 -07:00
/// 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 = ','
) ]
2025-04-09 18:57:24 -07:00
builtins : Vec < String > ,
} ,
/// Recipe utilities for validation and deeplinking
#[ command(about = " Recipe utilities for validation and deeplinking " ) ]
Recipe {
#[ command(subcommand) ]
command : RecipeCommand ,
2025-03-26 17:27:54 -07:00
} ,
/// 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 {
2025-04-08 14:43:43 -04:00
#[ command(subcommand) ]
cmd : BenchCommand ,
2025-03-26 17:27:54 -07:00
} ,
}
#[ derive(clap::ValueEnum, Clone, Debug) ]
enum CliProviderVariant {
OpenAi ,
Databricks ,
Ollama ,
}
2025-04-09 18:57:24 -07:00
struct InputConfig {
contents : Option < String > ,
extensions_override : Option < Vec < ExtensionConfig > > ,
additional_system_prompt : Option < String > ,
}
2025-03-26 17:27:54 -07:00
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 ,
2025-04-09 18:57:24 -07:00
extensions ,
remote_extensions ,
builtins ,
2025-03-26 17:27:54 -07:00
} ) = > {
2025-04-08 14:43:43 -04:00
return match command {
2025-04-23 17:21:27 +02:00
Some ( SessionCommand ::List {
verbose ,
format ,
ascending ,
} ) = > {
handle_session_list ( verbose , format , ascending ) ? ;
2025-04-08 14:43:43 -04:00
Ok ( ( ) )
2025-03-26 17:27:54 -07:00
}
2025-04-22 15:51:33 -07:00
Some ( SessionCommand ::Remove { id , regex } ) = > {
handle_session_remove ( id , regex ) ? ;
return Ok ( ( ) ) ;
}
2025-03-26 17:27:54 -07:00
None = > {
// Run session command by default
2025-05-02 08:47:45 +10:00
let mut session : crate ::Session = build_session ( SessionBuilderConfig {
2025-04-09 18:57:24 -07:00
identifier : identifier . map ( extract_identifier ) ,
2025-03-26 17:27:54 -07:00
resume ,
2025-04-09 18:57:24 -07:00
extensions ,
remote_extensions ,
builtins ,
extensions_override : None ,
additional_system_prompt : None ,
2025-03-26 17:27:54 -07:00
debug ,
2025-04-09 18:57:24 -07:00
} )
2025-03-26 17:27:54 -07:00
. await ;
setup_logging (
session . session_file ( ) . file_stem ( ) . and_then ( | s | s . to_str ( ) ) ,
None ,
) ? ;
let _ = session . interactive ( None ) . await ;
2025-04-08 14:43:43 -04:00
Ok ( ( ) )
2025-03-26 17:27:54 -07:00
}
2025-04-08 14:43:43 -04:00
} ;
2025-03-26 17:27:54 -07:00
}
Some ( Command ::Run {
instructions ,
input_text ,
2025-04-09 18:57:24 -07:00
recipe ,
2025-03-26 17:27:54 -07:00
interactive ,
identifier ,
resume ,
debug ,
2025-04-09 18:57:24 -07:00
extensions ,
remote_extensions ,
builtins ,
2025-05-02 08:47:45 +10:00
params ,
2025-03-26 17:27:54 -07:00
} ) = > {
2025-04-09 18:57:24 -07:00
let input_config = match ( instructions , input_text , recipe ) {
( Some ( file ) , _ , _ ) if file = = " - " = > {
let mut input = String ::new ( ) ;
2025-03-26 17:27:54 -07:00
std ::io ::stdin ( )
2025-04-09 18:57:24 -07:00
. read_to_string ( & mut input )
2025-03-26 17:27:54 -07:00
. expect ( " Failed to read from stdin " ) ;
2025-04-09 18:57:24 -07:00
InputConfig {
contents : Some ( input ) ,
extensions_override : None ,
additional_system_prompt : None ,
}
2025-03-26 17:27:54 -07:00
}
2025-04-09 18:57:24 -07:00
( 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 ,
} ,
2025-05-06 09:54:35 +10:00
( _ , _ , Some ( recipe_name ) ) = > {
let recipe =
load_recipe ( & recipe_name , true , Some ( params ) ) . unwrap_or_else ( | err | {
eprintln! ( " {} : {} " , console ::style ( " Error " ) . red ( ) . bold ( ) , err ) ;
std ::process ::exit ( 1 ) ;
} ) ;
2025-04-09 18:57:24 -07:00
InputConfig {
contents : recipe . prompt ,
extensions_override : recipe . extensions ,
2025-05-02 12:20:29 -04:00
additional_system_prompt : recipe . instructions ,
2025-04-09 18:57:24 -07:00
}
}
( None , None , None ) = > {
2025-04-11 11:34:46 -07:00
eprintln! ( " Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin. " ) ;
2025-03-26 17:27:54 -07:00
std ::process ::exit ( 1 ) ;
}
} ;
2025-04-09 18:57:24 -07:00
let mut session = build_session ( SessionBuilderConfig {
identifier : identifier . map ( extract_identifier ) ,
2025-03-26 17:27:54 -07:00
resume ,
2025-04-09 18:57:24 -07:00
extensions ,
remote_extensions ,
builtins ,
extensions_override : input_config . extensions_override ,
additional_system_prompt : input_config . additional_system_prompt ,
2025-03-26 17:27:54 -07:00
debug ,
2025-04-09 18:57:24 -07:00
} )
2025-03-26 17:27:54 -07:00
. await ;
setup_logging (
session . session_file ( ) . file_stem ( ) . and_then ( | s | s . to_str ( ) ) ,
None ,
) ? ;
if interactive {
2025-04-11 11:34:33 -07:00
let _ = session . interactive ( input_config . contents ) . await ;
2025-04-09 18:57:24 -07:00
} else if let Some ( contents ) = input_config . contents {
2025-04-11 11:34:33 -07:00
let _ = session . headless ( contents ) . await ;
2025-04-09 18:57:24 -07:00
} else {
eprintln! ( " Error: no text provided for prompt in headless mode " ) ;
std ::process ::exit ( 1 ) ;
2025-03-26 17:27:54 -07:00
}
return Ok ( ( ) ) ;
}
Some ( Command ::Update {
canary ,
reconfigure ,
} ) = > {
crate ::commands ::update ::update ( canary , reconfigure ) ? ;
return Ok ( ( ) ) ;
}
2025-04-08 14:43:43 -04:00
Some ( Command ::Bench { cmd } ) = > {
match cmd {
BenchCommand ::Selectors { config } = > BenchRunner ::list_selectors ( config ) ? ,
2025-05-01 11:00:54 -05:00
BenchCommand ::InitConfig { name } = > {
let mut config = BenchRunConfig ::default ( ) ;
let cwd =
std ::env ::current_dir ( ) . expect ( " Failed to get current working directory " ) ;
config . output_dir = Some ( cwd ) ;
config . save ( name ) ;
}
2025-04-08 14:43:43 -04:00
BenchCommand ::Run { config } = > BenchRunner ::new ( config ) ? . run ( ) ? ,
BenchCommand ::EvalModel { config } = > ModelRunner ::from ( config ) ? . run ( ) ? ,
BenchCommand ::ExecEval { config } = > {
EvalRunner ::from ( config ) ? . run ( agent_generator ) . await ?
2025-03-26 17:27:54 -07:00
}
}
return Ok ( ( ) ) ;
}
2025-04-09 18:57:24 -07:00
Some ( Command ::Recipe { command } ) = > {
match command {
2025-05-06 09:54:35 +10:00
RecipeCommand ::Validate { recipe_name } = > {
handle_validate ( & recipe_name ) ? ;
2025-04-09 18:57:24 -07:00
}
2025-05-06 09:54:35 +10:00
RecipeCommand ::Deeplink { recipe_name } = > {
handle_deeplink ( & recipe_name ) ? ;
2025-04-09 18:57:24 -07:00
}
}
return Ok ( ( ) ) ;
}
2025-03-26 17:27:54 -07:00
None = > {
2025-04-08 14:43:43 -04:00
return if ! Config ::global ( ) . exists ( ) {
2025-03-26 17:27:54 -07:00
let _ = handle_configure ( ) . await ;
2025-04-08 14:43:43 -04:00
Ok ( ( ) )
2025-03-26 17:27:54 -07:00
} else {
// Run session command by default
2025-04-09 18:57:24 -07:00
let mut session = build_session ( SessionBuilderConfig ::default ( ) ) . await ;
2025-03-26 17:27:54 -07:00
setup_logging (
session . session_file ( ) . file_stem ( ) . and_then ( | s | s . to_str ( ) ) ,
None ,
) ? ;
2025-04-11 14:16:38 -05:00
if let Err ( e ) = session . interactive ( None ) . await {
eprintln! ( " Session ended with error: {} " , e ) ;
}
2025-04-08 14:43:43 -04:00
Ok ( ( ) )
} ;
2025-03-26 17:27:54 -07:00
}
}
Ok ( ( ) )
}