diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index f385b750..cd8fd254 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod bench; pub mod configure; pub mod info; pub mod mcp; +pub mod session; pub mod update; diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs new file mode 100644 index 00000000..66e9cbea --- /dev/null +++ b/crates/goose-cli/src/commands/session.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use goose::session::info::{get_session_info, SessionInfo}; + +pub fn handle_session_list(verbose: bool, format: String) -> Result<()> { + let sessions = match get_session_info() { + Ok(sessions) => sessions, + Err(e) => { + tracing::error!("Failed to list sessions: {:?}", e); + return Err(anyhow::anyhow!("Failed to list sessions")); + } + }; + + match format.as_str() { + "json" => { + println!("{}", serde_json::to_string(&sessions)?); + } + _ => { + if sessions.is_empty() { + println!("No sessions found"); + return Ok(()); + } else { + println!("Available sessions:"); + for SessionInfo { + id, + path, + metadata, + modified, + } in sessions + { + let description = if metadata.description.is_empty() { + "(none)" + } else { + &metadata.description + }; + let output = format!("{} - {} - {}", id, description, modified); + if verbose { + println!(" {}", output); + println!(" Path: {}", path); + } else { + println!("{}", output); + } + } + } + } + } + Ok(()) +} diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index cd9473fc..ba533f06 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -8,6 +8,7 @@ use goose_cli::commands::bench::{list_suites, run_benchmark}; use goose_cli::commands::configure::handle_configure; use goose_cli::commands::info::handle_info; use goose_cli::commands::mcp::run_server; +use goose_cli::commands::session::handle_session_list; use goose_cli::logging::setup_logging; use goose_cli::session; use goose_cli::session::build_session; @@ -53,6 +54,23 @@ fn extract_identifier(identifier: Identifier) -> session::Identifier { } } +#[derive(Subcommand)] +enum SessionCommand { + #[command(about = "List all available sessions")] + List { + #[arg(short, long, help = "List all available sessions")] + verbose: bool, + + #[arg( + short, + long, + help = "Output format (text, json)", + default_value = "text" + )] + format: String, + }, +} + #[derive(Subcommand)] enum Command { /// Configure Goose settings @@ -77,6 +95,8 @@ enum Command { visible_alias = "s" )] Session { + #[command(subcommand)] + command: Option, /// Identifier for the chat session #[command(flatten)] identifier: Option, @@ -299,27 +319,36 @@ async fn main() -> Result<()> { let _ = run_server(&name).await; } Some(Command::Session { + command, identifier, resume, debug, extension, builtin, }) => { - let mut session = build_session( - identifier.map(extract_identifier), - resume, - extension, - builtin, - debug, - ) - .await; - - setup_logging( - session.session_file().file_stem().and_then(|s| s.to_str()), - None, - )?; - let _ = session.interactive(None).await; - return Ok(()); + match command { + Some(SessionCommand::List { verbose, format }) => { + handle_session_list(verbose, format)?; + return Ok(()); + } + None => { + // Run session command by default + let mut session = build_session( + identifier.map(extract_identifier), + resume, + extension, + builtin, + debug, + ) + .await; + setup_logging( + session.session_file().file_stem().and_then(|s| s.to_str()), + None, + )?; + let _ = session.interactive(None).await; + return Ok(()); + } + } } Some(Command::Run { instructions, diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 0d154ae0..e81c1f2a 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -7,16 +7,9 @@ use axum::{ }; use goose::message::Message; use goose::session; +use goose::session::info::{get_session_info, SessionInfo}; use serde::Serialize; -#[derive(Serialize)] -struct SessionInfo { - id: String, - path: String, - modified: String, - metadata: session::SessionMetadata, -} - #[derive(Serialize)] struct SessionListResponse { sessions: Vec, @@ -44,43 +37,9 @@ async fn list_sessions( return Err(StatusCode::UNAUTHORIZED); } - let sessions = match session::list_sessions() { - Ok(sessions) => sessions, - Err(e) => { - tracing::error!("Failed to list sessions: {:?}", e); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; + let sessions = get_session_info().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let session_infos = sessions - .into_iter() - .map(|(id, path)| { - // Get last modified time as string - let modified = path - .metadata() - .and_then(|m| m.modified()) - .map(|time| { - chrono::DateTime::::from(time) - .format("%Y-%m-%d %H:%M:%S UTC") - .to_string() - }) - .unwrap_or_else(|_| "Unknown".to_string()); - - // Get session description - let metadata = session::read_metadata(&path).expect("Failed to read session metadata"); - - SessionInfo { - id, - path: path.to_string_lossy().to_string(), - modified, - metadata, - } - }) - .collect(); - - Ok(Json(SessionListResponse { - sessions: session_infos, - })) + Ok(Json(SessionListResponse { sessions })) } // Get a specific session's history diff --git a/crates/goose/src/session/info.rs b/crates/goose/src/session/info.rs new file mode 100644 index 00000000..1259aa3d --- /dev/null +++ b/crates/goose/src/session/info.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use serde::Serialize; + +use crate::session::{self, SessionMetadata}; + +#[derive(Serialize)] +pub struct SessionInfo { + pub id: String, + pub path: String, + pub modified: String, + pub metadata: SessionMetadata, +} + +pub fn get_session_info() -> Result> { + let sessions = match session::list_sessions() { + Ok(sessions) => sessions, + Err(e) => { + tracing::error!("Failed to list sessions: {:?}", e); + return Err(anyhow::anyhow!("Failed to list sessions")); + } + }; + let session_infos = sessions + .into_iter() + .map(|(id, path)| { + // Get last modified time as string + let modified = path + .metadata() + .and_then(|m| m.modified()) + .map(|time| { + chrono::DateTime::::from(time) + .format("%Y-%m-%d %H:%M:%S UTC") + .to_string() + }) + .unwrap_or_else(|_| "Unknown".to_string()); + + // Get session description + let metadata = session::read_metadata(&path).expect("Failed to read session metadata"); + + SessionInfo { + id, + path: path.to_string_lossy().to_string(), + modified, + metadata, + } + }) + .collect(); + + Ok(session_infos) +} diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 398d4e6a..51d8957d 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -1,3 +1,4 @@ +pub mod info; pub mod storage; // Re-export common session types and functions @@ -6,3 +7,5 @@ pub use storage::{ get_path, list_sessions, persist_messages, read_messages, read_metadata, update_metadata, Identifier, SessionMetadata, }; + +pub use info::{get_session_info, SessionInfo};