diff --git a/Cargo.lock b/Cargo.lock index 22f21f8a..cc846d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2486,6 +2486,7 @@ dependencies = [ "mcp-server", "once_cell", "rand", + "regex", "reqwest 0.12.12", "rustyline", "serde", diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 765834b7..698d0d15 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -51,6 +51,7 @@ once_cell = "1.20.2" shlex = "1.3.0" async-trait = "0.1.86" base64 = "0.22.1" +regex = "1.11.1" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 5c13be00..bb168d11 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -8,7 +8,7 @@ use crate::commands::configure::handle_configure; use crate::commands::info::handle_info; use crate::commands::mcp::run_server; use crate::commands::recipe::{handle_deeplink, handle_validate}; -use crate::commands::session::handle_session_list; +use crate::commands::session::{handle_session_list, handle_session_remove}; use crate::logging::setup_logging; use crate::recipe::load_recipe; use crate::session; @@ -74,6 +74,18 @@ enum SessionCommand { )] format: String, }, + #[command(about = "Remove sessions")] + Remove { + #[arg(short, long, help = "session id to be removed", default_value = "")] + id: String, + #[arg( + short, + long, + help = "regex for removing matched session", + default_value = "" + )] + regex: String, + }, } #[derive(Subcommand)] @@ -385,6 +397,10 @@ pub async fn cli() -> Result<()> { handle_session_list(verbose, format)?; Ok(()) } + Some(SessionCommand::Remove { id, regex }) => { + handle_session_remove(id, regex)?; + return Ok(()); + } None => { // Run session command by default let mut session = build_session(SessionBuilderConfig { diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 66e9cbea..f09b8184 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -1,5 +1,61 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use goose::session::info::{get_session_info, SessionInfo}; +use regex::Regex; +use std::fs; + +pub fn remove_session(session: &SessionInfo) -> Result<()> { + let should_delete = cliclack::confirm(format!( + "Are you sure you want to delete session `{}`? (yes/no):?", + session.id + )) + .initial_value(true) + .interact()?; + if should_delete { + fs::remove_file(session.path.clone()) + .with_context(|| format!("Failed to remove session file '{}'", session.path))?; + println!("Session `{}` removed.", session.id); + } else { + println!("Skipping deletion of '{}'.", session.id); + } + Ok(()) +} + +pub fn handle_session_remove(id: String, regex_string: String) -> Result<()> { + let sessions = match get_session_info() { + Ok(sessions) => sessions, + Err(e) => { + tracing::error!("Failed to remove sessions: {:?}", e); + return Err(anyhow::anyhow!("Failed to remove sessions")); + } + }; + if !id.is_empty() { + if let Some(session_to_remove) = sessions.iter().find(|s| s.id == id) { + remove_session(session_to_remove)?; + } else { + return Err(anyhow::anyhow!("Session '{}' not found.", id)); + } + } else if !regex_string.is_empty() { + let session_regex: Regex = Regex::new(regex_string.as_str())?; + let mut removed_count = 0; + for session_info in sessions { + if session_regex.is_match(session_info.id.as_str()) { + remove_session(&session_info)?; + removed_count += 1; + } + } + if removed_count == 0 { + println!( + "Regex string '{}' does not match any sessions", + regex_string + ); + } + } else { + return Err(anyhow::anyhow!( + "Neither --regex nor --session-name flags provided." + )); + } + Ok(()) +} pub fn handle_session_list(verbose: bool, format: String) -> Result<()> { let sessions = match get_session_info() {