use std::fs;
use std::path::Path;
use crate::memory::types::{parse_frontmatter, MemoryType};
pub const MAX_MEMORY_FILES: usize = 200;
const FRONTMATTER_MAX_LINES: usize = 30;
#[derive(Debug, Clone)]
pub struct MemoryHeader {
pub filename: String,
pub file_path: String,
pub mtime_ms: u64,
pub description: Option<String>,
pub memory_type: Option<MemoryType>,
}
pub async fn scan_memory_files(memory_dir: &str) -> Vec<MemoryHeader> {
let path = Path::new(memory_dir);
if !path.exists() {
return Vec::new();
}
let mut md_files = Vec::new();
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let file_path = entry.path();
if file_path.is_file() {
if let Some(ext) = file_path.extension() {
if ext == "md" {
if let Some(name) = file_path.file_name() {
let name_str = name.to_string_lossy();
if name_str != "MEMORY.md" {
md_files.push(file_path);
}
}
}
}
}
}
}
let mut headers = Vec::new();
for file_path in md_files {
if let Some(filename) = file_path.file_name() {
let filename_str = filename.to_string_lossy().to_string();
let file_path_str = file_path.to_string_lossy().to_string();
let mtime_ms = file_path.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let (description, memory_type) = if let Ok(content) = read_frontmatter_lines(&file_path) {
if let Some(fm) = parse_frontmatter(&content) {
(Some(fm.description), Some(fm.memory_type))
} else {
(None, None)
}
} else {
(None, None)
};
headers.push(MemoryHeader {
filename: filename_str,
file_path: file_path_str,
mtime_ms,
description,
memory_type,
});
}
}
headers.sort_by(|a, b| b.mtime_ms.cmp(&a.mtime_ms));
headers.truncate(MAX_MEMORY_FILES);
headers
}
fn read_frontmatter_lines(path: &Path) -> std::io::Result<String> {
use std::io::{BufRead, BufReader};
let file = fs::File::open(path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for (i, line) in reader.lines().enumerate() {
if i >= FRONTMATTER_MAX_LINES {
break;
}
if let Ok(l) = line {
lines.push(l);
}
}
Ok(lines.join("\n"))
}
pub fn format_memory_manifest(memories: &[MemoryHeader]) -> String {
memories
.iter()
.map(|m| {
let tag = m.memory_type
.as_ref()
.map(|t| format!("[{}] ", t.as_str()))
.unwrap_or_default();
let ts = chrono::DateTime::from_timestamp_millis(m.mtime_ms as i64)
.map(|dt| dt.to_rfc3339())
.unwrap_or_else(|| "unknown".to_string());
match &m.description {
Some(desc) => format!("- {}{} ({}): {}", tag, m.filename, ts, desc),
None => format!("- {}{} ({})", tag, m.filename, ts),
}
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_format_memory_manifest() {
let headers = vec![
MemoryHeader {
filename: "user_test.md".to_string(),
file_path: "/tmp/memory/user_test.md".to_string(),
mtime_ms: 1700000000000,
description: Some("Test description".to_string()),
memory_type: Some(MemoryType::User),
},
MemoryHeader {
filename: "feedback_test.md".to_string(),
file_path: "/tmp/memory/feedback_test.md".to_string(),
mtime_ms: 1700000000000,
description: None,
memory_type: Some(MemoryType::Feedback),
},
];
let manifest = format_memory_manifest(&headers);
assert!(manifest.contains("user_test.md"));
assert!(manifest.contains("Test description"));
assert!(manifest.contains("[user]"));
}
#[tokio::test]
async fn test_scan_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let result = scan_memory_files(temp_dir.path().to_str().unwrap()).await;
assert!(result.is_empty());
}
#[tokio::test]
async fn test_scan_nonexistent_directory() {
let result = scan_memory_files("/nonexistent/path").await;
assert!(result.is_empty());
}
}