use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static MX_HOME: OnceLock<PathBuf> = OnceLock::new();
fn resolve_mx_home_with(env_val: Option<&str>) -> PathBuf {
if let Some(val) = env_val
&& !val.is_empty()
{
return PathBuf::from(val);
}
dirs::home_dir()
.expect("Could not determine home directory")
.join(".mx")
}
pub fn mx_home() -> &'static PathBuf {
MX_HOME.get_or_init(|| resolve_mx_home_with(std::env::var("MX_HOME").ok().as_deref()))
}
pub(crate) fn legacy_memory_path_set(env_val: Option<&str>) -> bool {
env_val.map(|v| !v.is_empty()).unwrap_or(false)
}
pub fn emit_legacy_memory_path_note() {
if legacy_memory_path_set(std::env::var("MX_MEMORY_PATH").ok().as_deref()) {
eprintln!(
"note: `MX_MEMORY_PATH` is no longer used. \
It was renamed to `MX_SURREAL_ROOT`. Update your environment."
);
}
}
pub fn swap_dir() -> PathBuf {
mx_home().join("swap")
}
pub fn sync_cache_dir(repo: &str) -> PathBuf {
let repo_slug = repo.replace('/', "-");
mx_home().join("cache").join("sync").join(repo_slug)
}
pub fn kv_schema_dir() -> PathBuf {
mx_home().join("kv").join("schema")
}
pub fn kv_data_dir() -> PathBuf {
mx_home().join("kv").join("data")
}
pub fn kv_schema_path(agent: &str) -> PathBuf {
kv_schema_dir().join(format!("{}.toml", agent))
}
pub fn kv_data_path(agent: &str) -> PathBuf {
kv_data_dir().join(format!("{}.json", agent))
}
pub fn legacy_crewu_kv_schema_path(agent: &str) -> Option<PathBuf> {
dirs::home_dir().map(|h| {
h.join(".crewu")
.join("kv")
.join(format!("{}.schema.toml", agent))
})
}
pub fn legacy_crewu_kv_data_path(agent: &str) -> Option<PathBuf> {
dirs::home_dir().map(|h| {
h.join(".crewu")
.join("kv")
.join(format!("{}.data.json", agent))
})
}
pub fn state_schemas_dir() -> PathBuf {
mx_home().join("state").join("schemas")
}
pub fn tensor_schema_path(id: &str) -> PathBuf {
state_schemas_dir().join(format!("{}.yaml", id))
}
fn surreal_root_with(env_val: Option<&str>, home: &Path) -> PathBuf {
if let Some(path) = env_val
&& !path.is_empty()
{
return PathBuf::from(path);
}
home.join("memory").join("surreal")
}
pub fn surreal_root() -> PathBuf {
surreal_root_with(std::env::var("MX_SURREAL_ROOT").ok().as_deref(), mx_home())
}
pub fn memory_seed_agents_dir() -> PathBuf {
mx_home().join("memory").join("seed").join("agents")
}
pub fn memory_seed_knowledge_dir() -> PathBuf {
mx_home().join("memory").join("seed").join("knowledge")
}
fn legacy_agents_dir_with(home: &Path) -> PathBuf {
home.join("agents")
}
pub fn legacy_agents_dir() -> PathBuf {
legacy_agents_dir_with(mx_home())
}
pub fn legacy_memory_index_jsonl() -> PathBuf {
mx_home().join("memory").join("index.jsonl")
}
fn fastembed_cache_dir_with(
isolate_env: Option<&str>,
cache_dir_fn: impl FnOnce() -> Option<PathBuf>,
home: &Path,
) -> PathBuf {
if isolate_env.map(|v| !v.is_empty()).unwrap_or(false) {
return home.join("memory").join("embed");
}
cache_dir_fn()
.map(|d| d.join("fastembed"))
.unwrap_or_else(|| PathBuf::from(".fastembed_cache"))
}
pub fn fastembed_cache_dir() -> PathBuf {
fastembed_cache_dir_with(
std::env::var("MX_ISOLATE_FASTEMBED").ok().as_deref(),
dirs::cache_dir,
mx_home(),
)
}
fn codex_dir_with(env_val: Option<&str>, home: &Path) -> PathBuf {
if let Some(path) = env_val
&& !path.is_empty()
{
return PathBuf::from(path);
}
home.join("codex")
}
pub fn codex_dir() -> PathBuf {
codex_dir_with(std::env::var("MX_CODEX_PATH").ok().as_deref(), mx_home())
}
fn claude_home_root() -> PathBuf {
dirs::home_dir().expect("Could not determine home directory")
}
pub fn claude_dir() -> PathBuf {
claude_home_root().join(".claude")
}
pub fn claude_projects_dir() -> PathBuf {
if let Ok(override_dir) = std::env::var("MX_CLAUDE_PROJECTS_DIR") {
return PathBuf::from(override_dir);
}
claude_dir().join("projects")
}
pub fn claude_config_path() -> PathBuf {
claude_home_root().join(".claude.json")
}
pub fn claude_subagents_dir(project_slug: &str, session_id: &str) -> PathBuf {
claude_projects_dir()
.join(project_slug)
.join(session_id)
.join("subagents")
}
pub fn claude_sessions_dir() -> PathBuf {
claude_dir().join("sessions")
}
pub fn claude_history_jsonl() -> PathBuf {
claude_dir().join("history.jsonl")
}
pub fn claude_mcp_logs_dir(cwd_encoded: &str) -> PathBuf {
claude_home_root()
.join(".cache")
.join("claude-cli-nodejs")
.join(cwd_encoded)
}
pub fn tmp_claude_tasks_dir(uid: u32, user_slug: &str, session_uuid: &str) -> PathBuf {
PathBuf::from(format!("/tmp/claude-{}", uid))
.join(user_slug)
.join(session_uuid)
.join("tasks")
}
pub fn wonka_vault_archives_dir() -> PathBuf {
claude_home_root()
.join(".wonka")
.join("vault")
.join("archives")
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn mx_home_default_when_unset() {
let result = resolve_mx_home_with(None);
let expected = dirs::home_dir().unwrap().join(".mx");
assert_eq!(result, expected);
}
#[test]
fn mx_home_respects_env_var() {
let result = resolve_mx_home_with(Some("/tmp/test-mx-home"));
assert_eq!(result, PathBuf::from("/tmp/test-mx-home"));
}
#[test]
fn mx_home_empty_env_is_default() {
let result = resolve_mx_home_with(Some(""));
let expected = dirs::home_dir().unwrap().join(".mx");
assert_eq!(result, expected);
}
#[test]
fn derived_dirs_under_mx_home() {
let home = mx_home();
let swap = swap_dir();
assert!(swap.starts_with(home), "swap_dir not under mx_home");
assert_eq!(swap.file_name().unwrap(), "swap");
let codex = codex_dir_with(None, home);
assert!(codex.starts_with(home), "codex_dir not under mx_home");
assert_eq!(codex.file_name().unwrap(), "codex");
let sync = sync_cache_dir("owner/repo");
assert!(sync.starts_with(home), "sync_cache_dir not under mx_home");
}
#[test]
fn codex_dir_respects_override() {
let home = mx_home().clone();
let result = codex_dir_with(Some("/custom/codex"), &home);
assert_eq!(result, PathBuf::from("/custom/codex"));
}
#[test]
fn codex_dir_empty_override_is_default() {
let home = mx_home().clone();
let result = codex_dir_with(Some(""), &home);
assert_eq!(result, home.join("codex"));
}
#[test]
fn codex_dir_none_override_is_default() {
let home = mx_home().clone();
let result = codex_dir_with(None, &home);
assert_eq!(result, home.join("codex"));
}
#[test]
fn swap_dir_is_under_mx_home() {
let swap = swap_dir();
assert!(swap.starts_with(mx_home()));
}
#[test]
fn sync_cache_dir_slugifies_repo() {
let dir = sync_cache_dir("owner/repo");
assert!(dir.to_string_lossy().contains("owner-repo"));
assert!(dir.starts_with(mx_home()));
}
#[test]
fn kv_helpers_under_mx_home() {
let home = mx_home();
assert!(kv_schema_dir().starts_with(home));
assert!(kv_data_dir().starts_with(home));
assert!(kv_schema_path("smith").ends_with("kv/schema/smith.toml"));
assert!(kv_data_path("smith").ends_with("kv/data/smith.json"));
}
#[test]
fn tensor_schema_path_layout() {
let p = tensor_schema_path("crewu");
assert!(p.ends_with("state/schemas/crewu.yaml"));
assert!(p.starts_with(mx_home()));
}
#[test]
fn surreal_root_default() {
let home = mx_home().clone();
let r = surreal_root_with(None, &home);
assert_eq!(r, home.join("memory").join("surreal"));
}
#[test]
fn surreal_root_respects_override() {
let home = mx_home().clone();
let r = surreal_root_with(Some("/custom/surreal"), &home);
assert_eq!(r, PathBuf::from("/custom/surreal"));
}
#[test]
fn surreal_root_empty_override_is_default() {
let home = mx_home().clone();
let r = surreal_root_with(Some(""), &home);
assert_eq!(r, home.join("memory").join("surreal"));
}
#[test]
fn memory_seed_dirs_under_mx_home() {
let home = mx_home();
assert!(memory_seed_agents_dir().starts_with(home));
assert!(memory_seed_agents_dir().ends_with("memory/seed/agents"));
assert!(memory_seed_knowledge_dir().starts_with(home));
assert!(memory_seed_knowledge_dir().ends_with("memory/seed/knowledge"));
}
#[test]
fn legacy_agents_dir_with_uses_supplied_home() {
let home = PathBuf::from("/tmp/some-test-home");
let r = legacy_agents_dir_with(&home);
assert_eq!(r, home.join("agents"));
}
#[test]
fn legacy_agents_dir_public_wrapper_matches_seam() {
assert_eq!(legacy_agents_dir(), legacy_agents_dir_with(mx_home()));
}
#[test]
fn fastembed_cache_default_uses_xdg() {
let home = mx_home().clone();
let xdg = PathBuf::from("/xdg/cache");
let r = fastembed_cache_dir_with(None, || Some(xdg.clone()), &home);
assert_eq!(r, xdg.join("fastembed"));
}
#[test]
fn fastembed_cache_isolate_uses_mx_home() {
let home = mx_home().clone();
let xdg = PathBuf::from("/xdg/cache");
let r = fastembed_cache_dir_with(Some("1"), || Some(xdg.clone()), &home);
assert_eq!(r, home.join("memory").join("embed"));
}
#[test]
fn fastembed_cache_isolate_empty_is_default() {
let home = mx_home().clone();
let xdg = PathBuf::from("/xdg/cache");
let r = fastembed_cache_dir_with(Some(""), || Some(xdg.clone()), &home);
assert_eq!(r, xdg.join("fastembed"));
}
#[test]
fn fastembed_cache_no_xdg_falls_back() {
let home = mx_home().clone();
let r = fastembed_cache_dir_with(None, || None, &home);
assert_eq!(r, PathBuf::from(".fastembed_cache"));
}
#[test]
#[serial]
fn claude_paths_under_home() {
let prev = std::env::var("MX_CLAUDE_PROJECTS_DIR").ok();
unsafe {
std::env::remove_var("MX_CLAUDE_PROJECTS_DIR");
}
let home = dirs::home_dir().unwrap();
assert_eq!(claude_projects_dir(), home.join(".claude").join("projects"));
assert_eq!(claude_config_path(), home.join(".claude.json"));
unsafe {
if let Some(v) = prev {
std::env::set_var("MX_CLAUDE_PROJECTS_DIR", v);
}
}
}
#[test]
fn claude_dir_is_dot_claude() {
let home = dirs::home_dir().unwrap();
assert_eq!(claude_dir(), home.join(".claude"));
}
#[test]
#[serial]
fn claude_subagents_dir_layout() {
let prev = std::env::var("MX_CLAUDE_PROJECTS_DIR").ok();
unsafe {
std::env::remove_var("MX_CLAUDE_PROJECTS_DIR");
}
let home = dirs::home_dir().unwrap();
let p = claude_subagents_dir("-home-charlie-recipes-coryzibell-mx", "abc-123");
let expected = home
.join(".claude")
.join("projects")
.join("-home-charlie-recipes-coryzibell-mx")
.join("abc-123")
.join("subagents");
assert_eq!(p, expected);
unsafe {
if let Some(v) = prev {
std::env::set_var("MX_CLAUDE_PROJECTS_DIR", v);
}
}
}
#[test]
fn claude_sessions_dir_layout() {
let home = dirs::home_dir().unwrap();
assert_eq!(claude_sessions_dir(), home.join(".claude").join("sessions"));
}
#[test]
fn claude_history_jsonl_layout() {
let home = dirs::home_dir().unwrap();
assert_eq!(
claude_history_jsonl(),
home.join(".claude").join("history.jsonl")
);
}
#[test]
fn claude_mcp_logs_dir_layout() {
let home = dirs::home_dir().unwrap();
let p = claude_mcp_logs_dir("-home-charlie-recipes-coryzibell-mx");
let expected = home
.join(".cache")
.join("claude-cli-nodejs")
.join("-home-charlie-recipes-coryzibell-mx");
assert_eq!(p, expected);
assert!(!p.to_string_lossy().contains("mcp-logs-"));
}
#[test]
fn tmp_claude_tasks_dir_layout() {
let p = tmp_claude_tasks_dir(
1002,
"-home-charlie",
"c3744b8d-5719-4df2-924f-707945438494",
);
let expected = PathBuf::from("/tmp/claude-1002")
.join("-home-charlie")
.join("c3744b8d-5719-4df2-924f-707945438494")
.join("tasks");
assert_eq!(p, expected);
}
#[test]
fn wonka_vault_archives_dir_layout() {
let home = dirs::home_dir().unwrap();
assert_eq!(
wonka_vault_archives_dir(),
home.join(".wonka").join("vault").join("archives")
);
}
#[test]
fn legacy_memory_path_unset_returns_false() {
assert!(!legacy_memory_path_set(None));
}
#[test]
fn legacy_memory_path_empty_returns_false() {
assert!(!legacy_memory_path_set(Some("")));
}
#[test]
fn legacy_memory_path_set_returns_true() {
assert!(legacy_memory_path_set(Some("/anything")));
}
}