Skip to main content

bamboo_memory/memory_store/
paths.rs

1use std::path::{Path, PathBuf};
2
3use bamboo_config::paths::bamboo_dir;
4
5use super::types::MemoryScope;
6
7pub const MEMORY_ROOT_DIR: &str = "memory";
8pub const MEMORY_VERSION_DIR: &str = "v1";
9pub const SESSIONS_DIR: &str = "sessions";
10pub const SCOPES_DIR: &str = "scopes";
11pub const GLOBAL_DIR: &str = "global";
12pub const PROJECTS_DIR: &str = "projects";
13pub const NOTE_DIR: &str = "note";
14pub const STATE_DIR: &str = "state";
15pub const INDEXES_DIR: &str = "indexes";
16pub const VIEWS_DIR: &str = "views";
17pub const LOGS_DIR: &str = "logs";
18pub const TOPICS_DIR: &str = "topics";
19
20#[derive(Debug, Clone)]
21pub struct MemoryPathResolver {
22    data_dir: PathBuf,
23    root: PathBuf,
24}
25
26impl Default for MemoryPathResolver {
27    fn default() -> Self {
28        Self::from_data_dir(bamboo_dir())
29    }
30}
31
32impl MemoryPathResolver {
33    pub fn new(root: impl Into<PathBuf>) -> Self {
34        let root = root.into();
35        let data_dir = infer_data_dir_from_root(&root);
36        Self { data_dir, root }
37    }
38
39    pub fn from_data_dir(data_dir: impl Into<PathBuf>) -> Self {
40        let data_dir = data_dir.into();
41        let root = data_dir.join(MEMORY_ROOT_DIR).join(MEMORY_VERSION_DIR);
42        Self { data_dir, root }
43    }
44
45    pub fn data_dir(&self) -> PathBuf {
46        self.data_dir.clone()
47    }
48
49    pub fn root(&self) -> PathBuf {
50        self.root.clone()
51    }
52
53    pub fn sessions_root(&self) -> PathBuf {
54        self.root.join(SESSIONS_DIR)
55    }
56
57    pub fn session_root(&self, session_id: &str) -> PathBuf {
58        self.sessions_root().join(session_id)
59    }
60
61    pub fn session_note_dir(&self, session_id: &str) -> PathBuf {
62        self.session_root(session_id).join(NOTE_DIR)
63    }
64
65    pub fn session_topic_path(&self, session_id: &str, topic: &str) -> PathBuf {
66        self.session_note_dir(session_id)
67            .join(format!("{}.md", topic))
68    }
69
70    pub fn session_state_path(&self, session_id: &str) -> PathBuf {
71        self.session_root(session_id).join("state.json")
72    }
73
74    pub fn scopes_root(&self) -> PathBuf {
75        self.root.join(SCOPES_DIR)
76    }
77
78    pub fn global_root(&self) -> PathBuf {
79        self.scopes_root().join(GLOBAL_DIR)
80    }
81
82    pub fn project_root(&self, project_key: &str) -> PathBuf {
83        self.scopes_root().join(PROJECTS_DIR).join(project_key)
84    }
85
86    pub fn scope_root(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
87        match scope {
88            MemoryScope::Global => self.global_root(),
89            MemoryScope::Project => self.project_root(project_key.unwrap_or("unknown")),
90            MemoryScope::Session => self.sessions_root(),
91        }
92    }
93
94    pub fn topic_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
95        match scope {
96            MemoryScope::Global => self.global_root().join(TOPICS_DIR),
97            MemoryScope::Project => self
98                .project_root(project_key.unwrap_or("unknown"))
99                .join(TOPICS_DIR),
100            MemoryScope::Session => self.sessions_root(),
101        }
102    }
103
104    pub fn topic_path(
105        &self,
106        scope: MemoryScope,
107        project_key: Option<&str>,
108        memory_id: &str,
109    ) -> PathBuf {
110        self.topic_dir(scope, project_key)
111            .join(format!("{}.md", memory_id))
112    }
113
114    pub fn indexes_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
115        match scope {
116            MemoryScope::Global => self.global_root().join(INDEXES_DIR),
117            MemoryScope::Project => self
118                .project_root(project_key.unwrap_or("unknown"))
119                .join(INDEXES_DIR),
120            MemoryScope::Session => self.sessions_root(),
121        }
122    }
123
124    pub fn views_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
125        match scope {
126            MemoryScope::Global => self.global_root().join(VIEWS_DIR),
127            MemoryScope::Project => self
128                .project_root(project_key.unwrap_or("unknown"))
129                .join(VIEWS_DIR),
130            MemoryScope::Session => self.sessions_root(),
131        }
132    }
133
134    pub fn logs_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
135        match scope {
136            MemoryScope::Global => self.global_root().join(LOGS_DIR),
137            MemoryScope::Project => self
138                .project_root(project_key.unwrap_or("unknown"))
139                .join(LOGS_DIR),
140            MemoryScope::Session => self.sessions_root(),
141        }
142    }
143
144    pub fn state_dir(&self, scope: MemoryScope, project_key: Option<&str>) -> PathBuf {
145        match scope {
146            MemoryScope::Global => self.global_root().join(STATE_DIR),
147            MemoryScope::Project => self
148                .project_root(project_key.unwrap_or("unknown"))
149                .join(STATE_DIR),
150            MemoryScope::Session => self.sessions_root(),
151        }
152    }
153
154    pub fn legacy_notes_root(&self) -> PathBuf {
155        self.data_dir.join("notes")
156    }
157
158    pub fn legacy_session_file(&self, session_id: &str) -> PathBuf {
159        self.legacy_notes_root().join(format!("{}.md", session_id))
160    }
161
162    pub fn legacy_session_topic_dir(&self, session_id: &str) -> PathBuf {
163        self.legacy_notes_root().join(session_id)
164    }
165}
166
167pub fn memory_root() -> PathBuf {
168    MemoryPathResolver::from_data_dir(bamboo_dir()).root()
169}
170
171fn infer_data_dir_from_root(root: &Path) -> PathBuf {
172    if root
173        .file_name()
174        .and_then(|value| value.to_str())
175        .is_some_and(|value| value == MEMORY_VERSION_DIR)
176    {
177        if let Some(memory_dir) = root.parent() {
178            if memory_dir
179                .file_name()
180                .and_then(|value| value.to_str())
181                .is_some_and(|value| value == MEMORY_ROOT_DIR)
182            {
183                if let Some(data_dir) = memory_dir.parent() {
184                    return data_dir.to_path_buf();
185                }
186            }
187        }
188    }
189    root.to_path_buf()
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn paths_follow_v1_layout() {
198        let resolver = MemoryPathResolver::new("/tmp/memory/v1");
199        assert_eq!(
200            resolver.session_topic_path("session-1", "default"),
201            PathBuf::from("/tmp/memory/v1/sessions/session-1/note/default.md")
202        );
203        assert_eq!(
204            resolver.topic_path(MemoryScope::Project, Some("proj-1"), "mem_1"),
205            PathBuf::from("/tmp/memory/v1/scopes/projects/proj-1/topics/mem_1.md")
206        );
207    }
208}