feat: follow XDG spec on linux/mac and use windows known folders for config and logs (#1153)

This commit is contained in:
Kalvin C
2025-02-11 08:47:28 -08:00
committed by GitHub
parent 01aeeaf413
commit 54af4c914c
19 changed files with 163 additions and 63 deletions

View File

@@ -27,7 +27,7 @@ tokio = { version = "1.0", features = ["full"] }
futures = "0.3" futures = "0.3"
serde = { version = "1.0", features = ["derive"] } # For serialization serde = { version = "1.0", features = ["derive"] } # For serialization
serde_yaml = "0.9" serde_yaml = "0.9"
dirs = "4.0" etcetera = "0.8.0"
reqwest = { version = "0.12.9", features = [ reqwest = { version = "0.12.9", features = [
"rustls-tls", "rustls-tls",
"json", "json",
@@ -46,11 +46,13 @@ tracing = "0.1"
chrono = "0.4" chrono = "0.4"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] }
tracing-appender = "0.2" tracing-appender = "0.2"
once_cell = "1.20.2"
winapi = { version = "0.3", features = ["wincred"], optional = true } winapi = { version = "0.3", features = ["wincred"], optional = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] } winapi = { version = "0.3", features = ["wincred"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
temp-env = { version = "0.3.6", features = ["async_closure"] } temp-env = { version = "0.3.6", features = ["async_closure"] }

View File

@@ -2,7 +2,7 @@ use rand::{distributions::Alphanumeric, Rng};
use std::process; use std::process;
use crate::prompt::rustyline::RustylinePrompt; 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 console::style;
use goose::agents::extension::{Envs, ExtensionError}; use goose::agents::extension::{Envs, ExtensionError};
use goose::agents::AgentFactory; use goose::agents::AgentFactory;
@@ -121,9 +121,18 @@ pub async fn build_session(
if session_file.exists() { if session_file.exists() {
let prompt = Box::new(RustylinePrompt::new()); let prompt = Box::new(RustylinePrompt::new());
return Session::new(agent, prompt, session_file); 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 { } else {
// Try to resume most recent session // Try to resume most recent session
if let Ok(session_file) = get_most_recent_session() { if let Ok(session_file) = get_most_recent_session() {

View File

@@ -1,3 +1,4 @@
use etcetera::{choose_app_strategy, AppStrategy};
use goose::providers::base::ProviderUsage; use goose::providers::base::ProviderUsage;
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
@@ -13,8 +14,15 @@ pub fn log_usage(session_file: String, usage: Vec<ProviderUsage>) {
}; };
// Ensure log directory exists // Ensure log directory exists
if let Some(home_dir) = dirs::home_dir() { if let Ok(home_dir) = choose_app_strategy(crate::APP_STRATEGY.clone()) {
let log_dir = home_dir.join(".config").join("goose").join("logs"); // 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) { if let Err(e) = std::fs::create_dir_all(&log_dir) {
eprintln!("Failed to create log directory: {}", e); eprintln!("Failed to create log directory: {}", e);
return; return;
@@ -49,6 +57,7 @@ pub fn log_usage(session_file: String, usage: Vec<ProviderUsage>) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use etcetera::{choose_app_strategy, AppStrategy};
use goose::providers::base::{ProviderUsage, Usage}; use goose::providers::base::{ProviderUsage, Usage};
use crate::{ use crate::{
@@ -59,11 +68,11 @@ mod tests {
#[test] #[test]
fn test_session_logging() { fn test_session_logging() {
run_with_tmp_dir(|| { 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 let log_file = home_dir
.join(".config") .in_state_dir("logs")
.join("goose") .unwrap_or_else(|| home_dir.in_data_dir("logs"))
.join("logs")
.join("goose.log"); .join("goose.log");
log_usage( log_usage(

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use etcetera::{choose_app_strategy, AppStrategy};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tracing_appender::rolling::Rotation; use tracing_appender::rolling::Rotation;
@@ -12,17 +13,16 @@ use goose::tracing::langfuse_layer;
/// Returns the directory where log files should be stored. /// Returns the directory where log files should be stored.
/// Creates the directory structure if it doesn't exist. /// Creates the directory structure if it doesn't exist.
fn get_log_directory() -> Result<PathBuf> { fn get_log_directory() -> Result<PathBuf> {
let home = if cfg!(windows) { // choose_app_strategy().state_dir()
std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")? // - macOS/Linux: ~/.local/state/goose/logs/cli
} else { // - Windows: ~\AppData\Roaming\Block\goose\data\logs\cli
std::env::var("HOME").context("HOME environment variable not set")? // - 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) let base_log_dir = home_dir
.join(".config") .in_state_dir("logs/cli")
.join("goose") .unwrap_or_else(|| home_dir.in_data_dir("logs/cli"));
.join("logs")
.join("cli"); // Add cli-specific subdirectory
// Create date-based subdirectory // Create date-based subdirectory
let now = chrono::Local::now(); let now = chrono::Local::now();

View File

@@ -1,5 +1,13 @@
use anyhow::Result; use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand}; use clap::{CommandFactory, Parser, Subcommand};
use etcetera::AppStrategyArgs;
use once_cell::sync::Lazy;
pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: "goose".to_string(),
});
mod commands; mod commands;
mod log_usage; mod log_usage;

View File

@@ -30,8 +30,8 @@ fn shorten_path(path: &str) -> String {
let path = PathBuf::from(path); let path = PathBuf::from(path);
// First try to convert to ~ if it's in home directory // First try to convert to ~ if it's in home directory
let home = dirs::home_dir(); let home = etcetera::home_dir();
let path_str = if let Some(home) = home { let path_str = if let Ok(home) = home {
if let Ok(stripped) = path.strip_prefix(home) { if let Ok(stripped) = path.strip_prefix(home) {
format!("~/{}", stripped.display()) format!("~/{}", stripped.display())
} else { } else {

View File

@@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use core::panic; use core::panic;
use etcetera::{choose_app_strategy, AppStrategy};
use futures::StreamExt; use futures::StreamExt;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, BufRead, Write}; use std::io::{self, BufRead, Write};
@@ -14,8 +15,12 @@ use mcp_core::role::Role;
// File management functions // File management functions
pub fn ensure_session_dir() -> Result<PathBuf> { pub fn ensure_session_dir() -> Result<PathBuf> {
let home_dir = dirs::home_dir().ok_or(anyhow::anyhow!("Could not determine home directory"))?; // choose_app_strategy().data_dir()
let config_dir = home_dir.join(".config").join("goose").join("sessions"); // - 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() { if !config_dir.exists() {
fs::create_dir_all(&config_dir)?; fs::create_dir_all(&config_dir)?;
@@ -24,6 +29,15 @@ pub fn ensure_session_dir() -> Result<PathBuf> {
Ok(config_dir) Ok(config_dir)
} }
/// LEGACY NOTE: remove this once old paths are no longer needed.
pub fn legacy_session_dir() -> Option<PathBuf> {
// 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<PathBuf> { pub fn get_most_recent_session() -> Result<PathBuf> {
let session_dir = ensure_session_dir()?; let session_dir = ensure_session_dir()?;
let mut entries = fs::read_dir(&session_dir)? let mut entries = fs::read_dir(&session_dir)?
@@ -31,6 +45,19 @@ pub fn get_most_recent_session() -> Result<PathBuf> {
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl")) .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl"))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// 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::<Vec<_>>();
entries.extend(old_entries);
}
}
if entries.is_empty() { if entries.is_empty() {
return Err(anyhow::anyhow!("No session files found")); return Err(anyhow::anyhow!("No session files found"));
} }

View File

@@ -29,13 +29,14 @@ xcap = "0.0.14"
reqwest = { version = "0.11", features = ["json", "rustls-tls"] , default-features = false} reqwest = { version = "0.11", features = ["json", "rustls-tls"] , default-features = false}
async-trait = "0.1" async-trait = "0.1"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
dirs = "5.0.1" etcetera = "0.8.0"
tempfile = "3.8" tempfile = "3.8"
include_dir = "0.7.4" include_dir = "0.7.4"
google-drive3 = "6.0.0" google-drive3 = "6.0.0"
webbrowser = "0.8" webbrowser = "0.8"
http-body-util = "0.1.2" http-body-util = "0.1.2"
regex = "1.11.1" regex = "1.11.1"
once_cell = "1.20.2"
[dev-dependencies] [dev-dependencies]
serial_test = "3.0.0" serial_test = "3.0.0"

View File

@@ -1,4 +1,5 @@
use base64::Engine; use base64::Engine;
use etcetera::{choose_app_strategy, AppStrategy};
use indoc::{formatdoc, indoc}; use indoc::{formatdoc, indoc};
use reqwest::{Client, Url}; use reqwest::{Client, Url};
use serde_json::{json, Value}; use serde_json::{json, Value};
@@ -216,11 +217,13 @@ impl ComputerControllerRouter {
}), }),
); );
// Create cache directory in user's home directory // choose_app_strategy().cache_dir()
let cache_dir = dirs::cache_dir() // - macOS/Linux: ~/.cache/goose/computer_controller/
.unwrap_or_else(|| create_system_automation().get_temp_path()) // - Windows: ~\AppData\Local\Block\goose\cache\computer_controller\
.join("goose") // keep previous behavior of defaulting to /tmp/
.join("computer_controller"); 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(|_| { fs::create_dir_all(&cache_dir).unwrap_or_else(|_| {
println!( println!(

View File

@@ -3,6 +3,7 @@ mod shell;
use anyhow::Result; use anyhow::Result;
use base64::Engine; use base64::Engine;
use etcetera::{choose_app_strategy, AppStrategy};
use indoc::formatdoc; use indoc::formatdoc;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::{ use std::{
@@ -278,9 +279,16 @@ impl DeveloperRouter {
}, },
}; };
// Check for global hints in ~/.config/goose/.goosehints // choose_app_strategy().config_dir()
let global_hints_path = // - macOS/Linux: ~/.config/goose/
PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()); // - 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 // Create the directory if it doesn't exist
let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap()); let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap());

View File

@@ -1,3 +1,12 @@
use etcetera::AppStrategyArgs;
use once_cell::sync::Lazy;
pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: "goose".to_string(),
});
mod computercontroller; mod computercontroller;
mod developer; mod developer;
mod google_drive; mod google_drive;

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use etcetera::{choose_app_strategy, AppStrategy};
use indoc::formatdoc; use indoc::formatdoc;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::{ use std::{
@@ -178,10 +179,13 @@ impl MemoryRouter {
.join(".goose") .join(".goose")
.join("memory"); .join("memory");
// Check for .config/goose/memory in user's home directory // choose_app_strategy().config_dir()
let global_memory_dir = dirs::home_dir() // - macOS/Linux: ~/.config/goose/memory/
.map(|home| home.join(".config/goose/memory")) // - Windows: ~\AppData\Roaming\Block\goose\config\memory
.unwrap_or_else(|| PathBuf::from(".config/goose/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(&global_memory_dir).unwrap();
fs::create_dir_all(&local_memory_dir).unwrap(); fs::create_dir_all(&local_memory_dir).unwrap();

View File

@@ -29,7 +29,8 @@ http = "1.0"
config = { version = "0.14.1", features = ["toml"] } config = { version = "0.14.1", features = ["toml"] }
thiserror = "1.0" thiserror = "1.0"
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
once_cell = "1.18" once_cell = "1.20.2"
etcetera = "0.8.0"
[[bin]] [[bin]]
name = "goosed" name = "goosed"

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use etcetera::{choose_app_strategy, AppStrategy};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tracing_appender::rolling::Rotation; use tracing_appender::rolling::Rotation;
@@ -12,17 +13,16 @@ use goose::tracing::langfuse_layer;
/// Returns the directory where log files should be stored. /// Returns the directory where log files should be stored.
/// Creates the directory structure if it doesn't exist. /// Creates the directory structure if it doesn't exist.
fn get_log_directory() -> Result<PathBuf> { fn get_log_directory() -> Result<PathBuf> {
let home = if cfg!(windows) { // choose_app_strategy().state_dir()
std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")? // - macOS/Linux: ~/.local/state/goose/logs/server
} else { // - Windows: ~\AppData\Roaming\Block\goose\data\logs\server
std::env::var("HOME").context("HOME environment variable not set")? // - 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) let base_log_dir = home_dir
.join(".config") .in_state_dir("logs/server")
.join("goose") .unwrap_or_else(|| home_dir.in_data_dir("logs/server"));
.join("logs")
.join("server"); // Add server-specific subdirectory
// Create date-based subdirectory // Create date-based subdirectory
let now = chrono::Local::now(); let now = chrono::Local::now();

View File

@@ -1,3 +1,12 @@
use etcetera::AppStrategyArgs;
use once_cell::sync::Lazy;
pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: "goose".to_string(),
});
mod commands; mod commands;
mod configuration; mod configuration;
mod error; mod error;

View File

@@ -58,7 +58,7 @@ ctor = "0.2.7"
paste = "1.0" paste = "1.0"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
once_cell = "1.20.2" once_cell = "1.20.2"
dirs = "6.0.0" etcetera = "0.8.0"
rand = "0.8.5" rand = "0.8.5"
# For Bedrock provider # For Bedrock provider

View File

@@ -1,5 +1,6 @@
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use keyring::Entry; use keyring::Entry;
use once_cell::sync::OnceCell; use once_cell::sync::{Lazy, OnceCell};
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
@@ -7,6 +8,12 @@ use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
pub static APP_STRATEGY: Lazy<AppStrategyArgs> = 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_SERVICE: &str = "goose";
const KEYRING_USERNAME: &str = "secrets"; const KEYRING_USERNAME: &str = "secrets";
@@ -99,10 +106,13 @@ static GLOBAL_CONFIG: OnceCell<Config> = OnceCell::new();
impl Default for Config { impl Default for Config {
fn default() -> Self { 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") .expect("goose requires a home dir")
.join(".config") .config_dir();
.join("goose");
std::fs::create_dir_all(&config_dir).expect("Failed to create config directory"); std::fs::create_dir_all(&config_dir).expect("Failed to create config directory");
let config_path = config_dir.join("config.yaml"); let config_path = config_dir.join("config.yaml");

View File

@@ -2,5 +2,5 @@ mod base;
mod extensions; mod extensions;
pub use crate::agents::ExtensionConfig; pub use crate::agents::ExtensionConfig;
pub use base::{Config, ConfigError}; pub use base::{Config, ConfigError, APP_STRATEGY};
pub use extensions::{ExtensionEntry, ExtensionManager}; pub use extensions::{ExtensionEntry, ExtensionManager};

View File

@@ -2,6 +2,7 @@ use anyhow::Result;
use axum::{extract::Query, response::Html, routing::get, Router}; use axum::{extract::Query, response::Html, routing::get, Router};
use base64::Engine; use base64::Engine;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use etcetera::{choose_app_strategy, AppStrategy};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@@ -31,13 +32,12 @@ struct TokenCache {
} }
fn get_base_path() -> PathBuf { fn get_base_path() -> PathBuf {
const BASE_PATH: &str = ".config/goose/databricks/oauth"; // choose_app_strategy().config_dir()
let home_dir = if cfg!(windows) { // - macOS/Linux: ~/.config/goose/databricks/oauth
std::env::var("USERPROFILE").expect("USERPROFILE environment variable not set") // - Windows: ~\AppData\Roaming\Block\goose\config\databricks\oauth\
} else { choose_app_strategy(crate::config::APP_STRATEGY.clone())
std::env::var("HOME").expect("HOME environment variable not set") .expect("goose requires a home dir")
}; .in_config_dir("databricks/oauth")
PathBuf::from(home_dir).join(BASE_PATH)
} }
impl TokenCache { impl TokenCache {