Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 63 additions & 39 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,24 @@ 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<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(
short,
long,
value_name = "PATH",
help = "Legacy: Path for the chat session",
Expand Down Expand Up @@ -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"
)]
Expand All @@ -197,16 +197,15 @@ enum SessionCommand {
#[arg(short = 'l', long = "limit", help = "Limit the number of results")]
limit: Option<usize>,
},
#[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<Identifier>,
#[arg(
short,
short = 'r',
long,
alias = "name",
help = "Session ID to be removed (optional)"
help = "Regex for removing matched sessions (optional)"
)]
id: Option<String>,
#[arg(short, long, help = "Regex for removing matched sessions (optional)")]
regex: Option<String>,
},
#[command(about = "Export a session")]
Expand All @@ -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<Identifier>,

/// Output path for the diagnostics zip file (optional, defaults to current directory)
#[arg(short, long)]
#[arg(short = 'o', long)]
output: Option<PathBuf>,
},
}
Expand All @@ -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",
Expand All @@ -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<usize>,
},
#[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")]
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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?;
Expand Down
56 changes: 33 additions & 23 deletions crates/goose-cli/src/commands/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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(())
}
Expand All @@ -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)),
}
}
}
Expand Down Expand Up @@ -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<usize>) -> Result<()> {
pub async fn handle_schedule_sessions(schedule_id: String, limit: Option<usize>) -> 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!(
Expand All @@ -227,31 +233,35 @@ pub async fn handle_schedule_sessions(id: String, limit: Option<usize>) -> 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(())
Expand Down Expand Up @@ -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(())
}
Loading
Loading