1use std::fs;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use chrono::Utc;
8use tracing::debug;
9
10use crate::parse::{
11 parse_index, parse_memory, remove_index_line, render_memory, upsert_index_line, validate_slug,
12};
13use crate::{Memory, MemoryRow, MemoryStore};
14
15const INDEX_FILE: &str = "MEMORY.md";
16const INDEX_HEADER: &str = "# Memory\n";
17
18impl MemoryStore {
19 pub fn open(dir: impl Into<PathBuf>) -> Result<Self> {
22 let dir = dir.into();
23 if !dir.exists() {
24 fs::create_dir_all(&dir)
25 .with_context(|| format!("failed to create memory dir {}", dir.display()))?;
26 }
27 let index = dir.join(INDEX_FILE);
28 if !index.exists() {
29 fs::write(&index, INDEX_HEADER)
30 .with_context(|| format!("failed to create {}", index.display()))?;
31 }
32 Ok(Self { dir })
33 }
34
35 pub fn open_default() -> Result<Self> {
38 let dir = if let Some(home) = std::env::var_os("MERLION_HOME") {
39 PathBuf::from(home).join("memory")
40 } else {
41 let home = dirs::home_dir()
42 .context("could not resolve home directory for default memory store")?;
43 home.join(".merlion").join("memory")
44 };
45 Self::open(dir)
46 }
47
48 pub fn dir(&self) -> &Path {
50 &self.dir
51 }
52
53 fn index_path(&self) -> PathBuf {
54 self.dir.join(INDEX_FILE)
55 }
56
57 fn memory_path(&self, name: &str) -> PathBuf {
58 self.dir.join(format!("{}.md", name))
59 }
60
61 fn read_index(&self) -> Result<String> {
62 let path = self.index_path();
63 if !path.exists() {
64 return Ok(INDEX_HEADER.to_string());
65 }
66 fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))
67 }
68
69 fn write_index(&self, text: &str) -> Result<()> {
70 let path = self.index_path();
71 fs::write(&path, text).with_context(|| format!("failed to write {}", path.display()))
72 }
73
74 pub fn list(&self) -> Result<Vec<MemoryRow>> {
76 let text = self.read_index()?;
77 Ok(parse_index(&text))
78 }
79
80 pub fn read(&self, name: &str) -> Result<Memory> {
82 validate_slug(name)?;
83 let path = self.memory_path(name);
84 let raw = fs::read_to_string(&path)
85 .with_context(|| format!("failed to read memory `{}` at {}", name, path.display()))?;
86 parse_memory(&raw).with_context(|| format!("failed to parse memory `{}`", name))
87 }
88
89 pub fn write(&self, m: &Memory) -> Result<()> {
95 validate_slug(&m.name)?;
96 let path = self.memory_path(&m.name);
97
98 let now = Utc::now();
99 let created_at = if path.exists() {
100 match fs::read_to_string(&path)
102 .ok()
103 .and_then(|raw| parse_memory(&raw).ok())
104 {
105 Some(existing) => existing.created_at,
106 None => m.created_at,
107 }
108 } else {
109 m.created_at
111 };
112
113 let to_write = Memory {
114 name: m.name.clone(),
115 description: m.description.clone(),
116 kind: m.kind.clone(),
117 body: m.body.clone(),
118 created_at,
119 updated_at: now,
120 };
121
122 let rendered = render_memory(&to_write)?;
123 fs::write(&path, rendered).with_context(|| {
124 format!("failed to write memory `{}` at {}", m.name, path.display())
125 })?;
126
127 let title = to_write.name.clone();
132 let index_text = self.read_index()?;
133 let new_index =
134 upsert_index_line(&index_text, &to_write.name, &title, &to_write.description);
135 self.write_index(&new_index)?;
136
137 debug!(name = %to_write.name, "wrote memory");
138 Ok(())
139 }
140
141 pub fn delete(&self, name: &str) -> Result<()> {
144 validate_slug(name)?;
145 let path = self.memory_path(name);
146 if path.exists() {
147 fs::remove_file(&path).with_context(|| {
148 format!("failed to remove memory `{}` at {}", name, path.display())
149 })?;
150 }
151 let index_text = self.read_index()?;
152 let new_index = remove_index_line(&index_text, name);
153 if new_index != index_text {
154 self.write_index(&new_index)?;
155 }
156 debug!(name = %name, "deleted memory");
157 Ok(())
158 }
159
160 pub fn render_context_block(&self, max_chars: usize) -> Result<String> {
164 let rows = self.list()?;
165 if rows.is_empty() {
166 return Ok(String::new());
167 }
168 let header = format!("# Persistent memory ({} entries)\n", rows.len());
169 let mut out = String::new();
170 if header.len() > max_chars {
171 return Ok(String::new());
173 }
174 out.push_str(&header);
175 for row in rows {
176 let line = format!("- [{}] {}\n", row.name, row.hook);
177 if out.len() + line.len() > max_chars {
178 break;
179 }
180 out.push_str(&line);
181 }
182 Ok(out)
183 }
184}