use crate::constants::env::ai;
use std::fs;
use std::path::{Path, PathBuf};
pub fn get_memory_base_dir() -> PathBuf {
if let Ok(dir) = std::env::var(ai::REMOTE_MEMORY_DIR) {
return PathBuf::from(dir);
}
if let Some(home) = dirs::home_dir() {
home.join(".ai")
} else {
std::env::current_dir()
.map(|p| p.join(".ai"))
.unwrap_or_else(|_| PathBuf::from(".ai"))
}
}
const AUTO_MEM_DIRNAME: &str = "memory";
const AUTO_MEM_ENTRYPOINT_NAME: &str = "MEMORY.md";
fn validate_memory_path(raw: Option<&str>, expand_tilde: bool) -> Option<PathBuf> {
let raw = raw?;
let candidate = if expand_tilde && (raw.starts_with("~/") || raw.starts_with("~\\")) {
let rest = &raw[2..];
if rest.is_empty()
|| rest == "."
|| rest == ".."
|| rest.starts_with("../")
|| rest.starts_with("..\\")
{
return None;
}
if let Some(home) = dirs::home_dir() {
home.join(rest)
} else {
return None;
}
} else {
PathBuf::from(raw)
};
let path_str = candidate.to_string_lossy().to_string();
let normalized: String = path_str
.chars()
.rev()
.skip_while(|c| *c == '/' || *c == '\\')
.collect::<String>()
.chars()
.rev()
.collect();
if !Path::new(&normalized).is_absolute() {
return None;
}
if normalized.len() < 3 {
return None;
}
if normalized.chars().nth(1) == Some(':') && normalized.len() == 2 {
return None;
}
if normalized.starts_with("\\\\") || normalized.starts_with("//") {
return None;
}
if normalized.contains('\0') {
return None;
}
let sep = std::path::MAIN_SEPARATOR;
if !normalized.ends_with(sep) {
Some(format!("{}{}", normalized, sep).into())
} else {
Some(PathBuf::from(normalized))
}
}
fn get_auto_mem_path_override() -> Option<PathBuf> {
validate_memory_path(
std::env::var(ai::COWORK_MEMORY_PATH_OVERRIDE)
.ok()
.as_deref(),
false,
)
}
pub fn has_auto_mem_path_override() -> bool {
get_auto_mem_path_override().is_some()
}
fn get_project_root() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn get_auto_mem_base() -> PathBuf {
get_project_root()
}
pub fn get_auto_mem_path() -> PathBuf {
if let Some(r#override) = get_auto_mem_path_override() {
return r#override;
}
let base = get_memory_base_dir();
let projects_dir = base.join("projects");
let project_slug = sanitize_path_component(&get_auto_mem_base().to_string_lossy());
let path = projects_dir.join(project_slug).join(AUTO_MEM_DIRNAME);
let path_str = path.to_string_lossy().to_string();
let sep = std::path::MAIN_SEPARATOR;
if !path_str.ends_with(sep) {
format!("{}{}", path_str, sep).into()
} else {
path
}
}
pub fn sanitize_path_component(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}
pub fn get_auto_mem_daily_log_path(date: &std::time::SystemTime) -> PathBuf {
let datetime: chrono::DateTime<chrono::Local> = (*date).into();
let yyyy = datetime.format("%Y").to_string();
let mm = datetime.format("%m").to_string();
let dd = datetime.format("%d").to_string();
get_auto_mem_path()
.join("logs")
.join(&yyyy)
.join(&mm)
.join(format!("{}-{}-{}.md", yyyy, mm, dd))
}
pub fn get_auto_mem_entrypoint() -> PathBuf {
get_auto_mem_path().join(AUTO_MEM_ENTRYPOINT_NAME)
}
pub fn is_auto_mem_path(absolute_path: &Path) -> bool {
let mem_path = get_auto_mem_path();
let path_str = absolute_path.to_string_lossy();
let mem_path_str = mem_path.to_string_lossy().to_string();
path_str.starts_with(mem_path_str.as_str())
}
pub fn is_auto_memory_enabled() -> bool {
if let Ok(env_val) = std::env::var(ai::CODE_DISABLE_AUTO_MEMORY) {
if is_env_truthy(&env_val) {
return false;
}
if is_env_defined_falsy(&env_val) {
return true;
}
}
if is_env_truthy(&std::env::var(ai::SIMPLE).unwrap_or_default()) {
return false;
}
if is_env_truthy(&std::env::var(ai::REMOTE).unwrap_or_default())
&& std::env::var(ai::REMOTE_MEMORY_DIR).is_err()
{
return false;
}
true
}
fn is_env_truthy(val: &str) -> bool {
let lower = val.to_lowercase();
lower == "1" || lower == "true" || lower == "yes" || lower == "on"
}
fn is_env_defined_falsy(val: &str) -> bool {
let lower = val.to_lowercase();
lower == "0" || lower == "false" || lower == "no" || lower == "off"
}
pub fn ensure_memory_dir_exists(memory_dir: &Path) -> std::io::Result<()> {
match fs::create_dir_all(memory_dir) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("ensureMemoryDirExists failed for {:?}: {}", memory_dir, e);
Ok(()) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_path_component() {
assert_eq!(sanitize_path_component("my-project"), "my-project");
assert_eq!(sanitize_path_component("My Project!"), "My_Project_");
assert_eq!(sanitize_path_component("123-test"), "123-test");
}
#[test]
fn test_is_env_truthy() {
assert!(is_env_truthy("1"));
assert!(is_env_truthy("true"));
assert!(is_env_truthy("yes"));
assert!(is_env_truthy("on"));
assert!(!is_env_truthy("0"));
assert!(!is_env_truthy("false"));
}
#[test]
fn test_get_auto_mem_path() {
let path = get_auto_mem_path();
assert!(path.is_absolute());
assert!(
path.to_string_lossy().ends_with("memory/")
|| path.to_string_lossy().ends_with("memory\\")
);
}
#[test]
fn test_is_auto_mem_path() {
let mem_path = get_auto_mem_path();
let inside = mem_path.join("test.md");
let outside = PathBuf::from("/tmp/test.md");
assert!(is_auto_mem_path(&inside));
assert!(!is_auto_mem_path(&outside));
}
#[test]
fn test_get_auto_mem_entrypoint() {
let entrypoint = get_auto_mem_entrypoint();
assert!(entrypoint.file_name().unwrap_or_default() == "MEMORY.md");
}
}