diff --git a/Cargo.lock b/Cargo.lock index a5c5a1f3e..eda75682d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tempfile", "thiserror 2.0.18", "tokio", "tokio-stream", diff --git a/src/cortex-app-server/Cargo.toml b/src/cortex-app-server/Cargo.toml index 8da2d7bd0..0d57c9354 100644 --- a/src/cortex-app-server/Cargo.toml +++ b/src/cortex-app-server/Cargo.toml @@ -73,3 +73,4 @@ gethostname = "0.5" [dev-dependencies] tokio-test = { workspace = true } +tempfile = { workspace = true } diff --git a/src/cortex-app-server/src/api/agents.rs b/src/cortex-app-server/src/api/agents.rs index 6a8469424..1702bcefa 100644 --- a/src/cortex-app-server/src/api/agents.rs +++ b/src/cortex-app-server/src/api/agents.rs @@ -15,6 +15,54 @@ use super::types::{ ImportAgentRequest, UpdateAgentRequest, }; +const PROJECT_AGENTS_DIR: &str = ".cortex/agents"; +const LEGACY_PROJECT_AGENTS_DIR: &str = ".factory/agents"; + +fn project_agents_dir() -> std::path::PathBuf { + std::path::PathBuf::from(PROJECT_AGENTS_DIR) +} + +fn legacy_project_agents_dir() -> std::path::PathBuf { + std::path::PathBuf::from(LEGACY_PROJECT_AGENTS_DIR) +} + +fn user_agents_dir() -> Option { + dirs::home_dir().map(|home| home.join(".cortex/agents")) +} + +fn legacy_user_agents_dir() -> Option { + dirs::home_dir().map(|home| home.join(".factory/agents")) +} + +fn writable_agents_dir(scope: &str) -> AppResult { + if scope == "project" { + Ok(project_agents_dir()) + } else { + user_agents_dir() + .ok_or_else(|| AppError::Internal("Cannot find home directory".to_string())) + } +} + +fn search_agent_dirs() -> Vec<(std::path::PathBuf, &'static str)> { + let mut dirs = vec![ + (project_agents_dir(), "project"), + (legacy_project_agents_dir(), "project"), + ]; + + if let Some(user_dir) = user_agents_dir() { + dirs.push((user_dir, "user")); + } + if let Some(user_dir) = legacy_user_agents_dir() { + dirs.push((user_dir, "user")); + } + + dirs +} + +fn agent_path_in(dir: &std::path::Path, name: &str) -> std::path::PathBuf { + dir.join(format!("{}.md", name)) +} + /// Read agent file from disk. fn read_agent_file(path: &std::path::Path, scope: &str) -> Option { if path.extension().and_then(|e| e.to_str()) != Some("md") { @@ -73,28 +121,17 @@ fn read_agent_file(path: &std::path::Path, scope: &str) -> Option AppResult>> { let mut agents = Vec::new(); + let mut seen_names = std::collections::HashSet::new(); - // Project agents (.factory/agents/) - let project_dir = std::path::Path::new(".factory/agents"); - if project_dir.exists() - && let Ok(entries) = std::fs::read_dir(project_dir) - { - for entry in entries.flatten() { - if let Some(agent) = read_agent_file(&entry.path(), "project") { - agents.push(agent); - } - } - } - - // User agents (~/.factory/agents/) - if let Some(home) = dirs::home_dir() { - let user_dir = home.join(".factory/agents"); - if user_dir.exists() - && let Ok(entries) = std::fs::read_dir(&user_dir) + for (dir, scope) in search_agent_dirs() { + if dir.exists() + && let Ok(entries) = std::fs::read_dir(&dir) { for entry in entries.flatten() { - if let Some(agent) = read_agent_file(&entry.path(), "user") { - agents.push(agent); + if let Some(agent) = read_agent_file(&entry.path(), scope) { + if seen_names.insert(agent.name.clone()) { + agents.push(agent); + } } } } @@ -105,16 +142,9 @@ pub async fn list_agents() -> AppResult>> { /// Get a specific agent. pub async fn get_agent(Path(name): Path) -> AppResult> { - // Check project first - let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name)); - if let Some(agent) = read_agent_file(&project_path, "project") { - return Ok(Json(agent)); - } - - // Check user - if let Some(home) = dirs::home_dir() { - let user_path = home.join(".factory/agents").join(format!("{}.md", name)); - if let Some(agent) = read_agent_file(&user_path, "user") { + for (dir, scope) in search_agent_dirs() { + let path = agent_path_in(&dir, &name); + if let Some(agent) = read_agent_file(&path, scope) { return Ok(Json(agent)); } } @@ -124,13 +154,7 @@ pub async fn get_agent(Path(name): Path) -> AppResult) -> AppResult> { - let dir = if req.scope == "project" { - std::path::PathBuf::from(".factory/agents") - } else { - dirs::home_dir() - .ok_or_else(|| AppError::Internal("Cannot find home directory".to_string()))? - .join(".factory/agents") - }; + let dir = writable_agents_dir(&req.scope)?; std::fs::create_dir_all(&dir) .map_err(|e| AppError::Internal(format!("Failed to create directory: {}", e)))?; @@ -169,19 +193,10 @@ pub async fn create_agent(Json(req): Json) -> AppResult) -> AppResult> { - // Try project first - let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name)); - if project_path.exists() { - std::fs::remove_file(&project_path) - .map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?; - return Ok(Json(serde_json::json!({"deleted": true}))); - } - - // Try user - if let Some(home) = dirs::home_dir() { - let user_path = home.join(".factory/agents").join(format!("{}.md", name)); - if user_path.exists() { - std::fs::remove_file(&user_path) + for (dir, _scope) in search_agent_dirs() { + let path = agent_path_in(&dir, &name); + if path.exists() { + std::fs::remove_file(&path) .map_err(|e| AppError::Internal(format!("Failed to delete: {}", e)))?; return Ok(Json(serde_json::json!({"deleted": true}))); } @@ -229,21 +244,16 @@ pub async fn update_agent( Json(req): Json, ) -> AppResult> { // Find existing agent - let project_path = std::path::Path::new(".factory/agents").join(format!("{}.md", name)); - let user_path = - dirs::home_dir().map(|h| h.join(".factory/agents").join(format!("{}.md", name))); - - let (existing, path) = if let Some(agent) = read_agent_file(&project_path, "project") { - (agent, project_path) - } else if let Some(ref user_path) = user_path { - if let Some(agent) = read_agent_file(user_path, "user") { - (agent, user_path.clone()) - } else { - return Err(AppError::NotFound(format!("Agent not found: {}", name))); + let mut found = None; + for (dir, scope) in search_agent_dirs() { + let path = agent_path_in(&dir, &name); + if let Some(agent) = read_agent_file(&path, scope) { + found = Some((agent, path)); + break; } - } else { - return Err(AppError::NotFound(format!("Agent not found: {}", name))); - }; + } + let (existing, path) = + found.ok_or_else(|| AppError::NotFound(format!("Agent not found: {}", name)))?; // Merge updates let updated = AgentDefinition { @@ -352,13 +362,7 @@ pub async fn import_agent(Json(req): Json) -> AppResult