use super::storage::{DailyNote, Memory};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct MemoryManager {
_workspace: PathBuf,
memory_path: PathBuf,
notes_dir: PathBuf,
history_path: PathBuf,
}
impl MemoryManager {
pub fn new<P: AsRef<Path>>(workspace: P) -> Self {
let workspace = workspace.as_ref().to_path_buf();
let memory_path = workspace.join("memory").join("MEMORY.md");
let history_path = workspace.join("memory").join("HISTORY.md");
let notes_dir = workspace.join("memory");
Self {
_workspace: workspace,
memory_path,
notes_dir,
history_path,
}
}
pub fn load_memory(&self) -> Memory {
if self.memory_path.exists() {
match std::fs::read_to_string(&self.memory_path) {
Ok(content) => Memory::with_content(content),
Err(_) => Memory::new(),
}
} else {
Memory::new()
}
}
pub fn save_memory(&self, memory: &Memory) -> crate::Result<()> {
if let Some(parent) = self.memory_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&self.memory_path, &memory.content)?;
Ok(())
}
pub fn load_history(&self) -> String {
if self.history_path.exists() {
std::fs::read_to_string(&self.history_path).unwrap_or_default()
} else {
String::new()
}
}
pub fn append_history(&self, entry: &str) -> crate::Result<()> {
if entry.trim().is_empty() {
return Ok(());
}
if let Some(parent) = self.history_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut content = self.load_history();
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str(entry.trim_end());
content.push_str("\n\n");
std::fs::write(&self.history_path, content)?;
Ok(())
}
pub fn load_daily_note(&self, date: impl AsRef<str>) -> DailyNote {
let date = date.as_ref();
let path = self.notes_dir.join(format!("{}.md", date));
if path.exists() {
match std::fs::read_to_string(&path) {
Ok(content) => {
let mut note = DailyNote::for_date(date);
note.content = content;
note
}
Err(_) => DailyNote::for_date(date),
}
} else {
DailyNote::for_date(date)
}
}
pub fn load_today_note(&self) -> DailyNote {
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
self.load_daily_note(&today)
}
pub fn save_daily_note(&self, note: &DailyNote) -> crate::Result<()> {
std::fs::create_dir_all(&self.notes_dir)?;
let path = self.notes_dir.join(note.filename());
std::fs::write(&path, ¬e.content)?;
Ok(())
}
pub fn list_notes(&self) -> Vec<String> {
let mut notes = Vec::new();
if let Ok(entries) = std::fs::read_dir(&self.notes_dir) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.ends_with(".md") && name != "MEMORY.md" {
let date = name.trim_end_matches(".md").to_string();
notes.push(date);
}
}
}
}
notes.sort_by(|a, b| b.cmp(a)); notes
}
pub fn memory_dir(&self) -> &Path {
&self.notes_dir
}
pub fn append_today(&self, content: &str) -> crate::Result<()> {
let mut note = self.load_today_note();
if note.content.is_empty() {
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
note.content = format!("# {}\n\n{}", today, content);
} else {
note.content.push('\n');
note.content.push_str(content);
}
self.save_daily_note(¬e)
}
pub fn get_recent_memories(&self, days: usize) -> String {
use chrono::Duration;
let mut memories = Vec::new();
let today = chrono::Local::now().date_naive();
for i in 0..days {
let date = today - Duration::days(i as i64);
let date_str = date.format("%Y-%m-%d").to_string();
let note = self.load_daily_note(&date_str);
if !note.content.is_empty() {
memories.push(note.content);
}
}
memories.join("\n\n---\n\n")
}
pub fn list_memory_files(&self) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = std::fs::read_dir(&self.notes_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.len() == 13 && name.ends_with(".md") && name != "MEMORY.md" {
let date_part = &name[..10];
if date_part.chars().filter(|c| *c == '-').count() == 2 {
files.push(path);
}
}
}
}
}
files.sort_by(|a, b| b.cmp(a));
files
}
pub fn get_memory_context(&self) -> String {
let memory = self.load_memory();
if memory.content.is_empty() {
String::new()
} else {
format!("## Long-term Memory\n{}", memory.content)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_memory_manager_creation() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
assert_eq!(manager._workspace, temp_dir.path());
}
#[test]
fn test_load_save_memory() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let memory = Memory::with_content("Test memory");
manager.save_memory(&memory).unwrap();
let loaded = manager.load_memory();
assert_eq!(loaded.content, "Test memory");
}
#[test]
fn test_load_save_daily_note() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let mut note = DailyNote::for_date("2024-01-15");
note.content = "Test note".to_string();
manager.save_daily_note(¬e).unwrap();
let loaded = manager.load_daily_note("2024-01-15");
assert_eq!(loaded.content, "Test note");
}
#[test]
fn test_list_notes() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let mut note1 = DailyNote::for_date("2024-01-15");
note1.content = "Note 1".to_string();
manager.save_daily_note(¬e1).unwrap();
let mut note2 = DailyNote::for_date("2024-01-16");
note2.content = "Note 2".to_string();
manager.save_daily_note(¬e2).unwrap();
let notes = manager.list_notes();
assert_eq!(notes.len(), 2);
assert_eq!(notes[0], "2024-01-16"); }
#[test]
fn test_append_today() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
manager.append_today("First entry").unwrap();
let note = manager.load_today_note();
assert!(note.content.contains("First entry"));
assert!(note.content.starts_with("#"));
manager.append_today("Second entry").unwrap();
let note = manager.load_today_note();
assert!(note.content.contains("First entry"));
assert!(note.content.contains("Second entry"));
}
#[test]
fn test_get_recent_memories() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let mut note1 = DailyNote::for_date("2024-01-15");
note1.content = "Memory 1".to_string();
manager.save_daily_note(¬e1).unwrap();
let mut note2 = DailyNote::for_date("2024-01-16");
note2.content = "Memory 2".to_string();
manager.save_daily_note(¬e2).unwrap();
let recent = manager.get_recent_memories(7);
assert!(recent.is_empty() || !recent.is_empty());
}
#[test]
fn test_list_memory_files() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let mut note1 = DailyNote::for_date("2024-01-15");
note1.content = "Note 1".to_string();
manager.save_daily_note(¬e1).unwrap();
let mut note2 = DailyNote::for_date("2024-01-16");
note2.content = "Note 2".to_string();
manager.save_daily_note(¬e2).unwrap();
let files = manager.list_memory_files();
assert_eq!(files.len(), 2);
assert!(files[0].to_str().unwrap().contains("2024-01-16"));
}
#[test]
fn test_get_memory_context() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let memory = Memory::with_content("Long term info");
manager.save_memory(&memory).unwrap();
let context = manager.get_memory_context();
assert!(context.contains("Long-term Memory"));
assert!(context.contains("Long term info"));
}
#[test]
fn test_get_memory_context_empty() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
let context = manager.get_memory_context();
assert_eq!(context, "");
}
#[test]
fn test_append_history() {
let temp_dir = TempDir::new().unwrap();
let manager = MemoryManager::new(temp_dir.path());
manager
.append_history("[2026-02-12 09:00] Added memory event")
.unwrap();
let history = manager.load_history();
assert!(history.contains("Added memory event"));
}
}