use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde_json::Value;
use tracing::warn;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KuzuDatabase {
pub path: PathBuf,
pub name: String,
}
pub struct KuzuSource {
pub db_info: KuzuDatabase,
}
impl KuzuSource {
pub fn discover(roots: &[PathBuf]) -> Vec<KuzuDatabase> {
let mut found = Vec::new();
for root in roots {
let Ok(entries) = std::fs::read_dir(root) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if Self::looks_like_kuzu_db(&path) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
found.push(KuzuDatabase {
path: path.clone(),
name,
});
continue;
}
let kuzu_sub = path.join("kuzu");
if Self::looks_like_kuzu_db(&kuzu_sub) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
found.push(KuzuDatabase {
path: kuzu_sub,
name,
});
}
}
}
found
}
fn looks_like_kuzu_db(path: &Path) -> bool {
if !path.is_dir() {
return false;
}
path.join("catalog.kz").exists()
|| path.join("data.kz").exists()
|| path.join("lock").exists()
}
pub fn default_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Some(home) = dirs::home_dir() {
roots.push(home.join(".claude-mpm").join("memory"));
roots.push(home.join(".open-mpm").join("memory"));
}
if let Ok(cwd) = std::env::current_dir() {
roots.push(cwd.join(".claude-mpm").join("memory"));
roots.push(cwd.join(".open-mpm").join("memory"));
}
roots
}
pub fn open(path: PathBuf) -> Result<Self> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string());
let db_info = KuzuDatabase {
path: path.clone(),
name,
};
#[cfg(feature = "memory-core-kuzu")]
{
warn!(
db = %path.display(),
"kuzu feature enabled but bindings not yet wired; using stub"
);
}
Ok(Self { db_info })
}
pub fn query(&self, cypher: &str) -> Result<Vec<HashMap<String, Value>>> {
warn!(
db = %self.db_info.path.display(),
query = %cypher,
"kuzu query stub — no results"
);
Ok(Vec::new())
}
pub fn recall(&self, query_text: &str, top_k: usize) -> Result<Vec<String>> {
let _cypher = format!(
"MATCH (m:Memory) WHERE m.content CONTAINS '{}' RETURN m.content LIMIT {}",
query_text.replace('\'', "\\'"),
top_k
);
warn!(
db = %self.db_info.path.display(),
query = %query_text,
"kuzu recall stub — no results"
);
Ok(Vec::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn discover_finds_kuzu_dir_with_catalog() {
let dir = tempdir().unwrap();
let kuzu_db = dir.path().join("my-project");
std::fs::create_dir_all(&kuzu_db).unwrap();
std::fs::write(kuzu_db.join("catalog.kz"), b"").unwrap();
let found = KuzuSource::discover(&[dir.path().to_path_buf()]);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "my-project");
}
#[test]
fn discover_finds_nested_kuzu_subdir() {
let dir = tempdir().unwrap();
let project_dir = dir.path().join("project-x");
let kuzu_sub = project_dir.join("kuzu");
std::fs::create_dir_all(&kuzu_sub).unwrap();
std::fs::write(kuzu_sub.join("data.kz"), b"").unwrap();
let found = KuzuSource::discover(&[dir.path().to_path_buf()]);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "project-x");
}
#[test]
fn discover_ignores_non_kuzu_dirs() {
let dir = tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("random-dir")).unwrap();
let found = KuzuSource::discover(&[dir.path().to_path_buf()]);
assert!(found.is_empty());
}
#[test]
fn discover_skips_missing_root() {
let dir = tempdir().unwrap();
let missing = dir.path().join("does-not-exist");
let found = KuzuSource::discover(&[missing]);
assert!(found.is_empty());
}
#[test]
fn open_and_recall_stub_returns_empty() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("catalog.kz"), b"").unwrap();
let src = KuzuSource::open(dir.path().to_path_buf()).unwrap();
let results = src.recall("anything", 10).unwrap();
assert!(results.is_empty());
let rows = src.query("MATCH (n) RETURN n LIMIT 1").unwrap();
assert!(rows.is_empty());
}
#[test]
fn default_roots_returns_some_paths() {
let roots = KuzuSource::default_roots();
assert!(!roots.is_empty());
}
}