mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
1811 lines
61 KiB
Rust
1811 lines
61 KiB
Rust
use anyhow::Result;
|
|
use clap::{Args, CommandFactory, Parser, Subcommand};
|
|
use clap_complete::{generate, Shell as ClapShell};
|
|
use goose::builtin_extension::register_builtin_extensions;
|
|
use goose::config::Config;
|
|
use goose::posthog::get_telemetry_choice;
|
|
use goose::recipe::Recipe;
|
|
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
|
use goose_mcp::{
|
|
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
|
};
|
|
|
|
use crate::commands::configure::{configure_telemetry_consent_dialog, 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};
|
|
use goose::agents::Container;
|
|
use goose::session::session_manager::SessionType;
|
|
use goose::session::SessionManager;
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
use tracing::warn;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "goose", author, version, display_name = "", about, long_about = None)]
|
|
pub struct Cli {
|
|
#[command(subcommand)]
|
|
command: Option<Command>,
|
|
}
|
|
|
|
#[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<String>,
|
|
|
|
#[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<String>,
|
|
|
|
#[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<PathBuf>,
|
|
}
|
|
|
|
/// 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>,
|
|
|
|
#[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>,
|
|
}
|
|
|
|
#[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 })
|
|
}
|
|
|
|
/// 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)",
|
|
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
|
|
)]
|
|
pub streamable_http_extensions: Vec<StreamableHttpOptions>,
|
|
|
|
#[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>,
|
|
|
|
#[arg(
|
|
long = "no-profile",
|
|
help = "Don't load your default extensions, only use CLI-specified extensions"
|
|
)]
|
|
pub no_profile: bool,
|
|
}
|
|
|
|
/// 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\nCan 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>,
|
|
}
|
|
|
|
async fn get_or_create_session_id(
|
|
identifier: Option<Identifier>,
|
|
resume: bool,
|
|
no_session: bool,
|
|
) -> Result<Option<String>> {
|
|
if no_session {
|
|
return Ok(None);
|
|
}
|
|
|
|
let session_manager = SessionManager::instance();
|
|
|
|
let resolved_id = if resume {
|
|
let Some(id) = identifier else {
|
|
let sessions = session_manager.list_sessions().await?;
|
|
let session_id = sessions
|
|
.first()
|
|
.map(|s| s.id.clone())
|
|
.ok_or_else(|| anyhow::anyhow!("No session found to resume"))?;
|
|
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)
|
|
})?
|
|
} else {
|
|
return Err(anyhow::anyhow!("Invalid identifier"));
|
|
}
|
|
} else {
|
|
let Some(id) = identifier else {
|
|
let session = session_manager
|
|
.create_session(
|
|
std::env::current_dir()?,
|
|
"CLI Session".to_string(),
|
|
SessionType::User,
|
|
)
|
|
.await?;
|
|
return Ok(Some(session.id));
|
|
};
|
|
|
|
if id.session_id.is_some() {
|
|
return Err(anyhow::anyhow!("Cannot use --session-id without --resume"));
|
|
}
|
|
|
|
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 {
|
|
session_manager
|
|
.update(&session.id)
|
|
.user_provided_name(name)
|
|
.apply()
|
|
.await?;
|
|
}
|
|
|
|
return Ok(Some(session.id));
|
|
};
|
|
|
|
Ok(Some(resolved_id))
|
|
}
|
|
|
|
async fn lookup_session_id(identifier: Identifier) -> Result<String> {
|
|
let session_manager = SessionManager::instance();
|
|
|
|
if let Some(session_id) = identifier.session_id {
|
|
Ok(session_id)
|
|
} else if let Some(name) = identifier.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) = 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<PathBuf>,
|
|
|
|
#[arg(short = 'l', long = "limit", help = "Limit the number of results")]
|
|
limit: Option<usize>,
|
|
},
|
|
#[command(about = "Remove sessions. Runs interactively if no ID, name, or regex is provided.")]
|
|
Remove {
|
|
#[command(flatten)]
|
|
identifier: Option<Identifier>,
|
|
#[arg(
|
|
short = 'r',
|
|
long,
|
|
help = "Regex for removing matched sessions (optional)"
|
|
)]
|
|
regex: Option<String>,
|
|
},
|
|
#[command(about = "Export a session")]
|
|
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>,
|
|
|
|
#[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<Identifier>,
|
|
|
|
/// Output path for the diagnostics zip file (optional, defaults to current directory)
|
|
#[arg(short = 'o', long)]
|
|
output: Option<PathBuf>,
|
|
},
|
|
}
|
|
|
|
#[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<usize>,
|
|
},
|
|
#[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)]
|
|
enum GatewayCommand {
|
|
#[command(about = "Show gateway status")]
|
|
Status {},
|
|
|
|
#[command(about = "Start a gateway")]
|
|
Start {
|
|
#[arg(help = "Gateway type (e.g., 'telegram')")]
|
|
gateway_type: String,
|
|
|
|
#[arg(
|
|
long = "bot-token",
|
|
help = "Bot token for the gateway platform",
|
|
long_help = "Authentication token for the gateway platform (e.g., Telegram bot token)"
|
|
)]
|
|
bot_token: String,
|
|
},
|
|
|
|
#[command(about = "Stop a running gateway")]
|
|
Stop {
|
|
#[arg(help = "Gateway type to stop (e.g., 'telegram')")]
|
|
gateway_type: String,
|
|
},
|
|
|
|
#[command(about = "Generate a pairing code for a gateway")]
|
|
Pair {
|
|
#[arg(help = "Gateway type to generate pairing code for")]
|
|
gateway_type: String,
|
|
},
|
|
}
|
|
|
|
#[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<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
},
|
|
|
|
/// 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<String>,
|
|
},
|
|
|
|
/// Start or resume interactive chat sessions
|
|
#[command(
|
|
about = "Start or resume interactive chat sessions",
|
|
visible_alias = "s"
|
|
)]
|
|
Session {
|
|
#[command(subcommand)]
|
|
command: Option<SessionCommand>,
|
|
|
|
#[command(flatten)]
|
|
identifier: Option<Identifier>,
|
|
|
|
/// 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,
|
|
|
|
/// 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)",
|
|
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."
|
|
)]
|
|
fork: 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<Identifier>,
|
|
|
|
#[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,
|
|
},
|
|
|
|
/// Manage gateways for external platform integrations (e.g., Telegram)
|
|
#[command(
|
|
about = "Manage gateways for external platform integrations",
|
|
visible_alias = "gw"
|
|
)]
|
|
Gateway {
|
|
#[command(subcommand)]
|
|
command: GatewayCommand,
|
|
},
|
|
|
|
/// 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,
|
|
},
|
|
|
|
/// 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<String>,
|
|
|
|
/// 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,
|
|
},
|
|
|
|
/// 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,
|
|
},
|
|
/// Manage local inference models
|
|
#[command(about = "Manage local inference models", visible_alias = "lm")]
|
|
LocalModels {
|
|
#[command(subcommand)]
|
|
command: LocalModelsCommand,
|
|
},
|
|
|
|
/// Generate completions for various shells
|
|
#[command(about = "Generate the autocompletion script for the specified shell")]
|
|
Completion {
|
|
#[arg(value_enum)]
|
|
shell: ClapShell,
|
|
|
|
#[arg(long, default_value = "goose", help = "Provide a custom binary name")]
|
|
bin_name: String,
|
|
},
|
|
|
|
#[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,
|
|
},
|
|
}
|
|
|
|
#[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,
|
|
},
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(clap::ValueEnum, Clone, Debug)]
|
|
enum CliProviderVariant {
|
|
OpenAi,
|
|
Databricks,
|
|
Ollama,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct InputConfig {
|
|
pub contents: Option<String>,
|
|
pub additional_system_prompt: Option<String>,
|
|
}
|
|
|
|
fn get_command_name(command: &Option<Command>) -> &'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::Gateway { .. }) => "gateway",
|
|
Some(Command::Schedule { .. }) => "schedule",
|
|
Some(Command::Update { .. }) => "update",
|
|
Some(Command::Recipe { .. }) => "recipe",
|
|
Some(Command::Web { .. }) => "web",
|
|
Some(Command::Term { .. }) => "term",
|
|
Some(Command::LocalModels { .. }) => "local-models",
|
|
Some(Command::Completion { .. }) => "completion",
|
|
Some(Command::ValidateExtensions { .. }) => "validate-extensions",
|
|
None => "default_session",
|
|
}
|
|
}
|
|
|
|
async fn handle_mcp_command(server: McpCommand) -> Result<()> {
|
|
let name = server.name();
|
|
let _ = crate::logging::setup_logging(Some(&format!("mcp-{name}")));
|
|
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_manager = SessionManager::instance();
|
|
let session_identifier = if let Some(id) = identifier {
|
|
lookup_session_id(id).await?
|
|
} else {
|
|
match crate::commands::session::prompt_interactive_session_selection(
|
|
&session_manager,
|
|
)
|
|
.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_manager = SessionManager::instance();
|
|
let session_id = if let Some(id) = identifier {
|
|
lookup_session_id(id).await?
|
|
} else {
|
|
match crate::commands::session::prompt_interactive_session_selection(
|
|
&session_manager,
|
|
)
|
|
.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<Identifier>,
|
|
resume: bool,
|
|
fork: bool,
|
|
history: bool,
|
|
session_opts: SessionOptions,
|
|
extension_opts: ExtensionOptions,
|
|
) -> Result<()> {
|
|
if get_telemetry_choice().is_none() {
|
|
configure_telemetry_consent_dialog()?;
|
|
}
|
|
|
|
let session_start = std::time::Instant::now();
|
|
let session_type = if fork {
|
|
"forked"
|
|
} else if resume {
|
|
"resumed"
|
|
} else {
|
|
"new"
|
|
};
|
|
|
|
tracing::info!(
|
|
monotonic_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 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);
|
|
}
|
|
}
|
|
|
|
let mut session: crate::CliSession = build_session(SessionBuilderConfig {
|
|
session_id,
|
|
resume,
|
|
fork,
|
|
no_session: false,
|
|
extensions: extension_opts.extensions,
|
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
|
builtins: extension_opts.builtins,
|
|
no_profile: extension_opts.no_profile,
|
|
recipe: None,
|
|
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(),
|
|
container: session_opts.container.map(Container::new),
|
|
})
|
|
.await;
|
|
|
|
if (resume || fork) && 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!(
|
|
monotonic_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!(
|
|
monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64,
|
|
session_type,
|
|
"Session duration"
|
|
);
|
|
|
|
if total_tokens > 0 {
|
|
tracing::info!(
|
|
monotonic_counter.goose.session_tokens = total_tokens,
|
|
session_type,
|
|
"Session tokens"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn parse_run_input(
|
|
input_opts: &InputOptions,
|
|
quiet: bool,
|
|
) -> Result<Option<(InputConfig, Option<Recipe>)>> {
|
|
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!(
|
|
monotonic_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) = 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))))
|
|
}
|
|
(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<()> {
|
|
if run_behavior.interactive && get_telemetry_choice().is_none() {
|
|
configure_telemetry_consent_dialog()?;
|
|
}
|
|
|
|
let parsed = parse_run_input(&input_opts, output_opts.quiet)?;
|
|
|
|
let Some((input_config, recipe)) = 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,
|
|
fork: false,
|
|
no_session: run_behavior.no_session,
|
|
extensions: extension_opts.extensions,
|
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
|
builtins: extension_opts.builtins,
|
|
no_profile: extension_opts.no_profile,
|
|
recipe: recipe.clone(),
|
|
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,
|
|
container: session_opts.container.map(Container::new),
|
|
})
|
|
.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.is_some() { "recipe" } else { "run" };
|
|
|
|
tracing::info!(
|
|
monotonic_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_gateway_command(command: GatewayCommand) -> Result<()> {
|
|
use crate::commands::gateway;
|
|
|
|
match command {
|
|
GatewayCommand::Status {} => gateway::handle_gateway_status().await,
|
|
GatewayCommand::Start {
|
|
gateway_type,
|
|
bot_token,
|
|
} => {
|
|
let platform_config = serde_json::json!({ "bot_token": bot_token });
|
|
gateway::handle_gateway_start(gateway_type, platform_config).await
|
|
}
|
|
GatewayCommand::Stop { gateway_type } => gateway::handle_gateway_stop(gateway_type).await,
|
|
GatewayCommand::Pair { gateway_type } => gateway::handle_gateway_pair(gateway_type).await,
|
|
}
|
|
}
|
|
|
|
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, ¶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_local_models_command(command: LocalModelsCommand) -> Result<()> {
|
|
use goose::providers::local_inference::hf_models;
|
|
use goose::providers::local_inference::local_model_registry::{
|
|
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 local_path =
|
|
goose::config::paths::Paths::in_data_dir("models").join(&file.filename);
|
|
|
|
println!(
|
|
"Downloading {} ({})...",
|
|
model_id,
|
|
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(),
|
|
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!("\nDownloaded: {}", 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!("\nDownload 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!("{:<50} {:<10} Downloaded", "ID", "Quant");
|
|
println!("{}", "-".repeat(70));
|
|
for m in models {
|
|
println!(
|
|
"{:<50} {:<10} {}",
|
|
m.id,
|
|
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(())
|
|
}
|
|
|
|
async fn handle_default_session() -> Result<()> {
|
|
if !Config::global().exists() {
|
|
return handle_configure().await;
|
|
}
|
|
|
|
if get_telemetry_choice().is_none() {
|
|
configure_telemetry_consent_dialog()?;
|
|
}
|
|
|
|
let session_id = get_or_create_session_id(None, false, false).await?;
|
|
|
|
let mut session = build_session(SessionBuilderConfig {
|
|
session_id,
|
|
resume: false,
|
|
fork: false,
|
|
no_session: false,
|
|
extensions: Vec::new(),
|
|
streamable_http_extensions: Vec::new(),
|
|
builtins: Vec::new(),
|
|
no_profile: false,
|
|
recipe: None,
|
|
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(),
|
|
container: None,
|
|
})
|
|
.await;
|
|
session.interactive(None).await
|
|
}
|
|
|
|
pub async fn cli() -> anyhow::Result<()> {
|
|
register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone());
|
|
|
|
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!(
|
|
monotonic_counter.goose.cli_commands = 1,
|
|
command = command_name,
|
|
"CLI command executed"
|
|
);
|
|
|
|
match cli.command {
|
|
Some(Command::Completion { shell, bin_name }) => {
|
|
let mut cmd = Cli::command();
|
|
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 }) => goose_acp::server::run(builtins).await,
|
|
Some(Command::Session {
|
|
command: Some(cmd), ..
|
|
}) => handle_session_subcommand(cmd).await,
|
|
Some(Command::Session {
|
|
command: None,
|
|
identifier,
|
|
resume,
|
|
fork,
|
|
history,
|
|
session_opts,
|
|
extension_opts,
|
|
}) => {
|
|
handle_interactive_session(
|
|
identifier,
|
|
resume,
|
|
fork,
|
|
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::Gateway { command }) => handle_gateway_command(command).await,
|
|
Some(Command::Schedule { command }) => handle_schedule_command(command).await,
|
|
Some(Command::Update {
|
|
canary,
|
|
reconfigure,
|
|
}) => {
|
|
crate::commands::update::update(canary, reconfigure).await?;
|
|
Ok(())
|
|
}
|
|
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
|
Some(Command::Web {
|
|
port,
|
|
host,
|
|
open,
|
|
auth_token,
|
|
no_auth,
|
|
}) => crate::commands::web::handle_web(port, host, open, auth_token, no_auth).await,
|
|
Some(Command::Term { command }) => handle_term_subcommand(command).await,
|
|
Some(Command::LocalModels { command }) => handle_local_models_command(command).await,
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
None => handle_default_session().await,
|
|
}
|
|
}
|