use anyhow::{Context, Result};
use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub struct MemIndex {
pub version: String,
pub user: UserContext,
pub blocks: HashMap<String, BlockMeta>,
pub projects: HashMap<String, ProjectInfo>,
pub concepts: ConceptGraph,
pub session: SessionContext,
pub stats: IndexStats,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserContext {
pub name: String,
pub flags: HashMap<String, bool>,
pub style: StylePrefs,
pub tone: TonePrefs,
pub preferred_cwd: Option<PathBuf>,
pub active_project: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StylePrefs {
pub verbosity: String,
pub bullet_preference: bool,
pub ascii_preferred: bool,
pub code_style: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TonePrefs {
pub humor_level: u8,
pub warning_style: String,
pub explanation_depth: String,
pub encouragement: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockMeta {
pub filename: String,
pub created: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub size: usize,
pub entry_count: usize,
pub topics: Vec<String>,
pub projects: Vec<String>,
pub summary: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
pub name: String,
pub path: PathBuf,
pub status: String,
pub tech_stack: Vec<String>,
pub memory_blocks: Vec<String>,
pub current_focus: Option<String>,
pub notes: Vec<String>,
pub last_activity: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConceptGraph {
pub relationships: HashMap<String, Vec<(String, f32)>>,
pub concept_blocks: HashMap<String, Vec<String>>,
pub recent: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionContext {
pub session_id: String,
pub started: DateTime<Utc>,
pub topics: Vec<String>,
pub accessed_paths: Vec<PathBuf>,
pub tools_used: Vec<String>,
pub nudges: Vec<Nudge>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Nudge {
pub suggestion: String,
pub reason: String,
pub timestamp: DateTime<Utc>,
pub response: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IndexStats {
pub total_blocks: usize,
pub total_size: usize,
pub total_conversations: usize,
pub created: DateTime<Utc>,
pub last_updated: DateTime<Utc>,
pub avg_compression_ratio: f32,
}
impl Default for MemIndex {
fn default() -> Self {
Self::new()
}
}
impl MemIndex {
pub fn load() -> Result<Self> {
let path = Self::index_path()?;
if path.exists() {
let content = fs::read_to_string(&path)?;
let mut index: MemIndex = serde_json::from_str(&content)?;
index.load_user_prefs()?;
Ok(index)
} else {
Ok(Self::new())
}
}
pub fn new() -> Self {
Self {
version: "1.0.0".to_string(),
user: UserContext {
name: whoami::username(),
flags: HashMap::new(),
style: StylePrefs {
verbosity: "normal".to_string(),
bullet_preference: true,
ascii_preferred: false,
code_style: HashMap::new(),
},
tone: TonePrefs {
humor_level: 5,
warning_style: "normal".to_string(),
explanation_depth: "normal".to_string(),
encouragement: true,
},
preferred_cwd: None,
active_project: None,
},
blocks: HashMap::new(),
projects: HashMap::new(),
concepts: ConceptGraph {
relationships: HashMap::new(),
concept_blocks: HashMap::new(),
recent: Vec::new(),
},
session: SessionContext {
session_id: uuid::Uuid::new_v4().to_string(),
started: Utc::now(),
topics: Vec::new(),
accessed_paths: Vec::new(),
tools_used: Vec::new(),
nudges: Vec::new(),
},
stats: IndexStats {
total_blocks: 0,
total_size: 0,
total_conversations: 0,
created: Utc::now(),
last_updated: Utc::now(),
avg_compression_ratio: 0.0,
},
}
}
pub fn save(&self) -> Result<()> {
let path = Self::index_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&path, content)?;
self.save_user_prefs()?;
Ok(())
}
fn index_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not find home directory")?;
Ok(home.join(".mem8").join("memindex.json"))
}
fn load_user_prefs(&mut self) -> Result<()> {
let mem8_dir = dirs::home_dir()
.context("Could not find home directory")?
.join(".mem8");
let flags_path = mem8_dir.join("prefs").join("user_flags.json");
if flags_path.exists() {
let content = fs::read_to_string(&flags_path)?;
self.user.flags = serde_json::from_str(&content)?;
}
let style_path = mem8_dir.join("prefs").join("style.json");
if style_path.exists() {
let content = fs::read_to_string(&style_path)?;
self.user.style = serde_json::from_str(&content)?;
}
let tone_path = mem8_dir.join("prefs").join("tone.json");
if tone_path.exists() {
let content = fs::read_to_string(&tone_path)?;
self.user.tone = serde_json::from_str(&content)?;
}
Ok(())
}
fn save_user_prefs(&self) -> Result<()> {
let prefs_dir = dirs::home_dir()
.context("Could not find home directory")?
.join(".mem8")
.join("prefs");
fs::create_dir_all(&prefs_dir)?;
let flags_content = serde_json::to_string_pretty(&self.user.flags)?;
fs::write(prefs_dir.join("user_flags.json"), flags_content)?;
let style_content = serde_json::to_string_pretty(&self.user.style)?;
fs::write(prefs_dir.join("style.json"), style_content)?;
let tone_content = serde_json::to_string_pretty(&self.user.tone)?;
fs::write(prefs_dir.join("tone.json"), tone_content)?;
Ok(())
}
pub fn register_block(&mut self, filename: &str, path: &Path) -> Result<()> {
let metadata = fs::metadata(path)?;
let block_meta = BlockMeta {
filename: filename.to_string(),
created: Utc::now(),
last_accessed: Utc::now(),
size: metadata.len() as usize,
entry_count: 0, topics: Vec::new(),
projects: Vec::new(),
summary: format!("Memory block: {}", filename),
};
self.blocks.insert(filename.to_string(), block_meta);
self.stats.total_blocks = self.blocks.len();
self.stats.total_size = self.blocks.values().map(|b| b.size).sum();
self.stats.last_updated = Utc::now();
Ok(())
}
pub fn update_project(&mut self, name: &str, path: PathBuf) {
let project = self
.projects
.entry(name.to_string())
.or_insert_with(|| ProjectInfo {
name: name.to_string(),
path: path.clone(),
status: "active".to_string(),
tech_stack: Vec::new(),
memory_blocks: Vec::new(),
current_focus: None,
notes: Vec::new(),
last_activity: Utc::now(),
});
project.last_activity = Utc::now();
self.stats.last_updated = Utc::now();
}
pub fn add_nudge(&mut self, suggestion: &str, reason: &str) {
self.session.nudges.push(Nudge {
suggestion: suggestion.to_string(),
reason: reason.to_string(),
timestamp: Utc::now(),
response: None,
});
}
pub fn add_concept_relation(&mut self, concept1: &str, concept2: &str, weight: f32) {
self.concepts
.relationships
.entry(concept1.to_string())
.or_default()
.push((concept2.to_string(), weight));
self.concepts
.relationships
.entry(concept2.to_string())
.or_default()
.push((concept1.to_string(), weight));
}
pub fn write_journal_entry(&self, content: &str) -> Result<()> {
let journal_dir = dirs::home_dir()
.context("Could not find home directory")?
.join(".mem8")
.join("journal");
fs::create_dir_all(&journal_dir)?;
let today = Local::now().format("%Y-%m-%d");
let journal_path = journal_dir.join(format!("{}.ctx.md", today));
let mut existing = if journal_path.exists() {
fs::read_to_string(&journal_path)?
} else {
format!("# Memory Journal - {}\n\n", today)
};
existing.push_str(&format!(
"\n## {} - Session {}\n\n",
Local::now().format("%H:%M"),
&self.session.session_id[..8]
));
existing.push_str(content);
existing.push('\n');
fs::write(&journal_path, existing)?;
Ok(())
}
}