use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClaudeImport {
pub user_memory: Option<String>,
pub project_memory: Option<String>,
pub commands: Vec<ClaudeCommand>,
pub agents: Vec<ClaudeAgentFile>,
pub settings: Option<ClaudeSettings>,
pub source_paths: Vec<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeCommand {
pub name: String,
pub body: String,
pub source: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeAgentFile {
pub name: String,
pub body: String,
pub source: PathBuf,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClaudeSettings {
#[serde(default)]
pub permissions: Option<serde_json::Value>,
#[serde(default)]
pub hooks: Option<serde_json::Value>,
#[serde(default)]
pub env: Option<serde_json::Value>,
}
pub fn discover(home: &Path, cwd: &Path) -> ClaudeImport {
let mut out = ClaudeImport::default();
let user_root = home.join(".claude");
if user_root.is_dir() {
out.source_paths.push(user_root.clone());
out.user_memory = read_optional(user_root.join("CLAUDE.md"));
out.commands
.extend(load_dir(&user_root.join("commands"), "command"));
out.agents
.extend(load_dir_as_agents(&user_root.join("agents")));
out.settings = read_optional(user_root.join("settings.json"))
.and_then(|s| serde_json::from_str(&s).ok());
}
let project_root = cwd.join(".claude");
if project_root.is_dir() {
out.source_paths.push(project_root.clone());
out.project_memory = read_optional(project_root.join("CLAUDE.md"));
out.commands
.extend(load_dir(&project_root.join("commands"), "command"));
out.agents
.extend(load_dir_as_agents(&project_root.join("agents")));
}
out
}
fn read_optional(p: PathBuf) -> Option<String> {
if p.is_file() {
std::fs::read_to_string(&p).ok()
} else {
None
}
}
fn load_dir(dir: &Path, _kind: &str) -> Vec<ClaudeCommand> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(dir) else {
return out;
};
for entry in rd.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("md") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
let Ok(body) = std::fs::read_to_string(&path) else {
continue;
};
out.push(ClaudeCommand {
name: stem.to_string(),
body,
source: path,
});
}
out
}
fn load_dir_as_agents(dir: &Path) -> Vec<ClaudeAgentFile> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(dir) else {
return out;
};
for entry in rd.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("md") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
let Ok(body) = std::fs::read_to_string(&path) else {
continue;
};
out.push(ClaudeAgentFile {
name: stem.to_string(),
body,
source: path,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn discover_picks_up_user_claude_md() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path();
let cdir = home.join(".claude");
fs::create_dir_all(&cdir).unwrap();
fs::write(cdir.join("CLAUDE.md"), "# rules\nbe concise").unwrap();
let cwd = tempfile::tempdir().unwrap();
let imported = discover(home, cwd.path());
assert!(imported.user_memory.as_deref().unwrap().contains("rules"));
assert!(imported.project_memory.is_none());
}
#[test]
fn discover_loads_commands_and_agents() {
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path();
let cmds = home.join(".claude").join("commands");
let agents = home.join(".claude").join("agents");
fs::create_dir_all(&cmds).unwrap();
fs::create_dir_all(&agents).unwrap();
fs::write(cmds.join("hello.md"), "/hello body").unwrap();
fs::write(agents.join("planner.md"), "---\nname: planner\n---\nbody").unwrap();
let cwd = tempfile::tempdir().unwrap();
let imported = discover(home, cwd.path());
assert_eq!(imported.commands.len(), 1);
assert_eq!(imported.commands[0].name, "hello");
assert_eq!(imported.agents.len(), 1);
assert_eq!(imported.agents[0].name, "planner");
}
#[test]
fn discover_returns_empty_when_no_claude_dir() {
let tmp = tempfile::tempdir().unwrap();
let imported = discover(tmp.path(), tmp.path());
assert!(imported.user_memory.is_none());
assert!(imported.commands.is_empty());
assert!(imported.agents.is_empty());
}
}