Skip to main content

codelens_engine/
memory.rs

1//! Project memory file management — .codelens/memories/ persistence layer.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{Result, bail};
6
7/// List all memory file names under the memories directory.
8/// Optionally filtered by topic prefix.
9pub fn list_memory_names(memories_dir: &Path, topic: Option<&str>) -> Vec<String> {
10    if !memories_dir.is_dir() {
11        return Vec::new();
12    }
13    let mut names = Vec::new();
14    collect_memory_files(memories_dir, memories_dir, &mut names);
15    names.sort();
16    if let Some(t) = topic {
17        let t = t.trim().trim_matches('/');
18        if !t.is_empty() {
19            names.retain(|n| n == t || n.starts_with(&format!("{t}/")));
20        }
21    }
22    names
23}
24
25fn collect_memory_files(base: &Path, dir: &Path, names: &mut Vec<String>) {
26    let entries = match std::fs::read_dir(dir) {
27        Ok(e) => e,
28        Err(_) => return,
29    };
30    for entry in entries.flatten() {
31        let path = entry.path();
32        if path.is_dir() {
33            collect_memory_files(base, &path, names);
34        } else if path.extension().and_then(|e| e.to_str()) == Some("md")
35            && let Ok(rel) = path.strip_prefix(base)
36        {
37            let name = rel
38                .to_string_lossy()
39                .replace('\\', "/")
40                .trim_end_matches(".md")
41                .to_string();
42            names.push(name);
43        }
44    }
45}
46
47/// Resolve a memory name to a filesystem path, with validation.
48pub fn resolve_memory_path(memories_dir: &Path, name: &str) -> Result<PathBuf> {
49    let normalized = name
50        .trim()
51        .replace('\\', "/")
52        .trim_matches('/')
53        .trim_end_matches(".md")
54        .to_string();
55    if normalized.is_empty() {
56        bail!("memory name must not be empty");
57    }
58    if normalized.contains("..") {
59        bail!("memory path must not contain '..': {name}");
60    }
61    Ok(memories_dir.join(format!("{normalized}.md")))
62}
63
64/// Read a memory file's content.
65pub fn read_memory(memories_dir: &Path, name: &str) -> Result<String> {
66    let path = resolve_memory_path(memories_dir, name)?;
67    std::fs::read_to_string(&path).map_err(|_| anyhow::anyhow!("memory not found: {name}"))
68}
69
70/// Write content to a memory file (creates directories if needed).
71pub fn write_memory(memories_dir: &Path, name: &str, content: &str) -> Result<()> {
72    let path = resolve_memory_path(memories_dir, name)?;
73    if let Some(parent) = path.parent() {
74        std::fs::create_dir_all(parent)?;
75    }
76    std::fs::write(&path, content)?;
77    Ok(())
78}
79
80/// Delete a memory file.
81pub fn delete_memory(memories_dir: &Path, name: &str) -> Result<()> {
82    let path = resolve_memory_path(memories_dir, name)?;
83    if !path.is_file() {
84        bail!("memory not found: {name}");
85    }
86    std::fs::remove_file(&path)?;
87    Ok(())
88}
89
90/// Rename a memory file.
91pub fn rename_memory(memories_dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
92    let old_path = resolve_memory_path(memories_dir, old_name)?;
93    let new_path = resolve_memory_path(memories_dir, new_name)?;
94    if !old_path.is_file() {
95        bail!("memory not found: {old_name}");
96    }
97    if new_path.exists() {
98        bail!("target already exists: {new_name}");
99    }
100    if let Some(parent) = new_path.parent() {
101        std::fs::create_dir_all(parent)?;
102    }
103    std::fs::rename(&old_path, &new_path)?;
104    Ok(())
105}