use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use chrono::Utc;
use tracing::debug;
use crate::parse::{
parse_index, parse_memory, remove_index_line, render_memory, upsert_index_line,
validate_slug,
};
use crate::{Memory, MemoryRow, MemoryStore};
const INDEX_FILE: &str = "MEMORY.md";
const INDEX_HEADER: &str = "# Memory\n";
impl MemoryStore {
pub fn open(dir: impl Into<PathBuf>) -> Result<Self> {
let dir = dir.into();
if !dir.exists() {
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create memory dir {}", dir.display()))?;
}
let index = dir.join(INDEX_FILE);
if !index.exists() {
fs::write(&index, INDEX_HEADER)
.with_context(|| format!("failed to create {}", index.display()))?;
}
Ok(Self { dir })
}
pub fn open_default() -> Result<Self> {
let dir = if let Some(home) = std::env::var_os("MERLION_HOME") {
PathBuf::from(home).join("memory")
} else {
let home = dirs::home_dir()
.context("could not resolve home directory for default memory store")?;
home.join(".merlion").join("memory")
};
Self::open(dir)
}
pub fn dir(&self) -> &Path {
&self.dir
}
fn index_path(&self) -> PathBuf {
self.dir.join(INDEX_FILE)
}
fn memory_path(&self, name: &str) -> PathBuf {
self.dir.join(format!("{}.md", name))
}
fn read_index(&self) -> Result<String> {
let path = self.index_path();
if !path.exists() {
return Ok(INDEX_HEADER.to_string());
}
fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))
}
fn write_index(&self, text: &str) -> Result<()> {
let path = self.index_path();
fs::write(&path, text)
.with_context(|| format!("failed to write {}", path.display()))
}
pub fn list(&self) -> Result<Vec<MemoryRow>> {
let text = self.read_index()?;
Ok(parse_index(&text))
}
pub fn read(&self, name: &str) -> Result<Memory> {
validate_slug(name)?;
let path = self.memory_path(name);
let raw = fs::read_to_string(&path)
.with_context(|| format!("failed to read memory `{}` at {}", name, path.display()))?;
parse_memory(&raw).with_context(|| format!("failed to parse memory `{}`", name))
}
pub fn write(&self, m: &Memory) -> Result<()> {
validate_slug(&m.name)?;
let path = self.memory_path(&m.name);
let now = Utc::now();
let created_at = if path.exists() {
match fs::read_to_string(&path).ok().and_then(|raw| parse_memory(&raw).ok()) {
Some(existing) => existing.created_at,
None => m.created_at,
}
} else {
m.created_at
};
let to_write = Memory {
name: m.name.clone(),
description: m.description.clone(),
kind: m.kind.clone(),
body: m.body.clone(),
created_at,
updated_at: now,
};
let rendered = render_memory(&to_write)?;
fs::write(&path, rendered)
.with_context(|| format!("failed to write memory `{}` at {}", m.name, path.display()))?;
let title = to_write.name.clone();
let index_text = self.read_index()?;
let new_index = upsert_index_line(&index_text, &to_write.name, &title, &to_write.description);
self.write_index(&new_index)?;
debug!(name = %to_write.name, "wrote memory");
Ok(())
}
pub fn delete(&self, name: &str) -> Result<()> {
validate_slug(name)?;
let path = self.memory_path(name);
if path.exists() {
fs::remove_file(&path)
.with_context(|| format!("failed to remove memory `{}` at {}", name, path.display()))?;
}
let index_text = self.read_index()?;
let new_index = remove_index_line(&index_text, name);
if new_index != index_text {
self.write_index(&new_index)?;
}
debug!(name = %name, "deleted memory");
Ok(())
}
pub fn render_context_block(&self, max_chars: usize) -> Result<String> {
let rows = self.list()?;
if rows.is_empty() {
return Ok(String::new());
}
let header = format!("# Persistent memory ({} entries)\n", rows.len());
let mut out = String::new();
if header.len() > max_chars {
return Ok(String::new());
}
out.push_str(&header);
for row in rows {
let line = format!("- [{}] {}\n", row.name, row.hook);
if out.len() + line.len() > max_chars {
break;
}
out.push_str(&line);
}
Ok(out)
}
}