1use anyhow::{Context, Result};
2use chrono::{DateTime, Local, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Serialize, Deserialize)]
10pub struct MemIndex {
11 pub version: String,
13
14 pub user: UserContext,
16
17 pub blocks: HashMap<String, BlockMeta>,
19
20 pub projects: HashMap<String, ProjectInfo>,
22
23 pub concepts: ConceptGraph,
25
26 pub session: SessionContext,
28
29 pub stats: IndexStats,
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct UserContext {
35 pub name: String,
37
38 pub flags: HashMap<String, bool>,
40
41 pub style: StylePrefs,
43
44 pub tone: TonePrefs,
46
47 pub preferred_cwd: Option<PathBuf>,
49
50 pub active_project: Option<String>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct StylePrefs {
56 pub verbosity: String,
58
59 pub bullet_preference: bool,
61
62 pub ascii_preferred: bool,
64
65 pub code_style: HashMap<String, String>,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70pub struct TonePrefs {
71 pub humor_level: u8,
73
74 pub warning_style: String, pub explanation_depth: String, pub encouragement: bool,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85pub struct BlockMeta {
86 pub filename: String,
88
89 pub created: DateTime<Utc>,
91
92 pub last_accessed: DateTime<Utc>,
94
95 pub size: usize,
97
98 pub entry_count: usize,
100
101 pub topics: Vec<String>,
103
104 pub projects: Vec<String>,
106
107 pub summary: String,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
112pub struct ProjectInfo {
113 pub name: String,
115
116 pub path: PathBuf,
118
119 pub status: String, pub tech_stack: Vec<String>,
124
125 pub memory_blocks: Vec<String>,
127
128 pub current_focus: Option<String>,
130
131 pub notes: Vec<String>,
133
134 pub last_activity: DateTime<Utc>,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct ConceptGraph {
140 pub relationships: HashMap<String, Vec<(String, f32)>>,
142
143 pub concept_blocks: HashMap<String, Vec<String>>,
145
146 pub recent: Vec<String>,
148}
149
150#[derive(Debug, Serialize, Deserialize)]
151pub struct SessionContext {
152 pub session_id: String,
154
155 pub started: DateTime<Utc>,
157
158 pub topics: Vec<String>,
160
161 pub accessed_paths: Vec<PathBuf>,
163
164 pub tools_used: Vec<String>,
166
167 pub nudges: Vec<Nudge>,
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172pub struct Nudge {
173 pub suggestion: String,
175
176 pub reason: String,
178
179 pub timestamp: DateTime<Utc>,
181
182 pub response: Option<String>,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187pub struct IndexStats {
188 pub total_blocks: usize,
190
191 pub total_size: usize,
193
194 pub total_conversations: usize,
196
197 pub created: DateTime<Utc>,
199
200 pub last_updated: DateTime<Utc>,
202
203 pub avg_compression_ratio: f32,
205}
206
207impl Default for MemIndex {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl MemIndex {
214 pub fn load() -> Result<Self> {
216 let path = Self::index_path()?;
217
218 if path.exists() {
219 let content = fs::read_to_string(&path)?;
220 let mut index: MemIndex = serde_json::from_str(&content)?;
221
222 index.load_user_prefs()?;
224
225 Ok(index)
226 } else {
227 Ok(Self::new())
228 }
229 }
230
231 pub fn new() -> Self {
233 Self {
234 version: "1.0.0".to_string(),
235 user: UserContext {
236 name: whoami::username(),
237 flags: HashMap::new(),
238 style: StylePrefs {
239 verbosity: "normal".to_string(),
240 bullet_preference: true,
241 ascii_preferred: false,
242 code_style: HashMap::new(),
243 },
244 tone: TonePrefs {
245 humor_level: 5,
246 warning_style: "normal".to_string(),
247 explanation_depth: "normal".to_string(),
248 encouragement: true,
249 },
250 preferred_cwd: None,
251 active_project: None,
252 },
253 blocks: HashMap::new(),
254 projects: HashMap::new(),
255 concepts: ConceptGraph {
256 relationships: HashMap::new(),
257 concept_blocks: HashMap::new(),
258 recent: Vec::new(),
259 },
260 session: SessionContext {
261 session_id: uuid::Uuid::new_v4().to_string(),
262 started: Utc::now(),
263 topics: Vec::new(),
264 accessed_paths: Vec::new(),
265 tools_used: Vec::new(),
266 nudges: Vec::new(),
267 },
268 stats: IndexStats {
269 total_blocks: 0,
270 total_size: 0,
271 total_conversations: 0,
272 created: Utc::now(),
273 last_updated: Utc::now(),
274 avg_compression_ratio: 0.0,
275 },
276 }
277 }
278
279 pub fn save(&self) -> Result<()> {
281 let path = Self::index_path()?;
282
283 if let Some(parent) = path.parent() {
285 fs::create_dir_all(parent)?;
286 }
287
288 let content = serde_json::to_string_pretty(self)?;
290 fs::write(&path, content)?;
291
292 self.save_user_prefs()?;
294
295 Ok(())
296 }
297
298 fn index_path() -> Result<PathBuf> {
300 let home = dirs::home_dir().context("Could not find home directory")?;
301 Ok(home.join(".mem8").join("memindex.json"))
302 }
303
304 fn load_user_prefs(&mut self) -> Result<()> {
306 let mem8_dir = dirs::home_dir()
307 .context("Could not find home directory")?
308 .join(".mem8");
309
310 let flags_path = mem8_dir.join("prefs").join("user_flags.json");
312 if flags_path.exists() {
313 let content = fs::read_to_string(&flags_path)?;
314 self.user.flags = serde_json::from_str(&content)?;
315 }
316
317 let style_path = mem8_dir.join("prefs").join("style.json");
319 if style_path.exists() {
320 let content = fs::read_to_string(&style_path)?;
321 self.user.style = serde_json::from_str(&content)?;
322 }
323
324 let tone_path = mem8_dir.join("prefs").join("tone.json");
326 if tone_path.exists() {
327 let content = fs::read_to_string(&tone_path)?;
328 self.user.tone = serde_json::from_str(&content)?;
329 }
330
331 Ok(())
332 }
333
334 fn save_user_prefs(&self) -> Result<()> {
336 let prefs_dir = dirs::home_dir()
337 .context("Could not find home directory")?
338 .join(".mem8")
339 .join("prefs");
340
341 fs::create_dir_all(&prefs_dir)?;
342
343 let flags_content = serde_json::to_string_pretty(&self.user.flags)?;
345 fs::write(prefs_dir.join("user_flags.json"), flags_content)?;
346
347 let style_content = serde_json::to_string_pretty(&self.user.style)?;
349 fs::write(prefs_dir.join("style.json"), style_content)?;
350
351 let tone_content = serde_json::to_string_pretty(&self.user.tone)?;
353 fs::write(prefs_dir.join("tone.json"), tone_content)?;
354
355 Ok(())
356 }
357
358 pub fn register_block(&mut self, filename: &str, path: &Path) -> Result<()> {
360 let metadata = fs::metadata(path)?;
361
362 let block_meta = BlockMeta {
363 filename: filename.to_string(),
364 created: Utc::now(),
365 last_accessed: Utc::now(),
366 size: metadata.len() as usize,
367 entry_count: 0, topics: Vec::new(),
369 projects: Vec::new(),
370 summary: format!("Memory block: {}", filename),
371 };
372
373 self.blocks.insert(filename.to_string(), block_meta);
374 self.stats.total_blocks = self.blocks.len();
375 self.stats.total_size = self.blocks.values().map(|b| b.size).sum();
376 self.stats.last_updated = Utc::now();
377
378 Ok(())
379 }
380
381 pub fn update_project(&mut self, name: &str, path: PathBuf) {
383 let project = self
384 .projects
385 .entry(name.to_string())
386 .or_insert_with(|| ProjectInfo {
387 name: name.to_string(),
388 path: path.clone(),
389 status: "active".to_string(),
390 tech_stack: Vec::new(),
391 memory_blocks: Vec::new(),
392 current_focus: None,
393 notes: Vec::new(),
394 last_activity: Utc::now(),
395 });
396
397 project.last_activity = Utc::now();
398 self.stats.last_updated = Utc::now();
399 }
400
401 pub fn add_nudge(&mut self, suggestion: &str, reason: &str) {
403 self.session.nudges.push(Nudge {
404 suggestion: suggestion.to_string(),
405 reason: reason.to_string(),
406 timestamp: Utc::now(),
407 response: None,
408 });
409 }
410
411 pub fn add_concept_relation(&mut self, concept1: &str, concept2: &str, weight: f32) {
413 self.concepts
414 .relationships
415 .entry(concept1.to_string())
416 .or_default()
417 .push((concept2.to_string(), weight));
418
419 self.concepts
420 .relationships
421 .entry(concept2.to_string())
422 .or_default()
423 .push((concept1.to_string(), weight));
424 }
425
426 pub fn write_journal_entry(&self, content: &str) -> Result<()> {
428 let journal_dir = dirs::home_dir()
429 .context("Could not find home directory")?
430 .join(".mem8")
431 .join("journal");
432
433 fs::create_dir_all(&journal_dir)?;
434
435 let today = Local::now().format("%Y-%m-%d");
436 let journal_path = journal_dir.join(format!("{}.ctx.md", today));
437
438 let mut existing = if journal_path.exists() {
440 fs::read_to_string(&journal_path)?
441 } else {
442 format!("# Memory Journal - {}\n\n", today)
443 };
444
445 existing.push_str(&format!(
446 "\n## {} - Session {}\n\n",
447 Local::now().format("%H:%M"),
448 &self.session.session_id[..8]
449 ));
450 existing.push_str(content);
451 existing.push('\n');
452
453 fs::write(&journal_path, existing)?;
454
455 Ok(())
456 }
457}