From a59535627ab14851e15a412d0200a9cd68f88ee7 Mon Sep 17 00:00:00 2001 From: Ariel Date: Thu, 27 Feb 2025 18:31:14 +0800 Subject: [PATCH] feat(cli): support arbitrary path for sessions (#1414) --- crates/goose-cli/src/main.rs | 83 +++++++++++++++++-------- crates/goose-cli/src/session/builder.rs | 25 ++++---- crates/goose-cli/src/session/mod.rs | 1 + crates/goose-cli/src/session/storage.rs | 17 ++++- 4 files changed, 88 insertions(+), 38 deletions(-) diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 2fb39dbc..8c3f1300 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -1,15 +1,16 @@ use anyhow::Result; -use clap::{CommandFactory, Parser, Subcommand}; +use clap::{Args, CommandFactory, Parser, Subcommand}; use console::style; use goose::config::Config; -use goose_cli::commands::agent_version::AgentCommand; use goose_cli::commands::configure::handle_configure; use goose_cli::commands::info::handle_info; use goose_cli::commands::mcp::run_server; use goose_cli::logging::setup_logging; use goose_cli::session::build_session; +use goose_cli::{commands::agent_version::AgentCommand, session}; use std::io::{self, Read}; +use std::path::PathBuf; #[derive(Parser)] #[command(author, version, display_name = "", about, long_about = None)] @@ -18,6 +19,38 @@ struct Cli { command: Option, } +#[derive(Args)] +#[group(required = false, multiple = false)] +struct Identifier { + #[arg( + short, + 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." + )] + name: Option, + + #[arg( + short, + long, + value_name = "PATH", + help = "Path for the chat session (e.g., './playground.jsonl')", + long_help = "Specify a path for your chat session. When used with --resume, will resume this specific session if it exists." + )] + path: Option, +} + +fn extract_identifier(identifier: Identifier) -> session::Identifier { + if let Some(name) = identifier.name { + session::Identifier::Name(name) + } else if let Some(path) = identifier.path { + session::Identifier::Path(path) + } else { + unreachable!() + } +} + #[derive(Subcommand)] enum Command { /// Configure Goose settings @@ -42,22 +75,16 @@ enum Command { visible_alias = "s" )] Session { - /// Name for the chat session - #[arg( - short, - 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." - )] - name: Option, + /// Identifier for the chat session + #[command(flatten)] + identifier: Option, /// Resume a previous session #[arg( short, long, help = "Resume a previous session (last used or specified by --name)", - long_help = "Continue from a previous chat session. If --name is provided, resumes that specific session. Otherwise resumes the last used session." + long_help = "Continue from a previous chat session. If --name or --path is provided, resumes that specific session. Otherwise resumes the last used session." )] resume: bool, @@ -114,15 +141,9 @@ enum Command { )] interactive: bool, - /// Name for this run session - #[arg( - short, - long, - value_name = "NAME", - help = "Name for this run session (e.g., 'daily-tasks')", - long_help = "Specify a name for this run session. This helps identify and resume specific runs later." - )] - name: Option, + /// Identifier for this run session + #[command(flatten)] + identifier: Option, /// Resume a previous run #[arg( @@ -200,12 +221,18 @@ async fn main() -> Result<()> { let _ = run_server(&name).await; } Some(Command::Session { - name, + identifier, resume, extension, builtin, }) => { - let mut session = build_session(name, resume, extension, builtin).await; + let mut session = build_session( + identifier.map(extract_identifier), + resume, + extension, + builtin, + ) + .await; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?; let _ = session.interactive(None).await; return Ok(()); @@ -214,7 +241,7 @@ async fn main() -> Result<()> { instructions, input_text, interactive, - name, + identifier, resume, extension, builtin, @@ -237,7 +264,13 @@ async fn main() -> Result<()> { .expect("Failed to read from stdin"); stdin }; - let mut session = build_session(name, resume, extension, builtin).await; + let mut session = build_session( + identifier.map(extract_identifier), + resume, + extension, + builtin, + ) + .await; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?; if interactive { diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 642bc26d..a709d149 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -11,7 +11,7 @@ use super::storage; use super::Session; pub async fn build_session( - name: Option, + identifier: Option, resume: bool, extensions: Vec, builtins: Vec, @@ -22,7 +22,6 @@ pub async fn build_session( let provider_name: String = config .get("GOOSE_PROVIDER") .expect("No provider configured. Run 'goose configure' first"); - let session_dir = storage::ensure_session_dir().expect("Failed to create session directory"); let model: String = config .get("GOOSE_MODEL") @@ -65,13 +64,12 @@ pub async fn build_session( // Handle session file resolution and resuming let session_file = if resume { - if let Some(ref session_name) = name { - // Try to resume specific named session - let session_file = session_dir.join(format!("{}.jsonl", session_name)); + if let Some(identifier) = identifier { + let session_file = storage::get_path(identifier); if !session_file.exists() { output::render_error(&format!( "Cannot resume session {} - no such session exists", - style(session_name).cyan() + style(session_file.display()).cyan() )); process::exit(1); } @@ -87,9 +85,13 @@ pub async fn build_session( } } } else { - // Create new session with provided or generated name - let session_name = name.unwrap_or_else(generate_session_name); - create_new_session_file(&session_dir, &session_name) + // Create new session with provided name/path or generated name + let id = match identifier { + Some(identifier) => identifier, + None => storage::Identifier::Name(generate_session_name()), + }; + let session_file = storage::get_path(id); + create_new_session_file(session_file) }; // Create new session @@ -138,10 +140,9 @@ fn generate_session_name() -> String { .collect() } -fn create_new_session_file(session_dir: &std::path::Path, name: &str) -> PathBuf { - let session_file = session_dir.join(format!("{}.jsonl", name)); +fn create_new_session_file(session_file: PathBuf) -> PathBuf { if session_file.exists() { - eprintln!("Session '{}' already exists", name); + eprintln!("Session '{:?}' already exists", session_file); process::exit(1); } session_file diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 575517df..88b68d0c 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -6,6 +6,7 @@ mod storage; mod thinking; pub use builder::build_session; +pub use storage::Identifier; use anyhow::Result; use etcetera::choose_app_strategy; diff --git a/crates/goose-cli/src/session/storage.rs b/crates/goose-cli/src/session/storage.rs index baf502ca..dc71435d 100644 --- a/crates/goose-cli/src/session/storage.rs +++ b/crates/goose-cli/src/session/storage.rs @@ -5,6 +5,21 @@ use std::fs::{self, File}; use std::io::{self, BufRead, Write}; use std::path::{Path, PathBuf}; +pub enum Identifier { + Name(String), + Path(PathBuf), +} + +pub fn get_path(id: Identifier) -> PathBuf { + match id { + Identifier::Name(name) => { + let session_dir = ensure_session_dir().expect("Failed to create session directory"); + session_dir.join(format!("{}.jsonl", name)) + } + Identifier::Path(path) => path, + } +} + /// Ensure the session directory exists and return its path pub fn ensure_session_dir() -> Result { let data_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) @@ -71,7 +86,7 @@ pub fn read_messages(session_file: &Path) -> Result> { /// /// Overwrites the file with all messages in JSONL format. pub fn persist_messages(session_file: &Path, messages: &[Message]) -> Result<()> { - let file = File::create(session_file)?; + let file = File::create(session_file).expect("The path specified does not exist"); let mut writer = io::BufWriter::new(file); for message in messages {