diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index efaad9e47f91..c642e48dbf70 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -41,17 +41,17 @@ struct Cli { #[group(required = false, multiple = false)] pub struct Identifier { #[arg( - short, + 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.", - alias = "id" + long_help = "Specify a name for your chat session. When used with --resume, will resume this specific session if it exists." )] pub name: Option, #[arg( long = "session-id", + alias = "id", value_name = "SESSION_ID", help = "Session ID (e.g., '20250921_143022')", long_help = "Specify a session ID directly. When used with --resume, will resume this specific session if it exists." @@ -59,7 +59,6 @@ pub struct Identifier { pub session_id: Option, #[arg( - short, long, value_name = "PATH", help = "Legacy: Path for the chat session", @@ -188,7 +187,8 @@ enum SessionCommand { ascending: bool, #[arg( - short = 'p', + short = 'w', + short_alias = 'p', long = "working_dir", help = "Filter sessions by working directory" )] @@ -197,16 +197,15 @@ enum SessionCommand { #[arg(short = 'l', long = "limit", help = "Limit the number of results")] limit: Option, }, - #[command(about = "Remove sessions. Runs interactively if no ID or regex is provided.")] + #[command(about = "Remove sessions. Runs interactively if no ID, name, or regex is provided.")] Remove { + #[command(flatten)] + identifier: Option, #[arg( - short, + short = 'r', long, - alias = "name", - help = "Session ID to be removed (optional)" + help = "Regex for removing matched sessions (optional)" )] - id: Option, - #[arg(short, long, help = "Regex for removing matched sessions (optional)")] regex: Option, }, #[command(about = "Export a session")] @@ -232,12 +231,12 @@ enum SessionCommand { }, #[command(name = "diagnostics")] Diagnostics { - /// Session ID to generate diagnostics for - #[arg(short, long)] - session_id: String, + /// Session identifier for generating diagnostics + #[command(flatten)] + identifier: Option, /// Output path for the diagnostics zip file (optional, defaults to current directory) - #[arg(short, long)] + #[arg(short = 'o', long)] output: Option, }, } @@ -246,8 +245,12 @@ enum SessionCommand { enum SchedulerCommand { #[command(about = "Add a new scheduled job")] Add { - #[arg(long, help = "Unique ID for the job")] - id: String, + #[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", @@ -264,29 +267,33 @@ enum SchedulerCommand { List {}, #[command(about = "Remove a scheduled job by ID")] Remove { - #[arg(long, help = "ID of the job to remove")] // Changed from positional to named --id - id: String, + #[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, help = "ID of the schedule")] // Explicitly make it --id - id: String, - #[arg(long, help = "Maximum number of sessions to return")] + #[arg(long = "schedule-id", alias = "id", help = "ID of the schedule")] + schedule_id: String, + #[arg(short = 'l', long, help = "Maximum number of sessions to return")] limit: Option, }, #[command(about = "Run a scheduled job immediately")] RunNow { /// ID of the schedule to run - #[arg(long, help = "ID of the schedule to run")] // Explicitly make it --id - id: String, + #[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 = "Check status of scheduler services")] + #[command(about = "[Deprecated] Check status of scheduler services")] ServicesStatus {}, /// Stop scheduler services (deprecated - no external services needed) - #[command(about = "Stop scheduler services")] + #[command(about = "[Deprecated] Stop scheduler services")] ServicesStop {}, /// Show cron expression examples and help #[command(about = "Show cron expression examples and help")] @@ -435,8 +442,8 @@ enum Command { #[arg( short, long, - help = "Resume a previous session (last used or specified by --name)", - long_help = "Continue from a previous chat session. If --name or --path is provided, resumes that specific session. Otherwise resumes the last used session." + 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, @@ -903,8 +910,13 @@ pub async fn cli() -> anyhow::Result<()> { working_dir, limit, }) => Ok(handle_session_list(format, ascending, working_dir, limit).await?), - Some(SessionCommand::Remove { id, regex }) => { - Ok(handle_session_remove(id, regex).await?) + Some(SessionCommand::Remove { identifier, regex }) => { + let (session_id, name) = if let Some(id) = identifier { + (id.session_id, id.name) + } else { + (None, None) + }; + Ok(handle_session_remove(session_id, name, regex).await?) } Some(SessionCommand::Export { identifier, @@ -933,7 +945,19 @@ pub async fn cli() -> anyhow::Result<()> { .await?; Ok(()) } - Some(SessionCommand::Diagnostics { session_id, output }) => { + Some(SessionCommand::Diagnostics { identifier, output }) => { + let session_id = if let Some(id) = identifier { + lookup_session_id(id).await? + } else { + match crate::commands::session::prompt_interactive_session_selection().await + { + Ok(id) => id, + Err(e) => { + eprintln!("Error: {}", e); + return Ok(()); + } + } + }; crate::commands::session::handle_diagnostics(&session_id, output).await?; Ok(()) } @@ -1264,25 +1288,25 @@ pub async fn cli() -> anyhow::Result<()> { Some(Command::Schedule { command }) => { match command { SchedulerCommand::Add { - id, + schedule_id, cron, recipe_source, } => { - handle_schedule_add(id, cron, recipe_source).await?; + handle_schedule_add(schedule_id, cron, recipe_source).await?; } SchedulerCommand::List {} => { handle_schedule_list().await?; } - SchedulerCommand::Remove { id } => { - handle_schedule_remove(id).await?; + SchedulerCommand::Remove { schedule_id } => { + handle_schedule_remove(schedule_id).await?; } - SchedulerCommand::Sessions { id, limit } => { + SchedulerCommand::Sessions { schedule_id, limit } => { // New arm - handle_schedule_sessions(id, limit).await?; + handle_schedule_sessions(schedule_id, limit).await?; } - SchedulerCommand::RunNow { id } => { + SchedulerCommand::RunNow { schedule_id } => { // New arm - handle_schedule_run_now(id).await?; + handle_schedule_run_now(schedule_id).await?; } SchedulerCommand::ServicesStatus {} => { handle_schedule_services_status().await?; diff --git a/crates/goose-cli/src/commands/schedule.rs b/crates/goose-cli/src/commands/schedule.rs index 12f2c08684bb..f8773f8da7ee 100644 --- a/crates/goose-cli/src/commands/schedule.rs +++ b/crates/goose-cli/src/commands/schedule.rs @@ -75,13 +75,13 @@ fn validate_cron_expression(cron: &str) -> Result<()> { } pub async fn handle_schedule_add( - id: String, + schedule_id: String, cron: String, recipe_source_arg: String, // This is expected to be a file path by the Scheduler ) -> Result<()> { println!( "[CLI Debug] Scheduling job ID: {}, Cron: {}, Recipe Source Path: {}", - id, cron, recipe_source_arg + schedule_id, cron, recipe_source_arg ); // Validate cron expression and provide helpful feedback @@ -90,7 +90,7 @@ pub async fn handle_schedule_add( // The Scheduler's add_scheduled_job will handle copying the recipe from recipe_source_arg // to its internal storage and validating the path. let job = ScheduledJob { - id: id.clone(), + id: schedule_id.clone(), source: recipe_source_arg.clone(), // Pass the original user-provided path cron, last_run: None, @@ -116,11 +116,12 @@ pub async fn handle_schedule_add( .extension() .and_then(|ext| ext.to_str()) .unwrap_or("yaml"); - let final_recipe_path = scheduled_recipes_dir.join(format!("{}.{}", id, extension)); + let final_recipe_path = + scheduled_recipes_dir.join(format!("{}.{}", schedule_id, extension)); println!( "Scheduled job '{}' added. Recipe expected at {:?}", - id, final_recipe_path + schedule_id, final_recipe_path ); Ok(()) } @@ -138,7 +139,7 @@ pub async fn handle_schedule_add( ); } _ => Err(anyhow::Error::new(e)) - .context(format!("Failed to add job '{}' to scheduler", id)), + .context(format!("Failed to add job '{}' to scheduler", schedule_id)), } } } @@ -179,41 +180,46 @@ pub async fn handle_schedule_list() -> Result<()> { Ok(()) } -pub async fn handle_schedule_remove(id: String) -> Result<()> { +pub async fn handle_schedule_remove(schedule_id: String) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.remove_scheduled_job(&id).await { + match scheduler.remove_scheduled_job(&schedule_id).await { Ok(_) => { - println!("Scheduled job '{}' and its associated recipe removed.", id); + println!( + "Scheduled job '{}' and its associated recipe removed.", + schedule_id + ); Ok(()) } Err(e) => match e { SchedulerError::JobNotFound(job_id) => { bail!("Error: Job with ID '{}' not found.", job_id); } - _ => Err(anyhow::Error::new(e)) - .context(format!("Failed to remove job '{}' from scheduler", id)), + _ => Err(anyhow::Error::new(e)).context(format!( + "Failed to remove job '{}' from scheduler", + schedule_id + )), }, } } -pub async fn handle_schedule_sessions(id: String, limit: Option) -> Result<()> { +pub async fn handle_schedule_sessions(schedule_id: String, limit: Option) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.sessions(&id, limit.unwrap_or(50)).await { + match scheduler.sessions(&schedule_id, limit.unwrap_or(50)).await { Ok(sessions) => { if sessions.is_empty() { - println!("No sessions found for schedule ID '{}'.", id); + println!("No sessions found for schedule ID '{}'.", schedule_id); } else { - println!("Sessions for schedule ID '{}':", id); + println!("Sessions for schedule ID '{}':", schedule_id); // sessions is now Vec<(String, SessionMetadata)> for (session_name, metadata) in sessions { println!( @@ -227,31 +233,35 @@ pub async fn handle_schedule_sessions(id: String, limit: Option) -> Resul } } Err(e) => { - bail!("Failed to get sessions for schedule '{}': {:?}", id, e); + bail!( + "Failed to get sessions for schedule '{}': {:?}", + schedule_id, + e + ); } } Ok(()) } -pub async fn handle_schedule_run_now(id: String) -> Result<()> { +pub async fn handle_schedule_run_now(schedule_id: String) -> Result<()> { let scheduler_storage_path = get_default_scheduler_storage_path().context("Failed to get scheduler storage path")?; let scheduler = SchedulerFactory::create(scheduler_storage_path) .await .context("Failed to initialize scheduler")?; - match scheduler.run_now(&id).await { + match scheduler.run_now(&schedule_id).await { Ok(session_id) => { println!( "Successfully triggered schedule '{}'. New session ID: {}", - id, session_id + schedule_id, session_id ); } Err(e) => match e { SchedulerError::JobNotFound(job_id) => { bail!("Error: Job with ID '{}' not found.", job_id); } - _ => bail!("Failed to run schedule '{}' now: {:?}", id, e), + _ => bail!("Failed to run schedule '{}' now: {:?}", schedule_id, e), }, } Ok(()) @@ -326,12 +336,12 @@ pub async fn handle_schedule_cron_help() -> Result<()> { println!("💡 EXAMPLES:"); println!( - " goose schedule add --id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml" + " goose schedule add --schedule-id hourly-report --cron \"0 * * * *\" --recipe-source report.yaml" ); println!( - " goose schedule add --id daily-backup --cron \"@daily\" --recipe-source backup.yaml" + " goose schedule add --schedule-id daily-backup --cron \"@daily\" --recipe-source backup.yaml" ); - println!(" goose schedule add --id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml"); + println!(" goose schedule add --schedule-id weekly-summary --cron \"0 9 * * 1\" --recipe-source summary.yaml"); Ok(()) } diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 40e1e1d689f3..a145f01ffd33 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -71,7 +71,11 @@ fn prompt_interactive_session_removal(sessions: &[Session]) -> Result, regex_string: Option) -> Result<()> { +pub async fn handle_session_remove( + session_id: Option, + name: Option, + regex_string: Option, +) -> Result<()> { let all_sessions = match SessionManager::list_sessions().await { Ok(sessions) => sessions, Err(e) => { @@ -82,11 +86,20 @@ pub async fn handle_session_remove(id: Option, regex_string: Option; - if let Some(id_val) = id { + if let Some(id_val) = session_id { if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) { matched_sessions = vec![session.clone()]; } else { - return Err(anyhow::anyhow!("Session '{}' not found.", id_val)); + return Err(anyhow::anyhow!("Session ID '{}' not found.", id_val)); + } + } else if let Some(name_val) = name { + if let Some(session) = all_sessions.iter().find(|s| s.name == name_val) { + matched_sessions = vec![session.clone()]; + } else { + return Err(anyhow::anyhow!( + "Session with name '{}' not found.", + name_val + )); } } else if let Some(regex_val) = regex_string { let session_regex = Regex::new(®ex_val) diff --git a/documentation/docs/guides/goose-cli-commands.md b/documentation/docs/guides/goose-cli-commands.md index f1d28e03eea2..47b18dc3bc6f 100644 --- a/documentation/docs/guides/goose-cli-commands.md +++ b/documentation/docs/guides/goose-cli-commands.md @@ -7,6 +7,21 @@ toc_max_heading_level: 4 Goose provides a command-line interface (CLI) with several commands for managing sessions, configurations and extensions. This guide covers all available CLI commands and interactive session features. +## Flag Naming Conventions + +Goose CLI follows consistent patterns for flag naming to make commands intuitive and predictable: + +- **`--session-id`**: Used for session identifiers (e.g., `20250305_113223`) +- **`--schedule-id`**: Used for schedule job identifiers (e.g., `daily-report`) +- **`-n, --name`**: Used for human-readable names +- **`-p, --path`**: Used for file paths (legacy support) +- **`-o, --output`**: Used for output file paths +- **`-r, --resume` or `-r, --regex`**: Context-dependent (resume for sessions, regex for filters) +- **`-v, --verbose`**: Used for verbose output +- **`-l, --limit`**: Used for limiting result counts +- **`-f, --format`**: Used for specifying output formats +- **`-w, --working-dir`**: Used for working directory filters + ### Core Commands #### help @@ -79,8 +94,10 @@ goose update --reconfigure Start or resume interactive chat sessions. **Basic Options:** +- **`--session-id `**: Specify a session by its ID (e.g., '20250921_143022') - **`-n, --name `**: Give the session a name -- **`-r, --resume`**: Resume a previous session +- **`-p, --path `**: Legacy parameter for specifying session by file path +- **`-r, --resume`**: Resume a previous session - **`--debug`**: Enable debug mode to output complete tool responses, detailed parameter values, and full file paths - **`--max-turns `**: Set the maximum number of turns allowed without user input (default: 1000) @@ -93,11 +110,12 @@ Start or resume interactive chat sessions. **Usage:** ```bash # Start a basic session -goose session --name my-project +goose session -n my-project # Resume a previous session -goose session --resume --name my-project -goose session --resume --id 2025250620_013617 +goose session --resume -n my-project +goose session --resume --session-id 20250620_013617 +goose session --resume -p ./session.jsonl # Start with extensions goose session --with-extension "npx -y @modelcontextprotocol/server-memory" @@ -112,7 +130,7 @@ goose session \ --with-builtin "developer" # Control session behavior -goose session --name my-session --debug --max-turns 25 +goose session -n my-session --debug --max-turns 25 ``` --- @@ -146,14 +164,15 @@ goose session list --ascending Remove one or more saved sessions. **Options:** -- **`-i, --id `**: Remove a specific session by its ID +- **`--session-id `**: Remove a specific session by its session ID - **`-n, --name `**: Remove a specific session by its name - **`-r, --regex `**: Remove sessions matching a regex pattern **Usage:** ```bash # Remove a specific session by ID -goose session remove -i 20250305_113223 +goose session remove --session-id 20250305_113223 +goose session remove --id 20250305_113223 # Remove a specific session by its name goose session remove -n my-session @@ -175,9 +194,9 @@ Session removal is permanent and cannot be undone. Goose will show which session Export sessions in different formats for backup, sharing, migration, or documentation purposes. **Options:** -- **`-i, --id `**: Export a specific session by ID +- **`--session-id `**: Export a specific session by session ID - **`-n, --name `**: Export a specific session by name -- **`-p, --path `**: Export a specific session by file path +- **`-p, --path `**: Export a specific session by file path (legacy) - **`-o, --output `**: Save exported content to a file (default: stdout) - **`--format `**: Output format: `markdown`, `json`, `yaml`. Default is `markdown` @@ -192,17 +211,17 @@ Export sessions in different formats for backup, sharing, migration, or document goose session export # Export specific session as JSON for backup -goose session export --name my-session --format json --output session-backup.json +goose session export -n my-session --format json -o session-backup.json # Export specific session as readable markdown -goose session export --name my-session --output session.md +goose session export -n my-session -o session.md # Export to stdout in different formats -goose session export --id 20250305_113223 --format json -goose session export --name my-session --format yaml +goose session export --session-id 20250305_113223 --format json +goose session export -n my-session --format yaml -# Export session by path -goose session export --path ./my-session.jsonl --output exported.md +# Export session by path (legacy) +goose session export -p ./my-session.jsonl -o exported.md ``` --- @@ -211,7 +230,7 @@ goose session export --path ./my-session.jsonl --output exported.md Generate a comprehensive diagnostics bundle for troubleshooting issues with a specific session. **Options:** -- **`-i, --id `**: Generate diagnostics for a specific session by ID +- **`--session-id `**: Generate diagnostics for a specific session by ID - **`-n, --name `**: Generate diagnostics for a specific session by name - **`-o, --output `**: Save diagnostics bundle to a specific file path (default: `diagnostics_{session_id}.zip`) @@ -224,13 +243,13 @@ Generate a comprehensive diagnostics bundle for troubleshooting issues with a sp **Usage:** ```bash # Generate diagnostics for a specific session by ID -goose session diagnostics --id 20250305_113223 +goose session diagnostics --session-id 20250305_113223 # Generate diagnostics for a session by name goose session diagnostics --name my-project-session # Save diagnostics to a custom location -goose session diagnostics --id 20250305_113223 --output /path/to/my-diagnostics.zip +goose session diagnostics --session-id 20250305_113223 --output /path/to/my-diagnostics.zip # Interactive selection (prompts you to choose a session) goose session diagnostics @@ -366,29 +385,29 @@ Automate recipes by running them on a [schedule](/docs/guides/recipes/session-re - `run-now`: Run a scheduled recipe immediately **Options:** -- `--id `: A unique ID for the scheduled job (e.g. `daily-report`) +- `--schedule-id `: A unique ID for the scheduled job (e.g. `daily-report`) - `--cron "* * * * * *"`: Specifies when a job should run using a [cron expression](https://en.wikipedia.org/wiki/Cron#Cron_expression) - `--recipe-source `: Path to the recipe YAML file -- `--limit `: Max number of sessions to display when using the `sessions` command +- `-l, --limit `: Max number of sessions to display when using the `sessions` command **Usage:** ```bash goose schedule # Add a new scheduled recipe which runs every day at 9 AM -goose schedule add --id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml +goose schedule add --schedule-id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml # List all scheduled jobs goose schedule list # List the 10 most recent Goose sessions created by a scheduled job -goose schedule sessions --id daily-report --limit 10 +goose schedule sessions --schedule-id daily-report -l 10 # Run a recipe immediately -goose schedule run-now --id daily-report +goose schedule run-now --schedule-id daily-report # Remove a scheduled job -goose schedule remove --id daily-report +goose schedule remove --schedule-id daily-report ``` --- diff --git a/documentation/docs/guides/recipes/session-recipes.md b/documentation/docs/guides/recipes/session-recipes.md index 51f811bfd06e..8816c0b963af 100644 --- a/documentation/docs/guides/recipes/session-recipes.md +++ b/documentation/docs/guides/recipes/session-recipes.md @@ -533,7 +533,7 @@ At the bottom of the `Schedule Details` page you can view the list of sessions c ```bash # Add a new scheduled recipe which runs every day at 9 AM - goose schedule add --id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml + goose schedule add --schedule-id daily-report --cron "0 0 9 * * *" --recipe-source ./recipes/daily-report.yaml ``` You can use either a 5, 6, or 7-digit cron expression for full scheduling precision, following the format "seconds minutes hours day-of-month month day-of-week year". diff --git a/documentation/docs/troubleshooting/diagnostics-and-reporting.md b/documentation/docs/troubleshooting/diagnostics-and-reporting.md index 5b36565d90fd..1cd54216afe8 100644 --- a/documentation/docs/troubleshooting/diagnostics-and-reporting.md +++ b/documentation/docs/troubleshooting/diagnostics-and-reporting.md @@ -39,13 +39,13 @@ The diagnostics feature creates a comprehensive troubleshooting bundle that incl ```sh # Generate diagnostics for a specific session - goose session diagnostics --id + goose session diagnostics --session-id # Interactive selection (prompts you to choose a session) goose session diagnostics # Save to a custom location - goose session diagnostics --id --output /path/to/diagnostics.zip + goose session diagnostics --session-id --output /path/to/diagnostics.zip ``` To find your session ID, first list available sessions: