Skip to main content

st/mem8/
memindex.rs

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/// Main memory index - the unified relationship file
9#[derive(Debug, Serialize, Deserialize)]
10pub struct MemIndex {
11    /// Index version
12    pub version: String,
13
14    /// User identification and context
15    pub user: UserContext,
16
17    /// All memory blocks with metadata
18    pub blocks: HashMap<String, BlockMeta>,
19
20    /// Active projects and their relationships
21    pub projects: HashMap<String, ProjectInfo>,
22
23    /// Concept graph - relationships between ideas
24    pub concepts: ConceptGraph,
25
26    /// Current session context
27    pub session: SessionContext,
28
29    /// Statistics and metadata
30    pub stats: IndexStats,
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct UserContext {
35    /// User identifier (name or handle)
36    pub name: String,
37
38    /// Quick preference flags (loaded from prefs/user_flags.json)
39    pub flags: HashMap<String, bool>,
40
41    /// Style preferences (loaded from prefs/style.json)
42    pub style: StylePrefs,
43
44    /// Communication tone (loaded from prefs/tone.json)
45    pub tone: TonePrefs,
46
47    /// Current working directory preference
48    pub preferred_cwd: Option<PathBuf>,
49
50    /// Active project (if any)
51    pub active_project: Option<String>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct StylePrefs {
56    /// Output style: terse, normal, verbose
57    pub verbosity: String,
58
59    /// Prefers bullet points
60    pub bullet_preference: bool,
61
62    /// ASCII over emoji
63    pub ascii_preferred: bool,
64
65    /// Code style preferences
66    pub code_style: HashMap<String, String>,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70pub struct TonePrefs {
71    /// Humor level 0-10
72    pub humor_level: u8,
73
74    /// Warning verbosity
75    pub warning_style: String, // "minimal", "normal", "detailed"
76
77    /// Explanation depth
78    pub explanation_depth: String, // "eli5", "normal", "expert"
79
80    /// Encouragement style
81    pub encouragement: bool,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85pub struct BlockMeta {
86    /// Filename in blocks/ directory
87    pub filename: String,
88
89    /// When this block was created
90    pub created: DateTime<Utc>,
91
92    /// Last accessed time
93    pub last_accessed: DateTime<Utc>,
94
95    /// Size in bytes
96    pub size: usize,
97
98    /// Number of messages/entries
99    pub entry_count: usize,
100
101    /// Key topics/concepts in this block
102    pub topics: Vec<String>,
103
104    /// Related projects
105    pub projects: Vec<String>,
106
107    /// Quick summary
108    pub summary: String,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
112pub struct ProjectInfo {
113    /// Project name
114    pub name: String,
115
116    /// Project root path
117    pub path: PathBuf,
118
119    /// Current status
120    pub status: String, // "active", "paused", "completed"
121
122    /// Technologies used
123    pub tech_stack: Vec<String>,
124
125    /// Related memory blocks
126    pub memory_blocks: Vec<String>,
127
128    /// Current focus/task
129    pub current_focus: Option<String>,
130
131    /// Key decisions/notes
132    pub notes: Vec<String>,
133
134    /// Last activity
135    pub last_activity: DateTime<Utc>,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct ConceptGraph {
140    /// Concept -> Related concepts with weight
141    pub relationships: HashMap<String, Vec<(String, f32)>>,
142
143    /// Concept -> Memory blocks containing it
144    pub concept_blocks: HashMap<String, Vec<String>>,
145
146    /// Recent concepts (for quick access)
147    pub recent: Vec<String>,
148}
149
150#[derive(Debug, Serialize, Deserialize)]
151pub struct SessionContext {
152    /// Current session ID
153    pub session_id: String,
154
155    /// Session start time
156    pub started: DateTime<Utc>,
157
158    /// Topics discussed this session
159    pub topics: Vec<String>,
160
161    /// Files/directories accessed
162    pub accessed_paths: Vec<PathBuf>,
163
164    /// Tools used
165    pub tools_used: Vec<String>,
166
167    /// Nudges given (what we suggested)
168    pub nudges: Vec<Nudge>,
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172pub struct Nudge {
173    /// What was suggested
174    pub suggestion: String,
175
176    /// Why it was suggested
177    pub reason: String,
178
179    /// When it was suggested
180    pub timestamp: DateTime<Utc>,
181
182    /// Was it accepted/rejected/ignored
183    pub response: Option<String>,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187pub struct IndexStats {
188    /// Total memory blocks
189    pub total_blocks: usize,
190
191    /// Total size of all blocks
192    pub total_size: usize,
193
194    /// Total conversations
195    pub total_conversations: usize,
196
197    /// Index created date
198    pub created: DateTime<Utc>,
199
200    /// Last updated
201    pub last_updated: DateTime<Utc>,
202
203    /// Compression ratio achieved
204    pub avg_compression_ratio: f32,
205}
206
207impl Default for MemIndex {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl MemIndex {
214    /// Load the index from ~/.mem8/memindex.json
215    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            // Load user preferences
223            index.load_user_prefs()?;
224
225            Ok(index)
226        } else {
227            Ok(Self::new())
228        }
229    }
230
231    /// Create a new index
232    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    /// Save the index
280    pub fn save(&self) -> Result<()> {
281        let path = Self::index_path()?;
282
283        // Ensure directory exists
284        if let Some(parent) = path.parent() {
285            fs::create_dir_all(parent)?;
286        }
287
288        // Save main index
289        let content = serde_json::to_string_pretty(self)?;
290        fs::write(&path, content)?;
291
292        // Save user preferences
293        self.save_user_prefs()?;
294
295        Ok(())
296    }
297
298    /// Get index file path
299    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    /// Load user preferences from separate files
305    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        // Load user flags
311        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        // Load style preferences
318        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        // Load tone preferences
325        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    /// Save user preferences to separate files
335    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        // Save user flags
344        let flags_content = serde_json::to_string_pretty(&self.user.flags)?;
345        fs::write(prefs_dir.join("user_flags.json"), flags_content)?;
346
347        // Save style
348        let style_content = serde_json::to_string_pretty(&self.user.style)?;
349        fs::write(prefs_dir.join("style.json"), style_content)?;
350
351        // Save tone
352        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    /// Register a new memory block
359    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, // Would be extracted from .m8 file
368            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    /// Add or update a project
382    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    /// Record a nudge given to the user
402    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    /// Update concept relationships
412    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    /// Write daily journal entry
427    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        // Append to existing or create new
439        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}