codelens_engine/memory/
paths.rs1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, bail};
4use serde::{Deserialize, Serialize};
5
6use super::policy::POLICY_FILENAME;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum MemoryTier {
11 Project,
13 Global,
15}
16
17impl MemoryTier {
18 pub fn as_str(&self) -> &'static str {
19 match self {
20 Self::Project => "project",
21 Self::Global => "global",
22 }
23 }
24}
25
26#[derive(Debug, Clone)]
28pub struct MemoryLocation {
29 pub tier: MemoryTier,
30 pub dir: PathBuf,
31 pub path: PathBuf,
32}
33
34pub fn resolve_memory_tier(
36 name: &str,
37 project_dir: &Path,
38 global_dir: Option<&Path>,
39) -> MemoryLocation {
40 if let Some(stripped) = name.strip_prefix("global/") {
41 let stripped = stripped.trim_start_matches('/');
42 if let Some(gdir) = global_dir {
43 let path = resolve_memory_path(gdir, stripped)
44 .unwrap_or_else(|_| gdir.join(format!("{stripped}.md")));
45 return MemoryLocation {
46 tier: MemoryTier::Global,
47 dir: gdir.to_path_buf(),
48 path,
49 };
50 }
51 }
52
53 let project_memories = project_dir.join(".codelens").join("memories");
54 let project_path = resolve_memory_path(&project_memories, name);
55 if let Ok(path) = &project_path
56 && path.is_file()
57 {
58 return MemoryLocation {
59 tier: MemoryTier::Project,
60 dir: project_memories,
61 path: path.clone(),
62 };
63 }
64
65 if let Some(gdir) = global_dir {
66 let global_path = resolve_memory_path(gdir, name);
67 if let Ok(path) = &global_path
68 && path.is_file()
69 {
70 return MemoryLocation {
71 tier: MemoryTier::Global,
72 dir: gdir.to_path_buf(),
73 path: path.clone(),
74 };
75 }
76 }
77
78 let fallback_dir = project_dir.join(".codelens").join("memories");
79 MemoryLocation {
80 tier: MemoryTier::Project,
81 dir: fallback_dir.clone(),
82 path: project_path.unwrap_or_else(|_| fallback_dir.join(format!("{name}.md"))),
83 }
84}
85
86pub fn global_memory_dir() -> Option<PathBuf> {
88 std::env::var_os("HOME")
89 .map(PathBuf::from)
90 .map(|home| home.join(".codelens").join("memories"))
91}
92
93pub fn resolve_memory_path(memories_dir: &Path, name: &str) -> Result<PathBuf> {
95 let normalized = name
96 .trim()
97 .replace('\\', "/")
98 .trim_matches('/')
99 .trim_end_matches(".md")
100 .to_string();
101 if normalized.is_empty() {
102 bail!("memory name must not be empty");
103 }
104 if normalized.contains("..") {
105 bail!("memory path must not contain '..': {name}");
106 }
107 Ok(memories_dir.join(format!("{normalized}.md")))
108}
109
110pub(crate) fn collect_memory_files(base: &Path, dir: &Path, names: &mut Vec<String>) {
111 let entries = match std::fs::read_dir(dir) {
112 Ok(e) => e,
113 Err(_) => return,
114 };
115 for entry in entries.flatten() {
116 let path = entry.path();
117 if path.is_dir() {
118 if let Some(fname) = path.file_name().and_then(|n| n.to_str())
119 && fname.starts_with('.')
120 {
121 continue;
122 }
123 collect_memory_files(base, &path, names);
124 } else if path.extension().and_then(|e| e.to_str()) == Some("md")
125 && let Ok(rel) = path.strip_prefix(base)
126 {
127 let name = rel
128 .to_string_lossy()
129 .replace('\\', "/")
130 .trim_end_matches(".md")
131 .to_string();
132 if name != POLICY_FILENAME {
133 names.push(name);
134 }
135 }
136 }
137}