use super::{FileMetadata, MemoryFile, MemoryType};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use rayon::prelude::*;
use sha2::{Digest, Sha256};
use std::fs;
use std::io::Read;
use std::path::Path;
use walkdir::WalkDir;
pub struct Scanner {
include_subdirs: bool,
max_depth: Option<usize>,
follow_symlinks: bool,
}
impl Scanner {
pub fn new() -> Self {
Self {
include_subdirs: false,
max_depth: None,
follow_symlinks: false,
}
}
pub fn with_subdirs(mut self, include: bool) -> Self {
self.include_subdirs = include;
self
}
pub fn with_max_depth(mut self, depth: Option<usize>) -> Self {
self.max_depth = depth;
self
}
pub fn with_follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
pub fn scan<P: AsRef<Path>>(&self, path: P) -> Result<Vec<MemoryFile>> {
let path = path.as_ref();
let mut files = Vec::new();
if let Some(home) = dirs::home_dir() {
let user_memory = home.join(".claude").join("CLAUDE.md");
if user_memory.exists() {
if let Ok(file) = self.scan_file(&user_memory, MemoryType::UserMemory) {
files.push(file);
}
}
}
let project_memory = path.join("CLAUDE.md");
if project_memory.exists() {
if let Ok(file) = self.scan_file(&project_memory, MemoryType::ProjectMemory) {
files.push(file);
}
}
let local_memory = path.join("CLAUDE.local.md");
if local_memory.exists() {
if let Ok(file) = self.scan_file(&local_memory, MemoryType::LocalMemory) {
files.push(file);
}
}
if self.include_subdirs {
let subdir_files = self.scan_subdirs(path)?;
files.extend(subdir_files);
}
Ok(files)
}
fn scan_subdirs<P: AsRef<Path>>(&self, path: P) -> Result<Vec<MemoryFile>> {
let path = path.as_ref();
let mut walker = WalkDir::new(path).follow_links(self.follow_symlinks);
if let Some(depth) = self.max_depth {
walker = walker.max_depth(depth);
}
let entries: Vec<_> = walker
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name() == "CLAUDE.md"
&& e.path() != path.join("CLAUDE.md")
&& e.path() != path.join("CLAUDE.local.md")
})
.collect();
let files: Vec<_> = entries
.par_iter()
.filter_map(|entry| self.scan_file(entry.path(), MemoryType::SubdirMemory).ok())
.collect();
Ok(files)
}
fn scan_file(&self, path: &Path, file_type: MemoryType) -> Result<MemoryFile> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
let metadata = self.get_file_metadata(path)?;
let imports = self.extract_imports(&content);
Ok(MemoryFile {
path: path.to_path_buf(),
file_type,
content,
imports,
metadata,
})
}
fn get_file_metadata(&self, path: &Path) -> Result<FileMetadata> {
let metadata = fs::metadata(path)?;
let modified = metadata.modified()?;
let size = metadata.len();
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
let hash = format!("{:x}", hasher.finalize());
let content = fs::read_to_string(path)?;
let line_count = content.lines().count();
Ok(FileMetadata {
size,
modified: DateTime::<Utc>::from(modified),
hash,
line_count,
})
}
fn extract_imports(&self, content: &str) -> Vec<super::Import> {
let mut imports = Vec::new();
for (line_number, line) in content.lines().enumerate() {
if line.trim_start().starts_with("{{import") {
if let Some(path) = self.parse_import_line(line) {
imports.push(super::Import {
path,
line_number: line_number + 1,
resolved_path: None,
});
}
}
}
imports
}
fn parse_import_line(&self, line: &str) -> Option<String> {
let trimmed = line.trim();
if trimmed.starts_with("{{import") && trimmed.ends_with("}}") {
let content = &trimmed[8..trimmed.len() - 2].trim();
Some(content.to_string())
} else {
None
}
}
}
impl Default for Scanner {
fn default() -> Self {
Self::new()
}
}