#![allow(dead_code)]
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentMemoryScope {
User,
Project,
Local,
}
impl AgentMemoryScope {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"user" => Some(AgentMemoryScope::User),
"project" => Some(AgentMemoryScope::Project),
"local" => Some(AgentMemoryScope::Local),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
AgentMemoryScope::User => "user",
AgentMemoryScope::Project => "project",
AgentMemoryScope::Local => "local",
}
}
}
fn sanitize_agent_type_for_path(agent_type: &str) -> String {
agent_type.replace(':', "-")
}
fn get_memory_base_dir() -> PathBuf {
std::env::var("CLAUDE_CODE_MEMORY_BASE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude")
})
}
fn get_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn get_local_agent_memory_dir(dir_name: &str) -> PathBuf {
if let Ok(remote_dir) = std::env::var("CLAUDE_CODE_REMOTE_MEMORY_DIR") {
let project_root = get_project_root();
PathBuf::from(&remote_dir)
.join("projects")
.join(sanitize_path(&project_root))
.join("agent-memory-local")
.join(dir_name)
} else {
get_cwd()
.join(".claude")
.join("agent-memory-local")
.join(dir_name)
}
}
fn sanitize_path(path: &str) -> String {
path.replace(
|c: char| !c.is_alphanumeric() && c != '/' && c != '-' && c != '_',
"_",
)
}
fn get_project_root() -> String {
get_cwd().to_string_lossy().to_string()
}
pub fn get_agent_memory_dir(agent_type: &str, scope: AgentMemoryScope) -> PathBuf {
let dir_name = sanitize_agent_type_for_path(agent_type);
match scope {
AgentMemoryScope::Project => get_cwd()
.join(".claude")
.join("agent-memory")
.join(dir_name),
AgentMemoryScope::Local => get_local_agent_memory_dir(&dir_name),
AgentMemoryScope::User => get_memory_base_dir().join("agent-memory").join(dir_name),
}
}
pub fn is_agent_memory_path(absolute_path: &str) -> bool {
let normalized = Path::new(absolute_path)
.canonicalize()
.unwrap_or_else(|_| absolute_path.into());
let normalized_str = normalized.to_string_lossy();
let memory_base = get_memory_base_dir();
if normalized_str.starts_with(
&memory_base
.join("agent-memory")
.to_string_lossy()
.to_string(),
) {
return true;
}
let project_mem = get_cwd().join(".claude").join("agent-memory");
if normalized_str.starts_with(&project_mem.to_string_lossy().to_string()) {
return true;
}
if let Ok(remote_dir) = std::env::var("CLAUDE_CODE_REMOTE_MEMORY_DIR") {
if normalized_str.contains("agent-memory-local")
&& normalized_str.starts_with(&format!("{}/projects", remote_dir))
{
return true;
}
} else {
let local_mem = get_cwd().join(".claude").join("agent-memory-local");
if normalized_str.starts_with(&local_mem.to_string_lossy().to_string()) {
return true;
}
}
false
}
pub fn get_agent_memory_entrypoint(agent_type: &str, scope: AgentMemoryScope) -> PathBuf {
get_agent_memory_dir(agent_type, scope).join("MEMORY.md")
}
pub fn get_memory_scope_display(scope: Option<AgentMemoryScope>) -> &'static str {
match scope {
Some(AgentMemoryScope::User) => "User (~/.claude/agent-memory/)",
Some(AgentMemoryScope::Project) => "Project (.claude/agent-memory/)",
Some(AgentMemoryScope::Local) => "Local (.claude/agent-memory-local/)",
None => "None",
}
}
pub fn load_agent_memory_prompt(agent_type: &str, scope: AgentMemoryScope) -> String {
let scope_note = match scope {
AgentMemoryScope::User => {
"- Since this memory is user-scope, keep learnings general since they apply across all projects"
}
AgentMemoryScope::Project => {
"- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project"
}
AgentMemoryScope::Local => {
"- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine"
}
};
let memory_dir = get_agent_memory_dir(agent_type, scope);
let _ = std::fs::create_dir_all(&memory_dir);
let extra_guidelines = std::env::var("CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES").ok();
let extra_guidelines = extra_guidelines.as_deref().filter(|s| !s.trim().is_empty());
build_memory_prompt(
"Persistent Agent Memory",
&memory_dir,
if let Some(guidelines) = extra_guidelines {
vec![scope_note, guidelines]
} else {
vec![scope_note]
},
)
}
fn build_memory_prompt(
display_name: &str,
memory_dir: &Path,
extra_guidelines: Vec<&str>,
) -> String {
let memory_contents = read_memory_files(memory_dir);
let guidelines = extra_guidelines.join("\n");
format!(
"# {display_name}\n\n\
Memory directory: {memory_dir}\n\n\
{guidelines}\n\n\
{memory_contents}",
memory_dir = memory_dir.display()
)
}
fn read_memory_files(memory_dir: &Path) -> String {
let mut contents = String::new();
if let Ok(entries) = std::fs::read_dir(memory_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("md") {
if let Ok(content) = std::fs::read_to_string(&path) {
contents.push_str(&format!("\n--- {} ---\n{}\n", path.display(), content));
}
}
}
}
contents
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_agent_type() {
assert_eq!(sanitize_agent_type_for_path("my-agent"), "my-agent");
assert_eq!(
sanitize_agent_type_for_path("my-plugin:my-agent"),
"my-plugin-my-agent"
);
}
#[test]
fn test_memory_scope_from_str() {
assert_eq!(
AgentMemoryScope::from_str("user"),
Some(AgentMemoryScope::User)
);
assert_eq!(
AgentMemoryScope::from_str("project"),
Some(AgentMemoryScope::Project)
);
assert_eq!(
AgentMemoryScope::from_str("local"),
Some(AgentMemoryScope::Local)
);
assert_eq!(AgentMemoryScope::from_str("invalid"), None);
}
#[test]
fn test_memory_scope_display() {
assert_eq!(get_memory_scope_display(None), "None");
assert_eq!(
get_memory_scope_display(Some(AgentMemoryScope::User)),
"User (~/.claude/agent-memory/)"
);
}
#[test]
fn test_get_agent_memory_entrypoint() {
let path = get_agent_memory_entrypoint("test-agent", AgentMemoryScope::Project);
assert!(path.to_string_lossy().contains("agent-memory"));
assert!(path.to_string_lossy().contains("MEMORY.md"));
}
}