feat(cli): support arbitrary path for sessions (#1414)

This commit is contained in:
Ariel
2025-02-27 18:31:14 +08:00
committed by GitHub
parent 234d55ea37
commit a59535627a
4 changed files with 88 additions and 38 deletions

View File

@@ -1,15 +1,16 @@
use anyhow::Result; use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand}; use clap::{Args, CommandFactory, Parser, Subcommand};
use console::style; use console::style;
use goose::config::Config; use goose::config::Config;
use goose_cli::commands::agent_version::AgentCommand;
use goose_cli::commands::configure::handle_configure; use goose_cli::commands::configure::handle_configure;
use goose_cli::commands::info::handle_info; use goose_cli::commands::info::handle_info;
use goose_cli::commands::mcp::run_server; use goose_cli::commands::mcp::run_server;
use goose_cli::logging::setup_logging; use goose_cli::logging::setup_logging;
use goose_cli::session::build_session; use goose_cli::session::build_session;
use goose_cli::{commands::agent_version::AgentCommand, session};
use std::io::{self, Read}; use std::io::{self, Read};
use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, display_name = "", about, long_about = None)] #[command(author, version, display_name = "", about, long_about = None)]
@@ -18,6 +19,38 @@ struct Cli {
command: Option<Command>, command: Option<Command>,
} }
#[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<String>,
#[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<PathBuf>,
}
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)] #[derive(Subcommand)]
enum Command { enum Command {
/// Configure Goose settings /// Configure Goose settings
@@ -42,22 +75,16 @@ enum Command {
visible_alias = "s" visible_alias = "s"
)] )]
Session { Session {
/// Name for the chat session /// Identifier for the chat session
#[arg( #[command(flatten)]
short, identifier: Option<Identifier>,
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<String>,
/// Resume a previous session /// Resume a previous session
#[arg( #[arg(
short, short,
long, long,
help = "Resume a previous session (last used or specified by --name)", 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, resume: bool,
@@ -114,15 +141,9 @@ enum Command {
)] )]
interactive: bool, interactive: bool,
/// Name for this run session /// Identifier for this run session
#[arg( #[command(flatten)]
short, identifier: Option<Identifier>,
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<String>,
/// Resume a previous run /// Resume a previous run
#[arg( #[arg(
@@ -200,12 +221,18 @@ async fn main() -> Result<()> {
let _ = run_server(&name).await; let _ = run_server(&name).await;
} }
Some(Command::Session { Some(Command::Session {
name, identifier,
resume, resume,
extension, extension,
builtin, 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()))?; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?;
let _ = session.interactive(None).await; let _ = session.interactive(None).await;
return Ok(()); return Ok(());
@@ -214,7 +241,7 @@ async fn main() -> Result<()> {
instructions, instructions,
input_text, input_text,
interactive, interactive,
name, identifier,
resume, resume,
extension, extension,
builtin, builtin,
@@ -237,7 +264,13 @@ async fn main() -> Result<()> {
.expect("Failed to read from stdin"); .expect("Failed to read from stdin");
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()))?; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?;
if interactive { if interactive {

View File

@@ -11,7 +11,7 @@ use super::storage;
use super::Session; use super::Session;
pub async fn build_session( pub async fn build_session(
name: Option<String>, identifier: Option<storage::Identifier>,
resume: bool, resume: bool,
extensions: Vec<String>, extensions: Vec<String>,
builtins: Vec<String>, builtins: Vec<String>,
@@ -22,7 +22,6 @@ pub async fn build_session(
let provider_name: String = config let provider_name: String = config
.get("GOOSE_PROVIDER") .get("GOOSE_PROVIDER")
.expect("No provider configured. Run 'goose configure' first"); .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 let model: String = config
.get("GOOSE_MODEL") .get("GOOSE_MODEL")
@@ -65,13 +64,12 @@ pub async fn build_session(
// Handle session file resolution and resuming // Handle session file resolution and resuming
let session_file = if resume { let session_file = if resume {
if let Some(ref session_name) = name { if let Some(identifier) = identifier {
// Try to resume specific named session let session_file = storage::get_path(identifier);
let session_file = session_dir.join(format!("{}.jsonl", session_name));
if !session_file.exists() { if !session_file.exists() {
output::render_error(&format!( output::render_error(&format!(
"Cannot resume session {} - no such session exists", "Cannot resume session {} - no such session exists",
style(session_name).cyan() style(session_file.display()).cyan()
)); ));
process::exit(1); process::exit(1);
} }
@@ -87,9 +85,13 @@ pub async fn build_session(
} }
} }
} else { } else {
// Create new session with provided or generated name // Create new session with provided name/path or generated name
let session_name = name.unwrap_or_else(generate_session_name); let id = match identifier {
create_new_session_file(&session_dir, &session_name) 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 // Create new session
@@ -138,10 +140,9 @@ fn generate_session_name() -> String {
.collect() .collect()
} }
fn create_new_session_file(session_dir: &std::path::Path, name: &str) -> PathBuf { fn create_new_session_file(session_file: PathBuf) -> PathBuf {
let session_file = session_dir.join(format!("{}.jsonl", name));
if session_file.exists() { if session_file.exists() {
eprintln!("Session '{}' already exists", name); eprintln!("Session '{:?}' already exists", session_file);
process::exit(1); process::exit(1);
} }
session_file session_file

View File

@@ -6,6 +6,7 @@ mod storage;
mod thinking; mod thinking;
pub use builder::build_session; pub use builder::build_session;
pub use storage::Identifier;
use anyhow::Result; use anyhow::Result;
use etcetera::choose_app_strategy; use etcetera::choose_app_strategy;

View File

@@ -5,6 +5,21 @@ use std::fs::{self, File};
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf}; 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 /// Ensure the session directory exists and return its path
pub fn ensure_session_dir() -> Result<PathBuf> { pub fn ensure_session_dir() -> Result<PathBuf> {
let data_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) let data_dir = choose_app_strategy(crate::APP_STRATEGY.clone())
@@ -71,7 +86,7 @@ pub fn read_messages(session_file: &Path) -> Result<Vec<Message>> {
/// ///
/// Overwrites the file with all messages in JSONL format. /// Overwrites the file with all messages in JSONL format.
pub fn persist_messages(session_file: &Path, messages: &[Message]) -> Result<()> { 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); let mut writer = io::BufWriter::new(file);
for message in messages { for message in messages {