2025-03-26 17:27:54 -07:00
use anyhow ::Result ;
2026-01-06 00:15:30 +05:30
use clap ::{ Args , CommandFactory , Parser , Subcommand } ;
use clap_complete ::{ generate , Shell as ClapShell } ;
2026-02-02 13:21:35 -05:00
use goose ::builtin_extension ::register_builtin_extensions ;
2026-01-19 13:13:54 +11:00
use goose ::config ::Config ;
2026-01-13 16:29:54 -05:00
use goose ::posthog ::get_telemetry_choice ;
2026-01-19 13:13:54 +11:00
use goose ::recipe ::Recipe ;
2025-11-18 19:24:49 -05:00
use goose_mcp ::mcp_server_runner ::{ serve , McpCommand } ;
use goose_mcp ::{
AutoVisualiserRouter , ComputerControllerServer , DeveloperServer , MemoryServer , TutorialServer ,
} ;
2025-03-26 17:27:54 -07:00
2026-01-13 16:29:54 -05:00
use crate ::commands ::configure ::{ configure_telemetry_consent_dialog , handle_configure } ;
2025-03-26 17:27:54 -07:00
use crate ::commands ::info ::handle_info ;
2025-08-22 07:12:54 -07:00
use crate ::commands ::project ::{ handle_project_default , handle_projects_interactive } ;
2025-10-06 10:06:42 -07:00
use crate ::commands ::recipe ::{ handle_deeplink , handle_list , handle_open , handle_validate } ;
2025-12-01 07:40:17 +01:00
use crate ::commands ::term ::{
handle_term_info , handle_term_init , handle_term_log , handle_term_run , Shell ,
} ;
2025-11-10 13:39:23 -05:00
2025-05-27 10:36:27 -07:00
use crate ::commands ::schedule ::{
2025-06-10 08:17:45 -07:00
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 ,
2025-05-27 10:36:27 -07:00
handle_schedule_sessions ,
} ;
2025-04-22 15:51:33 -07:00
use crate ::commands ::session ::{ handle_session_list , handle_session_remove } ;
2025-07-01 11:09:58 +10:00
use crate ::recipes ::extract_from_cli ::extract_recipe_info_from_cli ;
2025-07-17 14:38:20 +10:00
use crate ::recipes ::recipe ::{ explain_recipe , render_recipe_as_yaml } ;
2026-01-19 13:13:54 +11:00
use crate ::session ::{ build_session , SessionBuilderConfig } ;
2026-01-27 16:30:51 -05:00
use goose ::agents ::Container ;
2025-11-03 21:04:44 -05:00
use goose ::session ::session_manager ::SessionType ;
2025-09-26 14:56:45 -04:00
use goose ::session ::SessionManager ;
2025-03-26 17:27:54 -07:00
use std ::io ::Read ;
use std ::path ::PathBuf ;
2025-10-28 15:27:28 -04:00
use tracing ::warn ;
2025-03-26 17:27:54 -07:00
#[ derive(Parser) ]
2026-02-10 12:30:12 -03:00
#[ command(name = " goose " , author, version, display_name = " " , about, long_about = None) ]
pub struct Cli {
2025-03-26 17:27:54 -07:00
#[ command(subcommand) ]
command : Option < Command > ,
}
2025-10-27 15:44:57 -04:00
#[ derive(Args, Debug, Clone) ]
2025-03-26 17:27:54 -07:00
#[ group(required = false, multiple = false) ]
2025-10-27 15:44:57 -04:00
pub struct Identifier {
2025-03-26 17:27:54 -07:00
#[ arg(
2025-11-06 18:10:44 -05:00
short = 'n',
2025-03-26 17:27:54 -07:00
long,
value_name = " NAME " ,
help = " Name for the chat session (e.g., 'project-x') " ,
2025-11-06 18:10:44 -05:00
long_help = " Specify a name for your chat session. When used with --resume, will resume this specific session if it exists. "
2025-03-26 17:27:54 -07:00
) ]
2025-10-27 15:44:57 -04:00
pub name : Option < String > ,
2025-03-26 17:27:54 -07:00
2025-09-26 14:56:45 -04:00
#[ arg(
long = " session-id " ,
2025-11-06 18:10:44 -05:00
alias = " id " ,
2025-09-26 14:56:45 -04:00
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. "
) ]
2025-10-27 15:44:57 -04:00
pub session_id : Option < String > ,
2025-09-26 14:56:45 -04:00
2025-03-26 17:27:54 -07:00
#[ arg(
long,
value_name = " PATH " ,
2025-09-26 14:56:45 -04:00
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'). "
2025-03-26 17:27:54 -07:00
) ]
2025-10-27 15:44:57 -04:00
pub path : Option < PathBuf > ,
2025-03-26 17:27:54 -07:00
}
2026-01-05 20:30:44 -08:00
/// 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 < u32 > ,
#[ 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 < u32 > ,
2026-01-27 16:30:51 -05:00
#[ arg(
long = " container " ,
value_name = " CONTAINER_ID " ,
help = " Docker container ID to run extensions inside " ,
long_help = " Run extensions (stdio and built-in) inside the specified container. The extension must exist in the container. For built-in extensions, goose must be installed inside the container. "
) ]
pub container : Option < String > ,
2026-01-05 20:30:44 -08:00
}
2026-01-28 10:59:20 -05:00
#[ derive(Debug, Clone) ]
pub struct StreamableHttpOptions {
pub url : String ,
pub timeout : u64 ,
}
fn parse_streamable_http_extension ( input : & str ) -> Result < StreamableHttpOptions , String > {
let mut input_iter = input . split_whitespace ( ) ;
let ( mut url , mut timeout ) = ( String ::new ( ) , goose ::config ::DEFAULT_EXTENSION_TIMEOUT ) ;
if let Some ( url_str ) = input_iter . next ( ) {
url . push_str ( url_str ) ;
}
for kv_pair in input_iter {
if ! kv_pair . contains ( '=' ) {
continue ;
}
let ( key , value ) = kv_pair . split_once ( '=' ) . unwrap ( ) ;
// We Can have more keys here for setting other properties
if key = = " timeout " {
if let Ok ( seconds ) = value . parse ::< u64 > ( ) {
timeout = seconds ;
}
}
}
Ok ( StreamableHttpOptions { url , timeout } )
}
2026-01-05 20:30:44 -08:00
/// 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 < String > ,
#[ arg(
long = " with-streamable-http-extension " ,
value_name = " URL " ,
help = " Add streamable HTTP extensions (can be specified multiple times) " ,
2026-01-28 10:59:20 -05:00
long_help = " Add streamable HTTP extensions from a URL. Can be specified multiple times. Format: 'url...' or 'url... timeout=100' to set up timeout other than default " ,
action = clap::ArgAction::Append,
value_parser = parse_streamable_http_extension
2026-01-05 20:30:44 -08:00
) ]
2026-01-28 10:59:20 -05:00
pub streamable_http_extensions : Vec < StreamableHttpOptions > ,
2026-01-05 20:30:44 -08:00
#[ 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 < String > ,
2026-01-29 14:48:59 -05:00
#[ arg(
long = " no-profile " ,
help = " Don't load your default extensions, only use CLI-specified extensions "
) ]
pub no_profile : bool ,
2026-01-05 20:30:44 -08:00
}
/// 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 < 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. " ,
conflicts_with = " instructions " ,
conflicts_with = " recipe "
) ]
pub input_text : Option < String > ,
/// 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 < String > ,
/// 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 < String > ,
#[ 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 \n Can be specified multiple times to include multiple sub-recipes. " ,
action = clap::ArgAction::Append
) ]
pub additional_sub_recipes : Vec < String > ,
/// 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 < String > ,
/// 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 < String > ,
}
/// 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 < String > ,
}
2025-10-27 15:44:57 -04:00
async fn get_or_create_session_id (
identifier : Option < Identifier > ,
resume : bool ,
no_session : bool ,
) -> Result < Option < String > > {
if no_session {
return Ok ( None ) ;
}
2026-01-15 06:19:17 +08:00
let session_manager = SessionManager ::instance ( ) ;
2026-01-22 12:49:04 -05:00
let resolved_id = if resume {
let Some ( id ) = identifier else {
2026-01-15 06:19:17 +08:00
let sessions = session_manager . list_sessions ( ) . await ? ;
2025-10-27 17:36:50 -04:00
let session_id = sessions
. first ( )
. map ( | s | s . id . clone ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " No session found to resume " ) ) ? ;
2026-01-22 12:49:04 -05:00
return Ok ( Some ( session_id ) ) ;
} ;
if let Some ( session_id ) = id . session_id {
session_id
} else if let Some ( name ) = id . name {
let sessions = session_manager . 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 ) = id . 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 )
} ) ?
2025-10-27 17:36:50 -04:00
} else {
2026-01-22 12:49:04 -05:00
return Err ( anyhow ::anyhow! ( " Invalid identifier " ) ) ;
}
} else {
let Some ( id ) = identifier else {
2026-01-15 06:19:17 +08:00
let session = session_manager
. create_session (
std ::env ::current_dir ( ) ? ,
" CLI Session " . to_string ( ) ,
SessionType ::User ,
)
. await ? ;
2026-01-22 12:49:04 -05:00
return Ok ( Some ( session . id ) ) ;
2025-10-28 15:28:31 -04:00
} ;
2025-10-27 15:44:57 -04:00
2026-01-22 12:49:04 -05:00
if id . session_id . is_some ( ) {
return Err ( anyhow ::anyhow! ( " Cannot use --session-id without --resume " ) ) ;
}
2025-10-27 15:44:57 -04:00
2026-01-22 12:49:04 -05:00
let has_user_provided_name = id . name . is_some ( ) ;
let name = id . name . unwrap_or_else ( | | " CLI Session " . to_string ( ) ) ;
let session = session_manager
. create_session ( std ::env ::current_dir ( ) ? , name . clone ( ) , SessionType ::User )
. await ? ;
if has_user_provided_name {
2026-01-15 06:19:17 +08:00
session_manager
. update ( & session . id )
2025-10-27 15:44:57 -04:00
. user_provided_name ( name )
. apply ( )
. await ? ;
}
2026-01-22 12:49:04 -05:00
return Ok ( Some ( session . id ) ) ;
} ;
Ok ( Some ( resolved_id ) )
2025-10-27 15:44:57 -04:00
}
async fn lookup_session_id ( identifier : Identifier ) -> Result < String > {
2026-01-22 12:49:04 -05:00
let session_manager = SessionManager ::instance ( ) ;
2025-09-26 14:56:45 -04:00
if let Some ( session_id ) = identifier . session_id {
Ok ( session_id )
} else if let Some ( name ) = identifier . name {
2026-01-15 06:19:17 +08:00
let sessions = session_manager . list_sessions ( ) . await ? ;
2025-09-26 14:56:45 -04:00
sessions
. into_iter ( )
2025-10-24 13:34:08 -04:00
. find ( | s | s . name = = name | | s . id = = name )
2025-09-26 14:56:45 -04:00
. map ( | s | s . id )
. ok_or_else ( | | anyhow ::anyhow! ( " No session found with name '{}' " , name ) )
2025-03-26 17:27:54 -07:00
} else if let Some ( path ) = identifier . path {
2025-09-26 14:56:45 -04:00
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 ) )
2025-03-26 17:27:54 -07:00
} else {
2025-10-27 15:44:57 -04:00
Err ( anyhow ::anyhow! ( " No identifier provided " ) )
2025-03-26 17:27:54 -07:00
}
}
2025-10-27 15:44:57 -04:00
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 = " 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-10-01 00:12:15 +10:00
#[ arg(
2025-11-06 18:10:44 -05:00
short = 'w',
short_alias = 'p',
2025-10-01 00:12:15 +10:00
long = " working_dir " ,
help = " Filter sessions by working directory "
) ]
working_dir : Option < PathBuf > ,
#[ arg(short = 'l', long = " limit " , help = " Limit the number of results " ) ]
limit : Option < usize > ,
2025-03-26 17:27:54 -07:00
} ,
2025-11-06 18:10:44 -05:00
#[ command(about = " Remove sessions. Runs interactively if no ID, name, or regex is provided. " ) ]
2025-04-22 15:51:33 -07:00
Remove {
2025-11-06 18:10:44 -05:00
#[ command(flatten) ]
identifier : Option < Identifier > ,
2025-08-11 18:40:54 -04:00
#[ arg(
2025-11-06 18:10:44 -05:00
short = 'r',
2025-08-11 18:40:54 -04:00
long,
2025-11-06 18:10:44 -05:00
help = " Regex for removing matched sessions (optional) "
2025-08-11 18:40:54 -04:00
) ]
2025-05-25 19:27:15 +02:00
regex : Option < String > ,
2025-04-22 15:51:33 -07:00
} ,
2025-10-22 14:25:49 -04:00
#[ command(about = " Export a session " ) ]
2025-06-04 23:27:21 +02:00
Export {
#[ command(flatten) ]
identifier : Option < Identifier > ,
#[ 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 < PathBuf > ,
2025-09-26 14:56:45 -04:00
#[ arg(
long = " format " ,
value_name = " FORMAT " ,
help = " Output format (markdown, json, yaml) " ,
default_value = " markdown "
) ]
format : String ,
2025-06-04 23:27:21 +02:00
} ,
2025-10-22 14:25:49 -04:00
#[ command(name = " diagnostics " ) ]
Diagnostics {
2025-11-06 18:10:44 -05:00
/// Session identifier for generating diagnostics
#[ command(flatten) ]
identifier : Option < Identifier > ,
2025-10-22 14:25:49 -04:00
/// Output path for the diagnostics zip file (optional, defaults to current directory)
2025-11-06 18:10:44 -05:00
#[ arg(short = 'o', long) ]
2025-10-22 14:25:49 -04:00
output : Option < PathBuf > ,
} ,
2025-03-26 17:27:54 -07:00
}
2025-05-27 10:36:27 -07:00
#[ derive(Subcommand, Debug) ]
enum SchedulerCommand {
#[ command(about = " Add a new scheduled job " ) ]
Add {
2025-11-06 18:10:44 -05:00
#[ arg(
long = " schedule-id " ,
alias = " id " ,
help = " Unique ID for the recurring scheduled job "
) ]
schedule_id : String ,
2025-06-10 08:17:45 -07:00
#[ 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 "
) ]
2025-05-27 10:36:27 -07:00
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 {
2025-11-06 18:10:44 -05:00
#[ arg(
long = " schedule-id " ,
alias = " id " ,
help = " ID of the scheduled job to remove (removes the recurring schedule) "
) ]
schedule_id : String ,
2025-05-27 10:36:27 -07:00
} ,
/// List sessions created by a specific schedule
#[ command(about = " List sessions created by a specific schedule " ) ]
Sessions {
/// ID of the schedule
2025-11-06 18:10:44 -05:00
#[ 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 " ) ]
2025-10-01 00:12:15 +10:00
limit : Option < usize > ,
2025-05-27 10:36:27 -07:00
} ,
#[ command(about = " Run a scheduled job immediately " ) ]
RunNow {
/// ID of the schedule to run
2025-11-06 18:10:44 -05:00
#[ arg(long = " schedule-id " , alias = " id " , help = " ID of the schedule to run " ) ]
schedule_id : String ,
2025-05-27 10:36:27 -07:00
} ,
2025-10-29 12:20:08 +11:00
/// Check status of scheduler services (deprecated - no external services needed)
2025-11-06 18:10:44 -05:00
#[ command(about = " [Deprecated] Check status of scheduler services " ) ]
2025-06-10 08:17:45 -07:00
ServicesStatus { } ,
2025-10-29 12:20:08 +11:00
/// Stop scheduler services (deprecated - no external services needed)
2025-11-06 18:10:44 -05:00
#[ command(about = " [Deprecated] Stop scheduler services " ) ]
2025-06-10 08:17:45 -07:00
ServicesStop { } ,
/// Show cron expression examples and help
#[ command(about = " Show cron expression examples and help " ) ]
CronHelp { } ,
2025-05-27 10:36:27 -07:00
}
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-12-03 07:45:25 -08:00
/// 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 < String > ,
2025-04-09 18:57:24 -07:00
} ,
2025-07-16 04:59:11 +09:00
2025-10-06 10:06:42 -07:00
/// 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 ,
2025-12-03 07:45:25 -08:00
/// 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 < String > ,
2025-10-06 10:06:42 -07:00
} ,
2025-07-16 04:59:11 +09:00
/// 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 ,
} ,
2025-04-09 18:57:24 -07:00
}
2025-03-26 17:27:54 -07:00
#[ derive(Subcommand) ]
enum Command {
2025-09-26 23:44:38 -05:00
/// Configure goose settings
#[ command(about = " Configure goose settings " ) ]
2025-03-26 17:27:54 -07:00
Configure { } ,
2025-09-26 23:44:38 -05:00
/// Display goose configuration information
#[ command(about = " Display goose information " ) ]
2025-03-26 17:27:54 -07:00
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 " ) ]
2025-11-18 19:24:49 -05:00
Mcp {
#[ arg(value_parser = clap::value_parser!(McpCommand)) ]
server : McpCommand ,
} ,
2025-03-26 17:27:54 -07:00
2025-09-26 23:44:38 -05:00
/// Run goose as an ACP (Agent Client Protocol) agent
#[ command(about = " Run goose as an ACP agent server on stdio " ) ]
2025-12-30 13:08:38 +08:00
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 < String > ,
} ,
2025-09-05 11:49:26 +10:00
2025-03-26 17:27:54 -07:00
/// Start or resume interactive chat sessions
#[ command(
about = " Start or resume interactive chat sessions " ,
visible_alias = " s "
) ]
Session {
#[ command(subcommand) ]
command : Option < SessionCommand > ,
2026-01-05 20:30:44 -08:00
2025-03-26 17:27:54 -07:00
#[ command(flatten) ]
identifier : Option < Identifier > ,
/// Resume a previous session
#[ arg(
short,
long,
2025-11-06 18:10:44 -05:00
help = " Resume a previous session (last used or specified by --name/--session-id) " ,
2026-02-11 09:35:24 -05:00
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. "
2025-03-26 17:27:54 -07:00
) ]
resume : bool ,
2026-01-22 12:49:04 -05:00
/// Fork a previous session (creates new session with copied history)
#[ arg(
long,
requires = " resume " ,
help = " Fork a previous session (creates new session with copied history) " ,
2026-02-11 09:35:24 -05:00
long_help = " Create a new session by copying all messages from a previous session. Must be used with --resume. If --name or --session-id is provided, forks that specific session. Otherwise, forks the most recently used session. "
2026-01-22 12:49:04 -05:00
) ]
fork : bool ,
2025-05-08 01:42:52 +02:00
/// Show message history when resuming
#[ arg(
long,
help = " Show previous messages when resuming a session " ,
requires = " resume "
) ]
history : bool ,
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
session_opts : SessionOptions ,
2025-07-13 18:09:04 -05:00
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
extension_opts : ExtensionOptions ,
2025-03-26 17:27:54 -07:00
} ,
2025-08-22 07:12:54 -07:00
/// 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 ,
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 {
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
input_opts : InputOptions ,
2025-07-03 11:57:25 +10:00
2025-03-26 17:27:54 -07:00
#[ command(flatten) ]
identifier : Option < Identifier > ,
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
run_behavior : RunBehavior ,
2025-07-01 11:09:58 +10:00
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
session_opts : SessionOptions ,
2025-07-11 12:13:03 -04:00
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
extension_opts : ExtensionOptions ,
2025-11-04 22:33:17 +06:00
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
output_opts : OutputOptions ,
2025-07-11 12:13:03 -04:00
2026-01-05 20:30:44 -08:00
#[ command(flatten) ]
model_opts : ModelOptions ,
2025-04-09 18:57:24 -07:00
} ,
/// 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
} ,
2025-05-27 10:36:27 -07:00
/// Manage scheduled jobs
#[ command(about = " Manage scheduled jobs " , visible_alias = " sched " ) ]
Schedule {
#[ command(subcommand) ]
command : SchedulerCommand ,
} ,
2025-09-26 23:44:38 -05:00
/// Update the goose CLI version
2025-03-26 17:27:54 -07:00
#[ 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 ,
2025-09-26 23:44:38 -05:00
/// Enforce to re-configure goose during update
2025-03-26 17:27:54 -07:00
#[ arg(short, long, help = " Enforce to re-configure goose during update " ) ]
reconfigure : bool ,
} ,
2025-06-05 13:32:57 +10:00
/// Start a web server with a chat interface
2025-06-27 13:42:36 -04:00
#[ command(about = " Experimental: Start a web server with a chat interface " ) ]
2025-06-05 13:32:57 +10:00
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 ,
2025-09-29 13:05:17 -07:00
/// Authentication token for both Basic Auth (password) and Bearer token
#[ arg(long, help = " Authentication token to secure the web interface " ) ]
auth_token : Option < String > ,
2026-01-14 13:14:50 +11:00
/// Allow running without authentication when exposed on the network (unsafe)
#[ arg(
long,
help = " Skip auth requirement when exposed on the network (unsafe) "
) ]
no_auth : bool ,
2025-06-05 13:32:57 +10:00
} ,
2025-12-01 07:40:17 +01:00
/// 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 ,
} ,
2026-02-19 18:30:05 +00:00
/// Manage local inference models
#[ command(about = " Manage local inference models " , visible_alias = " lm " ) ]
LocalModels {
#[ command(subcommand) ]
command : LocalModelsCommand ,
} ,
2026-01-06 00:15:30 +05:30
/// Generate completions for various shells
#[ command(about = " Generate the autocompletion script for the specified shell " ) ]
Completion {
#[ arg(value_enum) ]
shell : ClapShell ,
2026-01-12 22:39:59 +05:30
#[ arg(long, default_value = " goose " , help = " Provide a custom binary name " ) ]
bin_name : String ,
2026-01-06 00:15:30 +05:30
} ,
2026-02-17 09:47:48 -08:00
#[ command(
name = " validate-extensions " ,
about = " Validate a bundled-extensions.json file " ,
hide = true
) ]
ValidateExtensions {
#[ arg(help = " Path to the bundled-extensions.json file " ) ]
file : PathBuf ,
} ,
2025-12-01 07:40:17 +01:00
}
2026-02-19 18:30:05 +00:00
#[ derive(Subcommand) ]
enum LocalModelsCommand {
/// Search HuggingFace for GGUF models
#[ command(about = " Search HuggingFace for GGUF models " ) ]
Search {
/// Search query
query : String ,
/// Maximum number of results
#[ arg(short, long, default_value = " 10 " ) ]
limit : usize ,
} ,
/// Download a model from HuggingFace
#[ command(about = " Download a GGUF model (e.g. bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M) " ) ]
Download {
/// Model spec in user/repo:quantization format
spec : String ,
} ,
/// List downloaded local models
#[ command(about = " List downloaded local models " ) ]
List ,
/// Delete a downloaded model
#[ command(about = " Delete a downloaded local model " ) ]
Delete {
/// Model ID to delete
id : String ,
} ,
}
2025-12-01 07:40:17 +01:00
#[ 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 < String > ,
/// 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 < String > ,
} ,
/// 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 ,
2025-03-26 17:27:54 -07:00
}
#[ derive(clap::ValueEnum, Clone, Debug) ]
enum CliProviderVariant {
OpenAi ,
Databricks ,
Ollama ,
}
2025-06-12 21:32:01 -04:00
#[ derive(Debug) ]
2025-07-01 11:09:58 +10:00
pub struct InputConfig {
pub contents : Option < String > ,
pub additional_system_prompt : Option < String > ,
2025-04-09 18:57:24 -07:00
}
2026-01-05 20:30:44 -08:00
fn get_command_name ( command : & Option < Command > ) -> & 'static str {
match command {
2025-08-01 12:15:12 -07:00
Some ( Command ::Configure { } ) = > " configure " ,
Some ( Command ::Info { .. } ) = > " info " ,
Some ( Command ::Mcp { .. } ) = > " mcp " ,
2025-12-30 13:08:38 +08:00
Some ( Command ::Acp { .. } ) = > " acp " ,
2025-08-01 12:15:12 -07:00
Some ( Command ::Session { .. } ) = > " session " ,
2025-08-22 07:12:54 -07:00
Some ( Command ::Project { } ) = > " project " ,
Some ( Command ::Projects ) = > " projects " ,
2025-08-01 12:15:12 -07:00
Some ( Command ::Run { .. } ) = > " run " ,
Some ( Command ::Schedule { .. } ) = > " schedule " ,
Some ( Command ::Update { .. } ) = > " update " ,
Some ( Command ::Recipe { .. } ) = > " recipe " ,
Some ( Command ::Web { .. } ) = > " web " ,
2025-12-01 07:40:17 +01:00
Some ( Command ::Term { .. } ) = > " term " ,
2026-02-19 18:30:05 +00:00
Some ( Command ::LocalModels { .. } ) = > " local-models " ,
2026-01-06 00:15:30 +05:30
Some ( Command ::Completion { .. } ) = > " completion " ,
2026-02-17 09:47:48 -08:00
Some ( Command ::ValidateExtensions { .. } ) = > " validate-extensions " ,
2025-08-01 12:15:12 -07:00
None = > " default_session " ,
2026-01-05 20:30:44 -08:00
}
}
async fn handle_mcp_command ( server : McpCommand ) -> Result < ( ) > {
let name = server . name ( ) ;
2026-02-09 17:08:46 +11:00
let _ = crate ::logging ::setup_logging ( Some ( & format! ( " mcp- {name} " ) ) ) ;
2026-01-05 20:30:44 -08:00
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 ,
} = > {
2026-01-15 06:19:17 +08:00
let session_manager = SessionManager ::instance ( ) ;
2026-01-05 20:30:44 -08:00
let session_identifier = if let Some ( id ) = identifier {
lookup_session_id ( id ) . await ?
} else {
2026-01-15 06:19:17 +08:00
match crate ::commands ::session ::prompt_interactive_session_selection (
& session_manager ,
)
. await
{
2026-01-05 20:30:44 -08:00
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 } = > {
2026-01-15 06:19:17 +08:00
let session_manager = SessionManager ::instance ( ) ;
2026-01-05 20:30:44 -08:00
let session_id = if let Some ( id ) = identifier {
lookup_session_id ( id ) . await ?
} else {
2026-01-15 06:19:17 +08:00
match crate ::commands ::session ::prompt_interactive_session_selection (
& session_manager ,
)
. await
{
2026-01-05 20:30:44 -08:00
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 < Identifier > ,
resume : bool ,
2026-01-22 12:49:04 -05:00
fork : bool ,
2026-01-05 20:30:44 -08:00
history : bool ,
session_opts : SessionOptions ,
extension_opts : ExtensionOptions ,
) -> Result < ( ) > {
2026-01-13 16:29:54 -05:00
if get_telemetry_choice ( ) . is_none ( ) {
configure_telemetry_consent_dialog ( ) ? ;
}
2026-01-05 20:30:44 -08:00
let session_start = std ::time ::Instant ::now ( ) ;
2026-01-22 12:49:04 -05:00
let session_type = if fork {
" forked "
} else if resume {
" resumed "
} else {
" new "
} ;
2026-01-05 20:30:44 -08:00
tracing ::info! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . session_starts = 1 ,
2026-01-05 20:30:44 -08:00
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 ) ;
}
}
2026-01-22 12:49:04 -05:00
let mut session_id = get_or_create_session_id ( identifier , resume , false ) . await ? ;
if fork {
if let Some ( id ) = session_id {
let session_manager = SessionManager ::instance ( ) ;
let original = session_manager . get_session ( & id , false ) . await ? ;
let copied = session_manager . copy_session ( & id , original . name ) . await ? ;
session_id = Some ( copied . id ) ;
}
}
2026-01-05 20:30:44 -08:00
let mut session : crate ::CliSession = build_session ( SessionBuilderConfig {
session_id ,
resume ,
2026-01-22 12:49:04 -05:00
fork ,
2026-01-05 20:30:44 -08:00
no_session : false ,
extensions : extension_opts . extensions ,
streamable_http_extensions : extension_opts . streamable_http_extensions ,
builtins : extension_opts . builtins ,
2026-01-29 14:48:59 -05:00
no_profile : extension_opts . no_profile ,
2026-01-19 13:13:54 +11:00
recipe : None ,
2026-01-05 20:30:44 -08:00
additional_system_prompt : 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 ,
output_format : " text " . to_string ( ) ,
2026-01-27 16:30:51 -05:00
container : session_opts . container . map ( Container ::new ) ,
2026-01-05 20:30:44 -08:00
} )
. await ;
2026-01-22 12:49:04 -05:00
if ( resume | | fork ) & & history {
2026-01-05 20:30:44 -08:00
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! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . session_completions = 1 ,
2026-01-05 20:30:44 -08:00
session_type ,
exit_type ,
duration_ms = session_duration . as_millis ( ) as u64 ,
total_tokens ,
message_count ,
" Session completed "
) ;
tracing ::info! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . session_duration_ms = session_duration . as_millis ( ) as u64 ,
2026-01-05 20:30:44 -08:00
session_type ,
" Session duration "
) ;
if total_tokens > 0 {
tracing ::info! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . session_tokens = total_tokens ,
2026-01-05 20:30:44 -08:00
session_type ,
" Session tokens "
) ;
}
}
fn parse_run_input (
input_opts : & InputOptions ,
quiet : bool ,
2026-01-19 13:13:54 +11:00
) -> Result < Option < ( InputConfig , Option < Recipe > ) > > {
2026-01-05 20:30:44 -08:00
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 ) ,
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 ) ,
additional_system_prompt : None ,
} ,
None ,
) ) )
}
( _ , Some ( text ) , _ ) = > Ok ( Some ( (
InputConfig {
contents : Some ( text . clone ( ) ) ,
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! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . recipe_runs = 1 ,
2026-01-05 20:30:44 -08:00
recipe_name = % recipe_display_name ,
recipe_version = % recipe_version ,
session_type = " recipe " ,
interface = " cli " ,
" Recipe execution started "
) ;
2026-01-19 13:13:54 +11:00
let ( input_config , recipe ) = extract_recipe_info_from_cli (
2026-01-05 20:30:44 -08:00
recipe_name . clone ( ) ,
input_opts . params . clone ( ) ,
input_opts . additional_sub_recipes . clone ( ) ,
quiet ,
) ? ;
2026-01-19 13:13:54 +11:00
Ok ( Some ( ( input_config , Some ( recipe ) ) ) )
2026-01-05 20:30:44 -08:00
}
( 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 < Identifier > ,
run_behavior : RunBehavior ,
session_opts : SessionOptions ,
extension_opts : ExtensionOptions ,
output_opts : OutputOptions ,
model_opts : ModelOptions ,
) -> Result < ( ) > {
2026-01-13 16:29:54 -05:00
if run_behavior . interactive & & get_telemetry_choice ( ) . is_none ( ) {
configure_telemetry_consent_dialog ( ) ? ;
}
2026-01-05 20:30:44 -08:00
let parsed = parse_run_input ( & input_opts , output_opts . quiet ) ? ;
2026-01-19 13:13:54 +11:00
let Some ( ( input_config , recipe ) ) = parsed else {
2026-01-05 20:30:44 -08:00
return Ok ( ( ) ) ;
2025-08-01 12:15:12 -07:00
} ;
2026-01-05 20:30:44 -08:00
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 ,
2026-01-22 12:49:04 -05:00
fork : false ,
2026-01-05 20:30:44 -08:00
no_session : run_behavior . no_session ,
extensions : extension_opts . extensions ,
streamable_http_extensions : extension_opts . streamable_http_extensions ,
builtins : extension_opts . builtins ,
2026-01-29 14:48:59 -05:00
no_profile : extension_opts . no_profile ,
2026-01-19 13:13:54 +11:00
recipe : recipe . clone ( ) ,
2026-01-05 20:30:44 -08:00
additional_system_prompt : input_config . additional_system_prompt ,
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 ,
output_format : output_opts . output_format ,
2026-01-27 16:30:51 -05:00
container : session_opts . container . map ( Container ::new ) ,
2026-01-05 20:30:44 -08:00
} )
. 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 ( ) ;
2026-01-19 13:13:54 +11:00
let session_type = if recipe . is_some ( ) { " recipe " } else { " run " } ;
2026-01-05 20:30:44 -08:00
tracing ::info! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . session_starts = 1 ,
2026-01-05 20:30:44 -08:00
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 ,
}
}
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 , & params ) ? ;
Ok ( ( ) )
}
RecipeCommand ::Open {
recipe_name ,
params ,
} = > handle_open ( & recipe_name , & params ) ,
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 ,
}
}
2026-02-19 18:30:05 +00:00
async fn handle_local_models_command ( command : LocalModelsCommand ) -> Result < ( ) > {
use goose ::providers ::local_inference ::hf_models ;
use goose ::providers ::local_inference ::local_model_registry ::{
display_name_from_repo , get_registry , model_id_from_repo , LocalModelEntry ,
} ;
match command {
LocalModelsCommand ::Search { query , limit } = > {
println! ( " Searching HuggingFace for ' {} '... " , query ) ;
let results = hf_models ::search_gguf_models ( & query , limit ) . await ? ;
if results . is_empty ( ) {
println! ( " No GGUF models found. " ) ;
return Ok ( ( ) ) ;
}
for model in & results {
println! (
" \n {} (by {} ) — {} downloads " ,
model . model_name , model . author , model . downloads
) ;
for file in & model . gguf_files {
let size = if file . size_bytes > 0 {
format! (
" {:.1} GB " ,
file . size_bytes as f64 / ( 1024.0 * 1024.0 * 1024.0 )
)
} else {
" unknown " . to_string ( )
} ;
println! ( " {} — {} " , file . quantization , size ) ;
}
println! (
" Download: goose local-models download {} :<quantization> " ,
model . repo_id
) ;
}
}
LocalModelsCommand ::Download { spec } = > {
println! ( " Resolving {} ... " , spec ) ;
let ( repo_id , file ) = hf_models ::resolve_model_spec ( & spec ) . await ? ;
let model_id = model_id_from_repo ( & repo_id , & file . quantization ) ;
let display_name = display_name_from_repo ( & repo_id , & file . quantization ) ;
let local_path =
goose ::config ::paths ::Paths ::in_data_dir ( " models " ) . join ( & file . filename ) ;
println! (
" Downloading {} ( {} )... " ,
display_name ,
if file . size_bytes > 0 {
format! (
" {:.1} GB " ,
file . size_bytes as f64 / ( 1024.0 * 1024.0 * 1024.0 )
)
} else {
" unknown size " . to_string ( )
}
) ;
// Register
let entry = LocalModelEntry {
id : model_id . clone ( ) ,
display_name : display_name . clone ( ) ,
repo_id : repo_id . clone ( ) ,
filename : file . filename . clone ( ) ,
quantization : file . quantization . clone ( ) ,
local_path : local_path . clone ( ) ,
source_url : file . download_url . clone ( ) ,
settings : Default ::default ( ) ,
size_bytes : file . size_bytes ,
} ;
{
let mut registry = get_registry ( )
. lock ( )
. map_err ( | _ | anyhow ::anyhow! ( " Failed to acquire registry lock " ) ) ? ;
registry . add_model ( entry ) ? ;
}
// Download
let manager = goose ::download_manager ::get_download_manager ( ) ;
manager
. download_model (
format! ( " {} -model " , model_id ) ,
file . download_url ,
local_path ,
None ,
)
. await ? ;
// Poll progress
loop {
if let Some ( progress ) = manager . get_progress ( & format! ( " {} -model " , model_id ) ) {
match progress . status {
goose ::download_manager ::DownloadStatus ::Downloading = > {
print! (
" \r {:.1} % ( {:.0} MB / {:.0} MB) " ,
progress . progress_percent ,
progress . bytes_downloaded as f64 / ( 1024.0 * 1024.0 ) ,
progress . total_bytes as f64 / ( 1024.0 * 1024.0 ) ,
) ;
use std ::io ::Write ;
std ::io ::stdout ( ) . flush ( ) . ok ( ) ;
}
goose ::download_manager ::DownloadStatus ::Completed = > {
println! ( " \n Downloaded: {} (id: {} ) " , display_name , model_id ) ;
break ;
}
goose ::download_manager ::DownloadStatus ::Failed = > {
let err = progress . error . unwrap_or_default ( ) ;
anyhow ::bail! ( " Download failed: {} " , err ) ;
}
goose ::download_manager ::DownloadStatus ::Cancelled = > {
println! ( " \n Download cancelled. " ) ;
break ;
}
}
}
tokio ::time ::sleep ( std ::time ::Duration ::from_millis ( 500 ) ) . await ;
}
}
LocalModelsCommand ::List = > {
let registry = get_registry ( )
. lock ( )
. map_err ( | _ | anyhow ::anyhow! ( " Failed to acquire registry lock " ) ) ? ;
let models = registry . list_models ( ) ;
if models . is_empty ( ) {
println! ( " No local models downloaded. " ) ;
return Ok ( ( ) ) ;
}
println! ( " {:<40} {:<20} {:<10} Downloaded " , " ID " , " Name " , " Quant " ) ;
println! ( " {} " , " - " . repeat ( 80 ) ) ;
for m in models {
println! (
" {:<40} {:<20} {:<10} {} " ,
m . id ,
m . display_name ,
m . quantization ,
if m . is_downloaded ( ) { " ✓ " } else { " ✗ " }
) ;
}
}
LocalModelsCommand ::Delete { id } = > {
let mut registry = get_registry ( )
. lock ( )
. map_err ( | _ | anyhow ::anyhow! ( " Failed to acquire registry lock " ) ) ? ;
if let Some ( entry ) = registry . get_model ( & id ) {
if entry . local_path . exists ( ) {
std ::fs ::remove_file ( & entry . local_path ) ? ;
}
registry . remove_model ( & id ) ? ;
println! ( " Deleted model: {} " , id ) ;
} else {
println! ( " Model not found: {} " , id ) ;
}
}
}
Ok ( ( ) )
}
2026-01-05 20:30:44 -08:00
async fn handle_default_session ( ) -> Result < ( ) > {
if ! Config ::global ( ) . exists ( ) {
return handle_configure ( ) . await ;
}
2026-01-13 16:29:54 -05:00
if get_telemetry_choice ( ) . is_none ( ) {
configure_telemetry_consent_dialog ( ) ? ;
}
2026-01-05 20:30:44 -08:00
let session_id = get_or_create_session_id ( None , false , false ) . await ? ;
let mut session = build_session ( SessionBuilderConfig {
session_id ,
resume : false ,
2026-01-22 12:49:04 -05:00
fork : false ,
2026-01-05 20:30:44 -08:00
no_session : false ,
extensions : Vec ::new ( ) ,
streamable_http_extensions : Vec ::new ( ) ,
builtins : Vec ::new ( ) ,
2026-01-29 14:48:59 -05:00
no_profile : false ,
2026-01-19 13:13:54 +11:00
recipe : None ,
2026-01-05 20:30:44 -08:00
additional_system_prompt : None ,
provider : None ,
model : None ,
debug : false ,
max_tool_repetitions : None ,
max_turns : None ,
scheduled_job_id : None ,
interactive : true ,
quiet : false ,
output_format : " text " . to_string ( ) ,
2026-01-27 16:30:51 -05:00
container : None ,
2026-01-05 20:30:44 -08:00
} )
. await ;
session . interactive ( None ) . await
}
pub async fn cli ( ) -> anyhow ::Result < ( ) > {
2026-02-02 13:21:35 -05:00
register_builtin_extensions ( goose_mcp ::BUILTIN_EXTENSIONS . clone ( ) ) ;
2026-01-05 20:30:44 -08:00
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 ) ;
2025-08-01 12:15:12 -07:00
tracing ::info! (
2026-02-16 16:45:15 -05:00
monotonic_counter . goose . cli_commands = 1 ,
2025-08-01 12:15:12 -07:00
command = command_name ,
" CLI command executed "
) ;
2025-03-26 17:27:54 -07:00
match cli . command {
2026-01-12 22:39:59 +05:30
Some ( Command ::Completion { shell , bin_name } ) = > {
2026-01-06 00:15:30 +05:30
let mut cmd = Cli ::command ( ) ;
generate ( shell , & mut cmd , bin_name , & mut std ::io ::stdout ( ) ) ;
2026-01-05 20:30:44 -08:00
Ok ( ( ) )
2026-01-06 00:15:30 +05:30
}
2026-01-05 20:30:44 -08:00
Some ( Command ::Configure { } ) = > handle_configure ( ) . await ,
Some ( Command ::Info { verbose } ) = > handle_info ( verbose ) ,
Some ( Command ::Mcp { server } ) = > handle_mcp_command ( server ) . await ,
2026-01-15 06:19:17 +08:00
Some ( Command ::Acp { builtins } ) = > goose_acp ::server ::run ( builtins ) . await ,
2026-01-05 20:30:44 -08:00
Some ( Command ::Session {
command : Some ( cmd ) , ..
} ) = > handle_session_subcommand ( cmd ) . await ,
2025-03-26 17:27:54 -07:00
Some ( Command ::Session {
2026-01-05 20:30:44 -08:00
command : None ,
2025-03-26 17:27:54 -07:00
identifier ,
resume ,
2026-01-22 12:49:04 -05:00
fork ,
2025-05-08 01:42:52 +02:00
history ,
2026-01-05 20:30:44 -08:00
session_opts ,
extension_opts ,
2025-03-26 17:27:54 -07:00
} ) = > {
2026-01-22 12:49:04 -05:00
handle_interactive_session (
identifier ,
resume ,
fork ,
history ,
session_opts ,
extension_opts ,
)
. await
2025-03-26 17:27:54 -07:00
}
2025-08-22 07:12:54 -07:00
Some ( Command ::Project { } ) = > {
handle_project_default ( ) ? ;
2026-01-05 20:30:44 -08:00
Ok ( ( ) )
2025-08-22 07:12:54 -07:00
}
Some ( Command ::Projects ) = > {
handle_projects_interactive ( ) ? ;
2026-01-05 20:30:44 -08:00
Ok ( ( ) )
2025-08-22 07:12:54 -07:00
}
2025-03-26 17:27:54 -07:00
Some ( Command ::Run {
2026-01-05 20:30:44 -08:00
input_opts ,
2025-03-26 17:27:54 -07:00
identifier ,
2026-01-05 20:30:44 -08:00
run_behavior ,
session_opts ,
extension_opts ,
output_opts ,
model_opts ,
2025-03-26 17:27:54 -07:00
} ) = > {
2026-01-05 20:30:44 -08:00
handle_run_command (
input_opts ,
identifier ,
run_behavior ,
session_opts ,
extension_opts ,
output_opts ,
model_opts ,
)
. await
2025-05-27 10:36:27 -07:00
}
2026-01-05 20:30:44 -08:00
Some ( Command ::Schedule { command } ) = > handle_schedule_command ( command ) . await ,
2025-03-26 17:27:54 -07:00
Some ( Command ::Update {
canary ,
reconfigure ,
} ) = > {
2026-02-18 22:18:25 +01:00
crate ::commands ::update ::update ( canary , reconfigure ) . await ? ;
2026-01-05 20:30:44 -08:00
Ok ( ( ) )
2025-04-09 18:57:24 -07:00
}
2026-01-05 20:30:44 -08:00
Some ( Command ::Recipe { command } ) = > handle_recipe_subcommand ( command ) ,
2025-09-29 13:05:17 -07:00
Some ( Command ::Web {
port ,
host ,
open ,
auth_token ,
2026-01-14 13:14:50 +11:00
no_auth ,
} ) = > crate ::commands ::web ::handle_web ( port , host , open , auth_token , no_auth ) . await ,
2026-01-05 20:30:44 -08:00
Some ( Command ::Term { command } ) = > handle_term_subcommand ( command ) . await ,
2026-02-19 18:30:05 +00:00
Some ( Command ::LocalModels { command } ) = > handle_local_models_command ( command ) . await ,
2026-02-17 09:47:48 -08:00
Some ( Command ::ValidateExtensions { file } ) = > {
use goose ::agents ::validate_extensions ::validate_bundled_extensions ;
match validate_bundled_extensions ( & file ) {
Ok ( msg ) = > {
println! ( " {msg} " ) ;
Ok ( ( ) )
}
Err ( e ) = > {
eprintln! ( " {e} " ) ;
std ::process ::exit ( 1 ) ;
}
}
}
2026-01-05 20:30:44 -08:00
None = > handle_default_session ( ) . await ,
2025-03-26 17:27:54 -07:00
}
}