diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 77a1df05e9..745b4e9621 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1.0", features = ["full"] } futures = "0.3" serde = { version = "1.0", features = ["derive"] } # For serialization serde_yaml = "0.9" -dirs = "4.0" +etcetera = "0.8.0" reqwest = { version = "0.12.9", features = [ "rustls-tls", "json", @@ -46,13 +46,15 @@ tracing = "0.1" chrono = "0.4" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] } tracing-appender = "0.2" +once_cell = "1.20.2" winapi = { version = "0.3", features = ["wincred"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } + [dev-dependencies] tempfile = "3" temp-env = { version = "0.3.6", features = ["async_closure"] } test-case = "3.3" -tokio = { version = "1.0", features = ["rt", "macros"] } \ No newline at end of file +tokio = { version = "1.0", features = ["rt", "macros"] } diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 6c1e77c526..0273260a68 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -2,7 +2,7 @@ use rand::{distributions::Alphanumeric, Rng}; use std::process; use crate::prompt::rustyline::RustylinePrompt; -use crate::session::{ensure_session_dir, get_most_recent_session, Session}; +use crate::session::{ensure_session_dir, get_most_recent_session, legacy_session_dir, Session}; use console::style; use goose::agents::extension::{Envs, ExtensionError}; use goose::agents::AgentFactory; @@ -121,9 +121,18 @@ pub async fn build_session( if session_file.exists() { let prompt = Box::new(RustylinePrompt::new()); return Session::new(agent, prompt, session_file); - } else { - eprintln!("Session '{}' not found, starting new session", session_name); } + + // LEGACY NOTE: remove this once old paths are no longer needed. + if let Some(legacy_dir) = legacy_session_dir() { + let legacy_file = legacy_dir.join(format!("{}.jsonl", session_name)); + if legacy_file.exists() { + let prompt = Box::new(RustylinePrompt::new()); + return Session::new(agent, prompt, legacy_file); + } + } + + eprintln!("Session '{}' not found, starting new session", session_name); } else { // Try to resume most recent session if let Ok(session_file) = get_most_recent_session() { diff --git a/crates/goose-cli/src/log_usage.rs b/crates/goose-cli/src/log_usage.rs index a7235b7f76..d38b9bbd49 100644 --- a/crates/goose-cli/src/log_usage.rs +++ b/crates/goose-cli/src/log_usage.rs @@ -1,3 +1,4 @@ +use etcetera::{choose_app_strategy, AppStrategy}; use goose::providers::base::ProviderUsage; #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -13,8 +14,15 @@ pub fn log_usage(session_file: String, usage: Vec) { }; // Ensure log directory exists - if let Some(home_dir) = dirs::home_dir() { - let log_dir = home_dir.join(".config").join("goose").join("logs"); + if let Ok(home_dir) = choose_app_strategy(crate::APP_STRATEGY.clone()) { + // choose_app_strategy().state_dir() + // - macOS/Linux: ~/.local/state/goose/logs/ + // - Windows: ~\AppData\Roaming\Block\goose\data\logs + // - Windows has no convention for state_dir, use data_dir instead + let log_dir = home_dir + .in_state_dir("logs") + .unwrap_or_else(|| home_dir.in_data_dir("logs")); + if let Err(e) = std::fs::create_dir_all(&log_dir) { eprintln!("Failed to create log directory: {}", e); return; @@ -49,6 +57,7 @@ pub fn log_usage(session_file: String, usage: Vec) { #[cfg(test)] mod tests { + use etcetera::{choose_app_strategy, AppStrategy}; use goose::providers::base::{ProviderUsage, Usage}; use crate::{ @@ -59,11 +68,11 @@ mod tests { #[test] fn test_session_logging() { run_with_tmp_dir(|| { - let home_dir = dirs::home_dir().unwrap(); + let home_dir = choose_app_strategy(crate::APP_STRATEGY.clone()).unwrap(); + let log_file = home_dir - .join(".config") - .join("goose") - .join("logs") + .in_state_dir("logs") + .unwrap_or_else(|| home_dir.in_data_dir("logs")) .join("goose.log"); log_usage( diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index d63531630c..df79ebaa9e 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use etcetera::{choose_app_strategy, AppStrategy}; use std::fs; use std::path::PathBuf; use tracing_appender::rolling::Rotation; @@ -12,17 +13,16 @@ use goose::tracing::langfuse_layer; /// Returns the directory where log files should be stored. /// Creates the directory structure if it doesn't exist. fn get_log_directory() -> Result { - let home = if cfg!(windows) { - std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")? - } else { - std::env::var("HOME").context("HOME environment variable not set")? - }; - - let base_log_dir = PathBuf::from(home) - .join(".config") - .join("goose") - .join("logs") - .join("cli"); // Add cli-specific subdirectory + // choose_app_strategy().state_dir() + // - macOS/Linux: ~/.local/state/goose/logs/cli + // - Windows: ~\AppData\Roaming\Block\goose\data\logs\cli + // - Windows has no convention for state_dir, use data_dir instead + let home_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) + .context("HOME environment variable not set")?; + + let base_log_dir = home_dir + .in_state_dir("logs/cli") + .unwrap_or_else(|| home_dir.in_data_dir("logs/cli")); // Create date-based subdirectory let now = chrono::Local::now(); diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 04a69ca8ee..bfe4697777 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -1,5 +1,13 @@ use anyhow::Result; use clap::{CommandFactory, Parser, Subcommand}; +use etcetera::AppStrategyArgs; +use once_cell::sync::Lazy; + +pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { + top_level_domain: "Block".to_string(), + author: "Block".to_string(), + app_name: "goose".to_string(), +}); mod commands; mod log_usage; diff --git a/crates/goose-cli/src/prompt/renderer.rs b/crates/goose-cli/src/prompt/renderer.rs index a16fc3edd4..9d569fa912 100644 --- a/crates/goose-cli/src/prompt/renderer.rs +++ b/crates/goose-cli/src/prompt/renderer.rs @@ -30,8 +30,8 @@ fn shorten_path(path: &str) -> String { let path = PathBuf::from(path); // First try to convert to ~ if it's in home directory - let home = dirs::home_dir(); - let path_str = if let Some(home) = home { + let home = etcetera::home_dir(); + let path_str = if let Ok(home) = home { if let Ok(stripped) = path.strip_prefix(home) { format!("~/{}", stripped.display()) } else { diff --git a/crates/goose-cli/src/session.rs b/crates/goose-cli/src/session.rs index c180e32419..c48e67be3f 100644 --- a/crates/goose-cli/src/session.rs +++ b/crates/goose-cli/src/session.rs @@ -1,5 +1,6 @@ use anyhow::Result; use core::panic; +use etcetera::{choose_app_strategy, AppStrategy}; use futures::StreamExt; use std::fs::{self, File}; use std::io::{self, BufRead, Write}; @@ -14,8 +15,12 @@ use mcp_core::role::Role; // File management functions pub fn ensure_session_dir() -> Result { - let home_dir = dirs::home_dir().ok_or(anyhow::anyhow!("Could not determine home directory"))?; - let config_dir = home_dir.join(".config").join("goose").join("sessions"); + // choose_app_strategy().data_dir() + // - macOS/Linux: ~/.local/share/goose/sessions/ + // - Windows: ~\AppData\Roaming\Block\goose\data\sessions + let config_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) + .expect("goose requires a home dir") + .in_data_dir("sessions"); if !config_dir.exists() { fs::create_dir_all(&config_dir)?; @@ -24,6 +29,15 @@ pub fn ensure_session_dir() -> Result { Ok(config_dir) } +/// LEGACY NOTE: remove this once old paths are no longer needed. +pub fn legacy_session_dir() -> Option { + // legacy path was in the config dir ~/.config/goose/sessions/ + // ignore errors if we can't re-create the legacy session dir + choose_app_strategy(crate::APP_STRATEGY.clone()) + .map(|strategy| strategy.in_config_dir("sessions")) + .ok() +} + pub fn get_most_recent_session() -> Result { let session_dir = ensure_session_dir()?; let mut entries = fs::read_dir(&session_dir)? @@ -31,6 +45,19 @@ pub fn get_most_recent_session() -> Result { .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl")) .collect::>(); + // LEGACY NOTE: remove this once old paths are no longer needed. + if entries.is_empty() { + if let Some(old_dir) = legacy_session_dir() { + // okay to return the error via ?, since that means we have no sessions in the + // new location, and this old location doesn't exist, so a new session will be created + let old_entries = fs::read_dir(&old_dir)? + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl")) + .collect::>(); + entries.extend(old_entries); + } + } + if entries.is_empty() { return Err(anyhow::anyhow!("No session files found")); } diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index fc01d4d000..9b64c793f1 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -29,13 +29,14 @@ xcap = "0.0.14" reqwest = { version = "0.11", features = ["json", "rustls-tls"] , default-features = false} async-trait = "0.1" chrono = { version = "0.4.38", features = ["serde"] } -dirs = "5.0.1" +etcetera = "0.8.0" tempfile = "3.8" include_dir = "0.7.4" google-drive3 = "6.0.0" webbrowser = "0.8" http-body-util = "0.1.2" regex = "1.11.1" +once_cell = "1.20.2" [dev-dependencies] serial_test = "3.0.0" diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index b6885f91ad..365a0a3049 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -1,4 +1,5 @@ use base64::Engine; +use etcetera::{choose_app_strategy, AppStrategy}; use indoc::{formatdoc, indoc}; use reqwest::{Client, Url}; use serde_json::{json, Value}; @@ -216,11 +217,13 @@ impl ComputerControllerRouter { }), ); - // Create cache directory in user's home directory - let cache_dir = dirs::cache_dir() - .unwrap_or_else(|| create_system_automation().get_temp_path()) - .join("goose") - .join("computer_controller"); + // choose_app_strategy().cache_dir() + // - macOS/Linux: ~/.cache/goose/computer_controller/ + // - Windows: ~\AppData\Local\Block\goose\cache\computer_controller\ + // keep previous behavior of defaulting to /tmp/ + let cache_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) + .map(|strategy| strategy.in_cache_dir("computer_controller")) + .unwrap_or_else(|_| create_system_automation().get_temp_path()); fs::create_dir_all(&cache_dir).unwrap_or_else(|_| { println!( diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index f85096a2da..398e33178b 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -3,6 +3,7 @@ mod shell; use anyhow::Result; use base64::Engine; +use etcetera::{choose_app_strategy, AppStrategy}; use indoc::formatdoc; use serde_json::{json, Value}; use std::{ @@ -278,9 +279,16 @@ impl DeveloperRouter { }, }; - // Check for global hints in ~/.config/goose/.goosehints - let global_hints_path = - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()); + // choose_app_strategy().config_dir() + // - macOS/Linux: ~/.config/goose/ + // - Windows: ~\AppData\Roaming\Block\goose\config\ + // keep previous behavior of expanding ~/.config in case this fails + let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone()) + .map(|strategy| strategy.in_config_dir(".goosehints")) + .unwrap_or_else(|_| { + PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()) + }); + // Create the directory if it doesn't exist let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap()); diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index 6ca2cf6dd2..f966cf55af 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -1,3 +1,12 @@ +use etcetera::AppStrategyArgs; +use once_cell::sync::Lazy; + +pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { + top_level_domain: "Block".to_string(), + author: "Block".to_string(), + app_name: "goose".to_string(), +}); + mod computercontroller; mod developer; mod google_drive; diff --git a/crates/goose-mcp/src/memory/mod.rs b/crates/goose-mcp/src/memory/mod.rs index 2c7a6f3f16..4a7411a54c 100644 --- a/crates/goose-mcp/src/memory/mod.rs +++ b/crates/goose-mcp/src/memory/mod.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use etcetera::{choose_app_strategy, AppStrategy}; use indoc::formatdoc; use serde_json::{json, Value}; use std::{ @@ -178,10 +179,13 @@ impl MemoryRouter { .join(".goose") .join("memory"); - // Check for .config/goose/memory in user's home directory - let global_memory_dir = dirs::home_dir() - .map(|home| home.join(".config/goose/memory")) - .unwrap_or_else(|| PathBuf::from(".config/goose/memory")); + // choose_app_strategy().config_dir() + // - macOS/Linux: ~/.config/goose/memory/ + // - Windows: ~\AppData\Roaming\Block\goose\config\memory + // if it fails, fall back to `.config/goose/memory` (relative to the current dir) + let global_memory_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) + .map(|strategy| strategy.in_config_dir("memory")) + .unwrap_or_else(|_| PathBuf::from(".config/goose/memory")); fs::create_dir_all(&global_memory_dir).unwrap(); fs::create_dir_all(&local_memory_dir).unwrap(); diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 0bbb8fd9a0..6f593b172a 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -29,7 +29,8 @@ http = "1.0" config = { version = "0.14.1", features = ["toml"] } thiserror = "1.0" clap = { version = "4.4", features = ["derive"] } -once_cell = "1.18" +once_cell = "1.20.2" +etcetera = "0.8.0" [[bin]] name = "goosed" @@ -37,4 +38,4 @@ path = "src/main.rs" [dev-dependencies] tower = "0.5" -async-trait = "0.1" \ No newline at end of file +async-trait = "0.1" diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index dc4ea80beb..c080296481 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use etcetera::{choose_app_strategy, AppStrategy}; use std::fs; use std::path::PathBuf; use tracing_appender::rolling::Rotation; @@ -12,17 +13,16 @@ use goose::tracing::langfuse_layer; /// Returns the directory where log files should be stored. /// Creates the directory structure if it doesn't exist. fn get_log_directory() -> Result { - let home = if cfg!(windows) { - std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")? - } else { - std::env::var("HOME").context("HOME environment variable not set")? - }; + // choose_app_strategy().state_dir() + // - macOS/Linux: ~/.local/state/goose/logs/server + // - Windows: ~\AppData\Roaming\Block\goose\data\logs\server + // - Windows has no convention for state_dir, use data_dir instead + let home_dir = choose_app_strategy(crate::APP_STRATEGY.clone()) + .context("HOME environment variable not set")?; - let base_log_dir = PathBuf::from(home) - .join(".config") - .join("goose") - .join("logs") - .join("server"); // Add server-specific subdirectory + let base_log_dir = home_dir + .in_state_dir("logs/server") + .unwrap_or_else(|| home_dir.in_data_dir("logs/server")); // Create date-based subdirectory let now = chrono::Local::now(); diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index 4647c75561..44c631f9b7 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -1,3 +1,12 @@ +use etcetera::AppStrategyArgs; +use once_cell::sync::Lazy; + +pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { + top_level_domain: "Block".to_string(), + author: "Block".to_string(), + app_name: "goose".to_string(), +}); + mod commands; mod configuration; mod error; diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 4c6135ac7e..3d0d4a7b28 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -58,7 +58,7 @@ ctor = "0.2.7" paste = "1.0" serde_yaml = "0.9.34" once_cell = "1.20.2" -dirs = "6.0.0" +etcetera = "0.8.0" rand = "0.8.5" # For Bedrock provider diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 6e305b647e..e2dc1ab780 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1,5 +1,6 @@ +use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; use keyring::Entry; -use once_cell::sync::OnceCell; +use once_cell::sync::{Lazy, OnceCell}; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; @@ -7,6 +8,12 @@ use std::env; use std::path::{Path, PathBuf}; use thiserror::Error; +pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { + top_level_domain: "Block".to_string(), + author: "Block".to_string(), + app_name: "goose".to_string(), +}); + const KEYRING_SERVICE: &str = "goose"; const KEYRING_USERNAME: &str = "secrets"; @@ -99,10 +106,13 @@ static GLOBAL_CONFIG: OnceCell = OnceCell::new(); impl Default for Config { fn default() -> Self { - let config_dir = dirs::home_dir() + // choose_app_strategy().config_dir() + // - macOS/Linux: ~/.config/goose/ + // - Windows: ~\AppData\Roaming\Block\goose\config\ + let config_dir = choose_app_strategy(APP_STRATEGY.clone()) .expect("goose requires a home dir") - .join(".config") - .join("goose"); + .config_dir(); + std::fs::create_dir_all(&config_dir).expect("Failed to create config directory"); let config_path = config_dir.join("config.yaml"); diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index fdaa8a864d..c72669a713 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -2,5 +2,5 @@ mod base; mod extensions; pub use crate::agents::ExtensionConfig; -pub use base::{Config, ConfigError}; +pub use base::{Config, ConfigError, APP_STRATEGY}; pub use extensions::{ExtensionEntry, ExtensionManager}; diff --git a/crates/goose/src/providers/oauth.rs b/crates/goose/src/providers/oauth.rs index 846f7a722b..15f5c59f8f 100644 --- a/crates/goose/src/providers/oauth.rs +++ b/crates/goose/src/providers/oauth.rs @@ -2,6 +2,7 @@ use anyhow::Result; use axum::{extract::Query, response::Html, routing::get, Router}; use base64::Engine; use chrono::{DateTime, Utc}; +use etcetera::{choose_app_strategy, AppStrategy}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -31,13 +32,12 @@ struct TokenCache { } fn get_base_path() -> PathBuf { - const BASE_PATH: &str = ".config/goose/databricks/oauth"; - let home_dir = if cfg!(windows) { - std::env::var("USERPROFILE").expect("USERPROFILE environment variable not set") - } else { - std::env::var("HOME").expect("HOME environment variable not set") - }; - PathBuf::from(home_dir).join(BASE_PATH) + // choose_app_strategy().config_dir() + // - macOS/Linux: ~/.config/goose/databricks/oauth + // - Windows: ~\AppData\Roaming\Block\goose\config\databricks\oauth\ + choose_app_strategy(crate::config::APP_STRATEGY.clone()) + .expect("goose requires a home dir") + .in_config_dir("databricks/oauth") } impl TokenCache {