use std::path::{Path, PathBuf};
use super::types::{MemoryMeta, MemoryType};
const MAX_INDEX_LINE_CHARS: usize = 150;
const MAX_INDEX_LINES: usize = 200;
pub fn write_memory(
memory_dir: &Path,
filename: &str,
meta: &MemoryMeta,
content: &str,
) -> Result<PathBuf, String> {
let _ = std::fs::create_dir_all(memory_dir);
let type_str = match &meta.memory_type {
Some(MemoryType::User) => "user",
Some(MemoryType::Feedback) => "feedback",
Some(MemoryType::Project) => "project",
Some(MemoryType::Reference) => "reference",
None => "user",
};
let file_content = format!(
"---\nname: {}\ndescription: {}\ntype: {}\n---\n\n{}",
meta.name, meta.description, type_str, content
);
let file_path = memory_dir.join(filename);
std::fs::write(&file_path, &file_content)
.map_err(|e| format!("Failed to write memory file: {e}"))?;
update_index(memory_dir, filename, &meta.name, &meta.description)?;
Ok(file_path)
}
fn update_index(
memory_dir: &Path,
filename: &str,
name: &str,
description: &str,
) -> Result<(), String> {
let index_path = memory_dir.join("MEMORY.md");
let existing = std::fs::read_to_string(&index_path).unwrap_or_default();
let mut line = format!("- [{}]({}) — {}", name, filename, description);
if line.len() > MAX_INDEX_LINE_CHARS {
line.truncate(MAX_INDEX_LINE_CHARS - 3);
line.push_str("...");
}
let mut lines: Vec<String> = existing
.lines()
.filter(|l| !l.contains(&format!("({})", filename)))
.map(|l| l.to_string())
.collect();
lines.push(line);
if lines.len() > MAX_INDEX_LINES {
lines.truncate(MAX_INDEX_LINES);
}
let new_index = lines.join("\n") + "\n";
std::fs::write(&index_path, new_index).map_err(|e| format!("Failed to update index: {e}"))?;
Ok(())
}
pub fn delete_memory(memory_dir: &Path, filename: &str) -> Result<(), String> {
let file_path = memory_dir.join(filename);
if file_path.exists() {
std::fs::remove_file(&file_path).map_err(|e| format!("Failed to delete: {e}"))?;
}
let index_path = memory_dir.join("MEMORY.md");
if let Ok(existing) = std::fs::read_to_string(&index_path) {
let filtered: Vec<&str> = existing
.lines()
.filter(|l| !l.contains(&format!("({})", filename)))
.collect();
let _ = std::fs::write(&index_path, filtered.join("\n") + "\n");
}
Ok(())
}
pub fn rebuild_index(memory_dir: &Path) -> Result<(), String> {
let headers = super::scanner::scan_memory_files(memory_dir);
let index_path = memory_dir.join("MEMORY.md");
let mut lines = Vec::new();
for h in &headers {
let name = h
.meta
.as_ref()
.map(|m| m.name.as_str())
.unwrap_or(&h.filename);
let desc = h
.meta
.as_ref()
.map(|m| m.description.as_str())
.unwrap_or("");
let mut line = format!("- [{}]({}) — {}", name, h.filename, desc);
if line.len() > MAX_INDEX_LINE_CHARS {
line.truncate(MAX_INDEX_LINE_CHARS - 3);
line.push_str("...");
}
lines.push(line);
}
if lines.len() > MAX_INDEX_LINES {
lines.truncate(MAX_INDEX_LINES);
}
let content = lines.join("\n") + "\n";
std::fs::write(&index_path, content).map_err(|e| format!("Failed to write index: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_meta() -> MemoryMeta {
MemoryMeta {
name: "Test Memory".to_string(),
description: "A test memory file".to_string(),
memory_type: Some(MemoryType::User),
}
}
#[test]
fn test_write_memory_creates_file_and_index() {
let dir = tempfile::tempdir().unwrap();
let meta = test_meta();
let path = write_memory(dir.path(), "test.md", &meta, "Hello world").unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("name: Test Memory"));
assert!(content.contains("type: user"));
assert!(content.contains("Hello world"));
let index = std::fs::read_to_string(dir.path().join("MEMORY.md")).unwrap();
assert!(index.contains("[Test Memory](test.md)"));
}
#[test]
fn test_write_memory_updates_existing_index_entry() {
let dir = tempfile::tempdir().unwrap();
let meta = test_meta();
write_memory(dir.path(), "test.md", &meta, "version 1").unwrap();
let meta2 = MemoryMeta {
name: "Updated".to_string(),
description: "Updated description".to_string(),
memory_type: Some(MemoryType::Feedback),
};
write_memory(dir.path(), "test.md", &meta2, "version 2").unwrap();
let index = std::fs::read_to_string(dir.path().join("MEMORY.md")).unwrap();
assert_eq!(index.matches("test.md").count(), 1);
assert!(index.contains("[Updated](test.md)"));
}
#[test]
fn test_delete_memory() {
let dir = tempfile::tempdir().unwrap();
let meta = test_meta();
write_memory(dir.path(), "test.md", &meta, "content").unwrap();
delete_memory(dir.path(), "test.md").unwrap();
assert!(!dir.path().join("test.md").exists());
let index = std::fs::read_to_string(dir.path().join("MEMORY.md")).unwrap();
assert!(!index.contains("test.md"));
}
#[test]
fn test_delete_nonexistent_memory() {
let dir = tempfile::tempdir().unwrap();
assert!(delete_memory(dir.path(), "nope.md").is_ok());
}
#[test]
fn test_rebuild_index() {
let dir = tempfile::tempdir().unwrap();
let meta = test_meta();
write_memory(dir.path(), "one.md", &meta, "first").unwrap();
let meta2 = MemoryMeta {
name: "Second".to_string(),
description: "Second file".to_string(),
memory_type: Some(MemoryType::Project),
};
write_memory(dir.path(), "two.md", &meta2, "second").unwrap();
std::fs::write(dir.path().join("MEMORY.md"), "garbage").unwrap();
rebuild_index(dir.path()).unwrap();
let index = std::fs::read_to_string(dir.path().join("MEMORY.md")).unwrap();
assert!(index.contains("one.md"));
assert!(index.contains("two.md"));
}
#[test]
fn test_index_line_length_cap() {
let dir = tempfile::tempdir().unwrap();
let meta = MemoryMeta {
name: "A".repeat(200),
description: "B".repeat(200),
memory_type: Some(MemoryType::User),
};
write_memory(dir.path(), "long.md", &meta, "content").unwrap();
let index = std::fs::read_to_string(dir.path().join("MEMORY.md")).unwrap();
for line in index.lines() {
assert!(line.len() <= MAX_INDEX_LINE_CHARS + 3); }
}
}