Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Feb 11, 2025
6 changes: 4 additions & 2 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"] }
tokio = { version = "1.0", features = ["rt", "macros"] }
15 changes: 12 additions & 3 deletions crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
21 changes: 15 additions & 6 deletions crates/goose-cli/src/log_usage.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use etcetera::{choose_app_strategy, AppStrategy};
use goose::providers::base::ProviderUsage;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
Expand All @@ -13,8 +14,15 @@ pub fn log_usage(session_file: String, usage: Vec<ProviderUsage>) {
};

// 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;
Expand Down Expand Up @@ -49,6 +57,7 @@ pub fn log_usage(session_file: String, usage: Vec<ProviderUsage>) {

#[cfg(test)]
mod tests {
use etcetera::{choose_app_strategy, AppStrategy};
use goose::providers::base::{ProviderUsage, Usage};

use crate::{
Expand All @@ -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(
Expand Down
22 changes: 11 additions & 11 deletions crates/goose-cli/src/logging.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<PathBuf> {
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();
Expand Down
8 changes: 8 additions & 0 deletions crates/goose-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: "goose".to_string(),
});

mod commands;
mod log_usage;
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-cli/src/prompt/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 29 additions & 2 deletions crates/goose-cli/src/session.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -14,8 +15,12 @@ use mcp_core::role::Role;

// File management functions
pub fn ensure_session_dir() -> Result<PathBuf> {
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)?;
Expand All @@ -24,13 +29,35 @@ pub fn ensure_session_dir() -> Result<PathBuf> {
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> {
let session_dir = ensure_session_dir()?;
let mut entries = fs::read_dir(&session_dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "jsonl"))
.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() {
return Err(anyhow::anyhow!("No session files found"));
}
Expand Down
3 changes: 2 additions & 1 deletion crates/goose-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 8 additions & 5 deletions crates/goose-mcp/src/computercontroller/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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!(
Expand Down
14 changes: 11 additions & 3 deletions crates/goose-mcp/src/developer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -230,9 +231,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());

Expand Down
9 changes: 9 additions & 0 deletions crates/goose-mcp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 developer;
mod google_drive;
Expand Down
12 changes: 8 additions & 4 deletions crates/goose-mcp/src/memory/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions crates/goose-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ 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"
path = "src/main.rs"

[dev-dependencies]
tower = "0.5"
async-trait = "0.1"
async-trait = "0.1"
20 changes: 10 additions & 10 deletions crates/goose-server/src/logging.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<PathBuf> {
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();
Expand Down
9 changes: 9 additions & 0 deletions crates/goose-server/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 configuration;
mod error;
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading