mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 22:54:24 +01:00
feat(cli): support arbitrary path for sessions (#1414)
This commit is contained in:
@@ -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<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)]
|
||||
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<String>,
|
||||
/// Identifier for the chat session
|
||||
#[command(flatten)]
|
||||
identifier: Option<Identifier>,
|
||||
|
||||
/// 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<String>,
|
||||
/// Identifier for this run session
|
||||
#[command(flatten)]
|
||||
identifier: Option<Identifier>,
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::storage;
|
||||
use super::Session;
|
||||
|
||||
pub async fn build_session(
|
||||
name: Option<String>,
|
||||
identifier: Option<storage::Identifier>,
|
||||
resume: bool,
|
||||
extensions: Vec<String>,
|
||||
builtins: Vec<String>,
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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.
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user