use dejadb_core::error::{Hash, DejaDbError, Result};
use dejadb_core::types::Fact;
use serde_json::{json, Value};
use crate::DejaDB;
pub const MEMORY_FILE_RELATION: &str = "memory_file";
const ROOT: &str = "/memories";
pub struct MemoryTool<'a> {
m: &'a mut DejaDB,
ns: String,
}
impl<'a> MemoryTool<'a> {
pub fn new(m: &'a mut DejaDB, ns: &str) -> Self {
MemoryTool { m, ns: ns.to_string() }
}
pub fn execute(&mut self, cmd: &Value) -> Result<String> {
let command = cmd.get("command").and_then(|v| v.as_str()).unwrap_or("");
let path = cmd.get("path").and_then(|v| v.as_str()).unwrap_or("");
match command {
"view" => self.view(path),
"create" => self.create(
path,
cmd.get("file_text").and_then(|v| v.as_str()).unwrap_or(""),
),
"str_replace" => self.str_replace(
path,
cmd.get("old_str").and_then(|v| v.as_str()).unwrap_or(""),
cmd.get("new_str").and_then(|v| v.as_str()).unwrap_or(""),
),
"insert" => self.insert(
path,
cmd.get("insert_line").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
cmd.get("insert_text").and_then(|v| v.as_str()).unwrap_or(""),
),
"delete" => self.delete(path),
"rename" => self.rename(
cmd.get("old_path").and_then(|v| v.as_str()).unwrap_or(path),
cmd.get("new_path").and_then(|v| v.as_str()).unwrap_or(""),
),
other => Err(DejaDbError::Validation(format!(
"unknown memory command: {other}"
))),
}
}
fn check_path(path: &str) -> Result<()> {
if !path.starts_with(ROOT) || path.contains("..") || path.contains("//") {
return Err(DejaDbError::Validation(format!(
"path must live under {ROOT} (got '{path}')"
)));
}
Ok(())
}
fn head(&mut self, path: &str) -> Result<Option<(Hash, String)>> {
match self.m.latest(&self.ns, path, MEMORY_FILE_RELATION)? {
Some(g) => {
let content = g
.fields
.get("context")
.and_then(|c| c.get("content"))
.and_then(|c| c.as_str())
.unwrap_or("")
.to_string();
Ok(Some((g.hash, content)))
}
None => Ok(None),
}
}
fn write_version(&mut self, path: &str, content: &str, prev: Option<Hash>) -> Result<Hash> {
use sha2::{Digest, Sha256};
let digest = hex::encode(&Sha256::digest(content.as_bytes())[..6]);
let mut f = Fact::new(path, MEMORY_FILE_RELATION, &format!("v:{digest}"));
f.common.namespace = Some(self.ns.clone());
f.common.context = Some(json!({ "content": content }));
f.common.embedding_text = Some(crate::migrate::clip_et(content));
f.common.source_type = Some("agent".to_string());
match prev {
Some(old) => self.m.supersede(&old, &mut f),
None => self.m.add(&f),
}
}
fn view(&mut self, path: &str) -> Result<String> {
Self::check_path(path)?;
let dirish = path == ROOT || path.ends_with('/');
if dirish {
let prefix = if path.ends_with('/') { path.to_string() } else { format!("{path}/") };
let files = self.m.subjects_with_relation(&self.ns, MEMORY_FILE_RELATION)?;
let listing: Vec<String> = files
.into_iter()
.filter(|f| f.starts_with(&prefix))
.collect();
if listing.is_empty() {
return Ok(format!("Directory: {path}\n(empty)"));
}
return Ok(format!("Directory: {path}\n{}", listing.join("\n")));
}
match self.head(path)? {
Some((_, content)) => {
let numbered: Vec<String> = content
.lines()
.enumerate()
.map(|(i, l)| format!("{:>4}: {l}", i + 1))
.collect();
Ok(numbered.join("\n"))
}
None => Err(DejaDbError::Validation(format!("file not found: {path}"))),
}
}
fn create(&mut self, path: &str, file_text: &str) -> Result<String> {
Self::check_path(path)?;
if path == ROOT || path.ends_with('/') {
return Err(DejaDbError::Validation("create requires a file path".into()));
}
let prev = self.head(path)?.map(|(h, _)| h);
let existed = prev.is_some();
self.write_version(path, file_text, prev)?;
Ok(format!(
"{} {path}",
if existed { "Overwrote (new version of)" } else { "Created" }
))
}
fn str_replace(&mut self, path: &str, old_str: &str, new_str: &str) -> Result<String> {
Self::check_path(path)?;
let (head, content) = self
.head(path)?
.ok_or_else(|| DejaDbError::Validation(format!("file not found: {path}")))?;
let occurrences = content.matches(old_str).count();
if old_str.is_empty() || occurrences == 0 {
return Err(DejaDbError::Validation("old_str not found in file".into()));
}
if occurrences > 1 {
return Err(DejaDbError::Validation(format!(
"old_str appears {occurrences} times — must be unique"
)));
}
let updated = content.replacen(old_str, new_str, 1);
self.write_version(path, &updated, Some(head))?;
Ok(format!("Replaced text in {path}"))
}
fn insert(&mut self, path: &str, insert_line: usize, insert_text: &str) -> Result<String> {
Self::check_path(path)?;
let (head, content) = self
.head(path)?
.ok_or_else(|| DejaDbError::Validation(format!("file not found: {path}")))?;
let mut lines: Vec<&str> = content.lines().collect();
let at = insert_line.min(lines.len());
lines.insert(at, insert_text);
let updated = lines.join("\n");
self.write_version(path, &updated, Some(head))?;
Ok(format!("Inserted at line {at} in {path}"))
}
fn delete(&mut self, path: &str) -> Result<String> {
Self::check_path(path)?;
if path == ROOT {
return Err(DejaDbError::Validation("refusing to delete the root".into()));
}
let versions = self.m.history(&self.ns, path, MEMORY_FILE_RELATION)?;
if versions.is_empty() {
return Err(DejaDbError::Validation(format!("file not found: {path}")));
}
let n = versions.len();
for v in versions {
self.m.forget(&v.hash)?;
}
Ok(format!("Deleted {path} ({n} versions erased)"))
}
fn rename(&mut self, old_path: &str, new_path: &str) -> Result<String> {
Self::check_path(old_path)?;
Self::check_path(new_path)?;
let (old_head, content) = self
.head(old_path)?
.ok_or_else(|| DejaDbError::Validation(format!("file not found: {old_path}")))?;
if self.head(new_path)?.is_some() {
return Err(DejaDbError::Validation(format!("target exists: {new_path}")));
}
let mut f = {
use sha2::{Digest, Sha256};
let digest = hex::encode(&Sha256::digest(content.as_bytes())[..6]);
let mut f = Fact::new(new_path, MEMORY_FILE_RELATION, &format!("v:{digest}"));
f.common.namespace = Some(self.ns.clone());
f.common.context = Some(json!({ "content": content }));
f.common.embedding_text = Some(crate::migrate::clip_et(&content));
f.common.derived_from = Some(old_head.to_hex());
f
};
self.m.add(&f)?;
let versions = self.m.history(&self.ns, old_path, MEMORY_FILE_RELATION)?;
for v in versions {
self.m.forget(&v.hash)?;
}
let _ = &mut f;
Ok(format!("Renamed {old_path} → {new_path}"))
}
}