ai-agent-sdk 0.4.0

Idiomatic agent sdk inspired by the claude code source leak
Documentation
//! Memory directory path resolution.
//!
//! Handles finding and managing the memory directory for persistent storage.

use std::fs;
use std::path::{Path, PathBuf};

/// Get the default memory base directory (~/.ai)
pub fn get_memory_base_dir() -> PathBuf {
    if let Ok(dir) = std::env::var("AI_REMOTE_MEMORY_DIR") {
        return PathBuf::from(dir);
    }

    // Default to ~/.ai
    if let Some(home) = dirs::home_dir() {
        home.join(".ai")
    } else {
        // Fallback to current directory if no home found
        std::env::current_dir()
            .map(|p| p.join(".ai"))
            .unwrap_or_else(|_| PathBuf::from(".ai"))
    }
}

/// Get the auto-memory directory path
/// Resolution order:
///   1. AI_MEMORY_PATH_OVERRIDE env var
///   2. autoMemoryDirectory in settings
///   3. <memoryBase>/projects/<project_slug>/memory/
pub fn get_auto_mem_path() -> PathBuf {
    // Check for env override
    if let Ok(override_path) = std::env::var("AI_MEMORY_PATH_OVERRIDE") {
        let path = PathBuf::from(&override_path);
        if is_valid_memory_path(&path) {
            return path;
        }
    }

    // Get base directory
    let base = get_memory_base_dir();
    let projects_dir = base.join("projects");

    // Get project slug from current directory
    let project_slug = get_project_slug();

    projects_dir.join(project_slug).join("memory")
}

/// Get a sanitized project slug from the current directory
pub fn get_project_slug() -> String {
    // Use current directory name, sanitized
    if let Ok(cwd) = std::env::current_dir() {
        let name = cwd.file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown");
        sanitize_path_component(name)
    } else {
        // Fallback to a safe default when current_dir fails
        "unknown".to_string()
    }
}

/// Sanitize a string for use in path components
fn sanitize_path_component(s: &str) -> String {
    s.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' || c == '_' {
                c
            } else {
                '_'
            }
        })
        .collect()
}

/// Validate a memory path for security
/// Rejects:
///   - Relative paths
///   - Root/near-root paths
///   - Paths with null bytes
fn is_valid_memory_path(path: &Path) -> bool {
    if !path.is_absolute() {
        return false;
    }

    let path_str = path.to_string_lossy();
    if path_str.contains('\0') {
        return false;
    }

    // Reject too-short paths (root level)
    if path_str.len() < 3 {
        return false;
    }

    true
}

/// Get the MEMORY.md entrypoint path
pub fn get_memory_entrypoint() -> PathBuf {
    get_auto_mem_path().join("MEMORY.md")
}

/// Check if auto-memory is enabled
pub fn is_auto_memory_enabled() -> bool {
    // Check env var to disable
    if let Ok(val) = std::env::var("AI_DISABLE_AUTO_MEMORY") {
        if val == "1" || val.to_lowercase() == "true" {
            return false;
        }
    }

    // Check for --simple mode
    if let Ok(val) = std::env::var("AI_SIMPLE") {
        if val == "1" || val.to_lowercase() == "true" {
            return false;
        }
    }

    true
}

/// Ensure the memory directory exists
pub fn ensure_memory_dir_exists() -> std::io::Result<()> {
    let path = get_auto_mem_path();
    fs::create_dir_all(path)?;
    Ok(())
}

/// Check if a path is within the auto-memory directory
pub fn is_auto_mem_path(absolute_path: &Path) -> bool {
    let mem_path = get_auto_mem_path();
    absolute_path.starts_with(&mem_path)
}

#[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_get_project_slug() {
        let slug = get_project_slug();
        assert!(!slug.is_empty());
    }
}