#![forbid(unsafe_code)]
#![allow(
clippy::needless_raw_string_hashes,
clippy::uninlined_format_args,
clippy::unwrap_used
)]
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub name: String,
pub content: String,
#[serde(default)]
pub tags: Vec<String>,
pub created_at: i64,
pub updated_at: i64,
}
pub struct MemoryManager {
memory_dir: PathBuf,
}
impl MemoryManager {
pub fn new(project_root: &Path) -> io::Result<Self> {
let coraline_dir = project_root.join(".coraline");
if !coraline_dir.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Coraline not initialized in {}. Run 'coraline init' first.",
project_root.display()
),
));
}
let memory_dir = coraline_dir.join("memories");
Ok(Self { memory_dir })
}
fn get_memory_path(&self, name: &str) -> PathBuf {
let name = name.trim_end_matches(".md");
self.memory_dir.join(format!("{name}.md"))
}
pub fn write_memory(&self, name: &str, content: &str) -> io::Result<String> {
fs::create_dir_all(&self.memory_dir)?;
let path = self.get_memory_path(name);
fs::write(&path, content)?;
Ok(format!("Memory '{name}' written successfully"))
}
pub fn read_memory(&self, name: &str) -> io::Result<String> {
let path = self.get_memory_path(name);
if !path.exists() {
return Ok(format!(
"Memory '{name}' not found. Consider creating it with write_memory if needed."
));
}
fs::read_to_string(&path)
}
pub fn list_memories(&self) -> io::Result<Vec<String>> {
let mut memories = Vec::new();
if !self.memory_dir.exists() {
return Ok(memories);
}
for entry in fs::read_dir(&self.memory_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& path.extension().and_then(|s| s.to_str()) == Some("md")
&& let Some(name) = path.file_stem().and_then(|s| s.to_str())
{
memories.push(name.to_string());
}
}
memories.sort();
Ok(memories)
}
pub fn delete_memory(&self, name: &str) -> io::Result<String> {
let path = self.get_memory_path(name);
if !path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Memory '{name}' not found"),
));
}
fs::remove_file(&path)?;
Ok(format!("Memory '{name}' deleted successfully"))
}
pub fn memory_exists(&self, name: &str) -> bool {
self.get_memory_path(name).exists()
}
pub fn memory_dir(&self) -> &Path {
&self.memory_dir
}
}
pub fn create_initial_memories(project_root: &Path, project_name: &str) -> io::Result<()> {
let manager = MemoryManager::new(project_root)?;
let project_overview = format!(
r"# {project_name} - Project Overview
## Purpose
[Describe the main purpose and goals of this project]
## Architecture
[High-level architecture description]
## Key Components
- [Component 1]: [Description]
- [Component 2]: [Description]
## Technologies
- [Technology stack]
## Entry Points
- [Main files or modules]
## Notes
[Any important notes or context]
"
);
manager.write_memory("project_overview", &project_overview)?;
let style_conventions = r"# Code Style Conventions
## General Principles
- [Principle 1]
- [Principle 2]
## Naming Conventions
- Files: [convention]
- Functions: [convention]
- Variables: [convention]
- Types: [convention]
## Code Organization
- [Organizational pattern]
## Best Practices
- [Practice 1]
- [Practice 2]
## Patterns to Avoid
- [Anti-pattern 1]
- [Anti-pattern 2]
";
manager.write_memory("style_conventions", style_conventions)?;
let suggested_commands = r"# Suggested Development Commands
## Build
```bash
# Development build
cargo build
# Production build
cargo build --release
```
## Test
```bash
# Run all tests
cargo test
# Run specific test
cargo test <test_name>
```
## Run
```bash
# Run the application
cargo run
```
## Other Useful Commands
```bash
# Format code
cargo fmt
# Lint
cargo clippy
# Check types
cargo check
```
";
manager.write_memory("suggested_commands", suggested_commands)?;
let completion_checklist = r"# Feature Completion Checklist
When implementing a new feature, ensure:
- [ ] Code follows style conventions
- [ ] Unit tests written and passing
- [ ] Integration tests added if needed
- [ ] Documentation updated
- [ ] Error handling implemented
- [ ] Edge cases considered
- [ ] Performance implications reviewed
- [ ] Security implications reviewed
- [ ] Code reviewed
- [ ] Memory and resource leaks checked
- [ ] API documentation updated
- [ ] Changelog updated
";
manager.write_memory("completion_checklist", completion_checklist)?;
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use tempfile::TempDir;
fn init_project_root(path: &Path) {
fs::create_dir_all(path.join(".coraline"))
.expect("Failed to initialize .coraline directory");
}
#[test]
fn test_memory_manager_write_and_read() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
let result = manager
.write_memory("test_memory", "This is test content")
.expect("Failed to write memory");
assert!(result.contains("written successfully"));
let content = manager
.read_memory("test_memory")
.expect("Failed to read memory");
assert_eq!(content, "This is test content");
}
#[test]
fn test_memory_manager_handles_md_extension() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
manager
.write_memory("test.md", "content")
.expect("Failed to write memory");
let content = manager.read_memory("test").expect("Failed to read memory");
assert_eq!(content, "content");
let content = manager
.read_memory("test.md")
.expect("Failed to read memory with .md extension");
assert_eq!(content, "content");
}
#[test]
fn test_memory_manager_list() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
manager
.write_memory("memory1", "content1")
.expect("Failed to write memory1");
manager
.write_memory("memory2", "content2")
.expect("Failed to write memory2");
manager
.write_memory("memory3", "content3")
.expect("Failed to write memory3");
let memories = manager.list_memories().expect("Failed to list memories");
assert_eq!(memories.len(), 3);
assert!(memories.contains(&"memory1".to_string()));
assert!(memories.contains(&"memory2".to_string()));
assert!(memories.contains(&"memory3".to_string()));
}
#[test]
fn test_memory_manager_delete() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
manager
.write_memory("to_delete", "content")
.expect("Failed to write memory");
assert!(manager.memory_exists("to_delete"));
manager
.delete_memory("to_delete")
.expect("Failed to delete memory");
assert!(!manager.memory_exists("to_delete"));
}
#[test]
fn test_memory_not_found() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
let result = manager
.read_memory("nonexistent")
.expect("Failed to read memory");
assert!(result.contains("not found"));
}
#[test]
fn test_create_initial_memories() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
init_project_root(temp_dir.path());
create_initial_memories(temp_dir.path(), "test_project")
.expect("Failed to create initial memories");
let manager = MemoryManager::new(temp_dir.path()).expect("Failed to create MemoryManager");
let memories = manager.list_memories().expect("Failed to list memories");
assert_eq!(memories.len(), 4);
assert!(memories.contains(&"project_overview".to_string()));
assert!(memories.contains(&"style_conventions".to_string()));
assert!(memories.contains(&"suggested_commands".to_string()));
assert!(memories.contains(&"completion_checklist".to_string()));
let overview = manager
.read_memory("project_overview")
.expect("Failed to read project_overview");
assert!(overview.contains("test_project"));
}
}