1use serde::{Deserialize, Serialize};
9
10pub const MEMORY_TYPES: &[&str] = &["user", "feedback", "project", "reference"];
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum MemoryType {
16 User,
17 Feedback,
18 Project,
19 Reference,
20}
21
22impl MemoryType {
23 pub fn from_str(s: &str) -> Option<Self> {
25 match s.to_lowercase().as_str() {
26 "user" => Some(Self::User),
27 "feedback" => Some(Self::Feedback),
28 "project" => Some(Self::Project),
29 "reference" => Some(Self::Reference),
30 _ => None,
31 }
32 }
33
34 pub fn as_str(&self) -> &'static str {
36 match self {
37 Self::User => "user",
38 Self::Feedback => "feedback",
39 Self::Project => "project",
40 Self::Reference => "reference",
41 }
42 }
43}
44
45impl std::fmt::Display for MemoryType {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.as_str())
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Memory {
54 pub name: String,
55 pub description: String,
56 #[serde(rename = "type")]
57 pub memory_type: MemoryType,
58 pub content: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct MemoryFrontmatter {
64 pub name: String,
65 pub description: String,
66 #[serde(rename = "type")]
67 pub memory_type: MemoryType,
68}
69
70pub fn parse_frontmatter(content: &str) -> Option<MemoryFrontmatter> {
72 let trimmed = content.trim();
73
74 if !trimmed.starts_with("---") {
76 return None;
77 }
78
79 let end_idx = trimmed[3..].find("---")? + 3;
81 let frontmatter = &trimmed[3..end_idx];
82
83 let mut name = String::new();
84 let mut description = String::new();
85 let mut memory_type = MemoryType::User; for line in frontmatter.lines() {
88 let line = line.trim();
89 if let Some((key, value)) = line.split_once(':') {
90 let key = key.trim();
91 let value = value.trim();
92
93 match key {
94 "name" => name = value.to_string(),
95 "description" => description = value.to_string(),
96 "type" => {
97 if let Some(t) = MemoryType::from_str(value) {
98 memory_type = t;
99 }
100 }
101 _ => {}
102 }
103 }
104 }
105
106 if name.is_empty() {
107 return None;
108 }
109
110 Some(MemoryFrontmatter {
111 name,
112 description,
113 memory_type,
114 })
115}
116
117pub fn extract_content(content: &str) -> String {
119 let trimmed = content.trim();
120
121 if !trimmed.starts_with("---") {
122 return trimmed.to_string();
123 }
124
125 if let Some(end_idx) = trimmed[3..].find("---") {
126 let after_frontmatter = &trimmed[3 + end_idx + 3..];
127 after_frontmatter.trim().to_string()
128 } else {
129 trimmed.to_string()
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct EntrypointTruncation {
136 pub content: String,
137 pub line_count: usize,
138 pub byte_count: usize,
139 pub was_line_truncated: bool,
140 pub was_byte_truncated: bool,
141}
142
143pub const MAX_ENTRYPOINT_LINES: usize = 200;
145pub const MAX_ENTRYPOINT_BYTES: usize = 25_000;
147
148pub fn truncate_entrypoint(raw: &str) -> EntrypointTruncation {
150 let trimmed = raw.trim();
151 let content_lines: Vec<&str> = trimmed.lines().collect();
152 let line_count = content_lines.len();
153 let byte_count = trimmed.len();
154
155 let was_line_truncated = line_count > MAX_ENTRYPOINT_LINES;
156 let was_byte_truncated = byte_count > MAX_ENTRYPOINT_BYTES;
157
158 if !was_line_truncated && !byte_count <= MAX_ENTRYPOINT_BYTES {
159 return EntrypointTruncation {
160 content: trimmed.to_string(),
161 line_count,
162 byte_count,
163 was_line_truncated: false,
164 was_byte_truncated: false,
165 };
166 }
167
168 let truncated = if was_line_truncated {
169 content_lines[..MAX_ENTRYPOINT_LINES].join("\n")
170 } else {
171 trimmed.to_string()
172 };
173
174 let truncated = if truncated.len() > MAX_ENTRYPOINT_BYTES {
175 if let Some(cut_at) = truncated.rfind('\n') {
176 if cut_at > MAX_ENTRYPOINT_BYTES {
177 truncated[..cut_at].to_string()
178 } else {
179 truncated[..MAX_ENTRYPOINT_BYTES].to_string()
180 }
181 } else {
182 truncated[..MAX_ENTRYPOINT_BYTES].to_string()
183 }
184 } else {
185 truncated
186 };
187
188 let reason = if was_byte_truncated && !was_line_truncated {
189 format!("{} (limit: {} bytes)", byte_count, MAX_ENTRYPOINT_BYTES)
190 } else if was_line_truncated && !was_byte_truncated {
191 format!("{} lines (limit: {})", line_count, MAX_ENTRYPOINT_LINES)
192 } else {
193 format!("{} lines and {} bytes", line_count, byte_count)
194 };
195
196 let content = format!(
197 "{}\n\n> WARNING: MEMORY.md is {}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.",
198 truncated, reason
199 );
200
201 EntrypointTruncation {
202 content,
203 line_count,
204 byte_count,
205 was_line_truncated,
206 was_byte_truncated,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_memory_type_from_str() {
216 assert_eq!(MemoryType::from_str("user"), Some(MemoryType::User));
217 assert_eq!(MemoryType::from_str("feedback"), Some(MemoryType::Feedback));
218 assert_eq!(MemoryType::from_str("project"), Some(MemoryType::Project));
219 assert_eq!(
220 MemoryType::from_str("reference"),
221 Some(MemoryType::Reference)
222 );
223 assert_eq!(MemoryType::from_str("unknown"), None);
224 }
225
226 #[test]
227 fn test_parse_frontmatter() {
228 let content = r#"---
229name: test_memory
230description: A test memory
231type: user
232---
233
234This is the content."#;
235
236 let fm = parse_frontmatter(content).unwrap();
237 assert_eq!(fm.name, "test_memory");
238 assert_eq!(fm.description, "A test memory");
239 assert_eq!(fm.memory_type, MemoryType::User);
240 }
241
242 #[test]
243 fn test_extract_content() {
244 let content = r#"---
245name: test
246description: test
247type: user
248---
249
250This is the actual content."#;
251
252 let extracted = extract_content(content);
253 assert_eq!(extracted, "This is the actual content.");
254 }
255}