@@ -15,6 +15,54 @@ use super::types::{
1515 ImportAgentRequest , UpdateAgentRequest ,
1616} ;
1717
18+ const PROJECT_AGENTS_DIR : & str = ".cortex/agents" ;
19+ const LEGACY_PROJECT_AGENTS_DIR : & str = ".factory/agents" ;
20+
21+ fn project_agents_dir ( ) -> std:: path:: PathBuf {
22+ std:: path:: PathBuf :: from ( PROJECT_AGENTS_DIR )
23+ }
24+
25+ fn legacy_project_agents_dir ( ) -> std:: path:: PathBuf {
26+ std:: path:: PathBuf :: from ( LEGACY_PROJECT_AGENTS_DIR )
27+ }
28+
29+ fn user_agents_dir ( ) -> Option < std:: path:: PathBuf > {
30+ dirs:: home_dir ( ) . map ( |home| home. join ( ".cortex/agents" ) )
31+ }
32+
33+ fn legacy_user_agents_dir ( ) -> Option < std:: path:: PathBuf > {
34+ dirs:: home_dir ( ) . map ( |home| home. join ( ".factory/agents" ) )
35+ }
36+
37+ fn writable_agents_dir ( scope : & str ) -> AppResult < std:: path:: PathBuf > {
38+ if scope == "project" {
39+ Ok ( project_agents_dir ( ) )
40+ } else {
41+ user_agents_dir ( )
42+ . ok_or_else ( || AppError :: Internal ( "Cannot find home directory" . to_string ( ) ) )
43+ }
44+ }
45+
46+ fn search_agent_dirs ( ) -> Vec < ( std:: path:: PathBuf , & ' static str ) > {
47+ let mut dirs = vec ! [
48+ ( project_agents_dir( ) , "project" ) ,
49+ ( legacy_project_agents_dir( ) , "project" ) ,
50+ ] ;
51+
52+ if let Some ( user_dir) = user_agents_dir ( ) {
53+ dirs. push ( ( user_dir, "user" ) ) ;
54+ }
55+ if let Some ( user_dir) = legacy_user_agents_dir ( ) {
56+ dirs. push ( ( user_dir, "user" ) ) ;
57+ }
58+
59+ dirs
60+ }
61+
62+ fn agent_path_in ( dir : & std:: path:: Path , name : & str ) -> std:: path:: PathBuf {
63+ dir. join ( format ! ( "{}.md" , name) )
64+ }
65+
1866/// Read agent file from disk.
1967fn read_agent_file ( path : & std:: path:: Path , scope : & str ) -> Option < AgentDefinition > {
2068 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<AgentDefinitio
73121/// List all agents.
74122pub async fn list_agents ( ) -> AppResult < Json < Vec < AgentDefinition > > > {
75123 let mut agents = Vec :: new ( ) ;
124+ let mut seen_names = std:: collections:: HashSet :: new ( ) ;
76125
77- // Project agents (.factory/agents/)
78- let project_dir = std:: path:: Path :: new ( ".factory/agents" ) ;
79- if project_dir. exists ( )
80- && let Ok ( entries) = std:: fs:: read_dir ( project_dir)
81- {
82- for entry in entries. flatten ( ) {
83- if let Some ( agent) = read_agent_file ( & entry. path ( ) , "project" ) {
84- agents. push ( agent) ;
85- }
86- }
87- }
88-
89- // User agents (~/.factory/agents/)
90- if let Some ( home) = dirs:: home_dir ( ) {
91- let user_dir = home. join ( ".factory/agents" ) ;
92- if user_dir. exists ( )
93- && let Ok ( entries) = std:: fs:: read_dir ( & user_dir)
126+ for ( dir, scope) in search_agent_dirs ( ) {
127+ if dir. exists ( )
128+ && let Ok ( entries) = std:: fs:: read_dir ( & dir)
94129 {
95130 for entry in entries. flatten ( ) {
96- if let Some ( agent) = read_agent_file ( & entry. path ( ) , "user" ) {
97- agents. push ( agent) ;
131+ if let Some ( agent) = read_agent_file ( & entry. path ( ) , scope) {
132+ if seen_names. insert ( agent. name . clone ( ) ) {
133+ agents. push ( agent) ;
134+ }
98135 }
99136 }
100137 }
@@ -105,16 +142,9 @@ pub async fn list_agents() -> AppResult<Json<Vec<AgentDefinition>>> {
105142
106143/// Get a specific agent.
107144pub async fn get_agent ( Path ( name) : Path < String > ) -> AppResult < Json < AgentDefinition > > {
108- // Check project first
109- let project_path = std:: path:: Path :: new ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ;
110- if let Some ( agent) = read_agent_file ( & project_path, "project" ) {
111- return Ok ( Json ( agent) ) ;
112- }
113-
114- // Check user
115- if let Some ( home) = dirs:: home_dir ( ) {
116- let user_path = home. join ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ;
117- if let Some ( agent) = read_agent_file ( & user_path, "user" ) {
145+ for ( dir, scope) in search_agent_dirs ( ) {
146+ let path = agent_path_in ( & dir, & name) ;
147+ if let Some ( agent) = read_agent_file ( & path, scope) {
118148 return Ok ( Json ( agent) ) ;
119149 }
120150 }
@@ -124,13 +154,7 @@ pub async fn get_agent(Path(name): Path<String>) -> AppResult<Json<AgentDefiniti
124154
125155/// Create or update an agent.
126156pub async fn create_agent ( Json ( req) : Json < CreateAgentRequest > ) -> AppResult < Json < AgentDefinition > > {
127- let dir = if req. scope == "project" {
128- std:: path:: PathBuf :: from ( ".factory/agents" )
129- } else {
130- dirs:: home_dir ( )
131- . ok_or_else ( || AppError :: Internal ( "Cannot find home directory" . to_string ( ) ) ) ?
132- . join ( ".factory/agents" )
133- } ;
157+ let dir = writable_agents_dir ( & req. scope ) ?;
134158
135159 std:: fs:: create_dir_all ( & dir)
136160 . map_err ( |e| AppError :: Internal ( format ! ( "Failed to create directory: {}" , e) ) ) ?;
@@ -169,19 +193,10 @@ pub async fn create_agent(Json(req): Json<CreateAgentRequest>) -> AppResult<Json
169193
170194/// Delete an agent.
171195pub async fn delete_agent ( Path ( name) : Path < String > ) -> AppResult < Json < serde_json:: Value > > {
172- // Try project first
173- let project_path = std:: path:: Path :: new ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ;
174- if project_path. exists ( ) {
175- std:: fs:: remove_file ( & project_path)
176- . map_err ( |e| AppError :: Internal ( format ! ( "Failed to delete: {}" , e) ) ) ?;
177- return Ok ( Json ( serde_json:: json!( { "deleted" : true } ) ) ) ;
178- }
179-
180- // Try user
181- if let Some ( home) = dirs:: home_dir ( ) {
182- let user_path = home. join ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ;
183- if user_path. exists ( ) {
184- std:: fs:: remove_file ( & user_path)
196+ for ( dir, _scope) in search_agent_dirs ( ) {
197+ let path = agent_path_in ( & dir, & name) ;
198+ if path. exists ( ) {
199+ std:: fs:: remove_file ( & path)
185200 . map_err ( |e| AppError :: Internal ( format ! ( "Failed to delete: {}" , e) ) ) ?;
186201 return Ok ( Json ( serde_json:: json!( { "deleted" : true } ) ) ) ;
187202 }
@@ -229,21 +244,16 @@ pub async fn update_agent(
229244 Json ( req) : Json < UpdateAgentRequest > ,
230245) -> AppResult < Json < AgentDefinition > > {
231246 // Find existing agent
232- let project_path = std:: path:: Path :: new ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ;
233- let user_path =
234- dirs:: home_dir ( ) . map ( |h| h. join ( ".factory/agents" ) . join ( format ! ( "{}.md" , name) ) ) ;
235-
236- let ( existing, path) = if let Some ( agent) = read_agent_file ( & project_path, "project" ) {
237- ( agent, project_path)
238- } else if let Some ( ref user_path) = user_path {
239- if let Some ( agent) = read_agent_file ( user_path, "user" ) {
240- ( agent, user_path. clone ( ) )
241- } else {
242- return Err ( AppError :: NotFound ( format ! ( "Agent not found: {}" , name) ) ) ;
247+ let mut found = None ;
248+ for ( dir, scope) in search_agent_dirs ( ) {
249+ let path = agent_path_in ( & dir, & name) ;
250+ if let Some ( agent) = read_agent_file ( & path, scope) {
251+ found = Some ( ( agent, path) ) ;
252+ break ;
243253 }
244- } else {
245- return Err ( AppError :: NotFound ( format ! ( "Agent not found: {}" , name ) ) ) ;
246- } ;
254+ }
255+ let ( existing , path ) =
256+ found . ok_or_else ( || AppError :: NotFound ( format ! ( "Agent not found: {}" , name ) ) ) ? ;
247257
248258 // Merge updates
249259 let updated = AgentDefinition {
@@ -352,13 +362,7 @@ pub async fn import_agent(Json(req): Json<ImportAgentRequest>) -> AppResult<Json
352362 let ( name, agent) = parse_agent_content ( & req. content , & req. format ) ?;
353363
354364 // Determine directory
355- let dir = if req. scope == "project" {
356- std:: path:: PathBuf :: from ( ".factory/agents" )
357- } else {
358- dirs:: home_dir ( )
359- . ok_or_else ( || AppError :: Internal ( "Cannot find home directory" . to_string ( ) ) ) ?
360- . join ( ".factory/agents" )
361- } ;
365+ let dir = writable_agents_dir ( & req. scope ) ?;
362366
363367 std:: fs:: create_dir_all ( & dir)
364368 . map_err ( |e| AppError :: Internal ( format ! ( "Failed to create directory: {}" , e) ) ) ?;
@@ -409,3 +413,49 @@ pub async fn generate_agent_prompt(
409413 permission_mode : "default" . to_string ( ) ,
410414 } ) )
411415}
416+
417+ #[ cfg( test) ]
418+ mod tests {
419+ use super :: * ;
420+
421+ #[ test]
422+ fn test_writable_project_agents_dir_uses_standard_loader_path ( ) {
423+ assert_eq ! (
424+ writable_agents_dir( "project" ) . unwrap( ) ,
425+ std:: path:: PathBuf :: from( ".cortex/agents" )
426+ ) ;
427+ }
428+
429+ #[ test]
430+ fn test_search_agent_dirs_include_standard_before_legacy_project_path ( ) {
431+ let dirs = search_agent_dirs ( ) ;
432+
433+ assert_eq ! (
434+ dirs[ 0 ] ,
435+ ( std:: path:: PathBuf :: from( ".cortex/agents" ) , "project" )
436+ ) ;
437+ assert_eq ! (
438+ dirs[ 1 ] ,
439+ ( std:: path:: PathBuf :: from( ".factory/agents" ) , "project" )
440+ ) ;
441+ }
442+
443+ #[ test]
444+ fn test_read_agent_file_reads_standard_markdown_agent ( ) {
445+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
446+ let path = dir. path ( ) . join ( "reviewer.md" ) ;
447+ std:: fs:: write (
448+ & path,
449+ "---\n description: Code reviewer\n tools: [\" Read\" , \" Grep\" ]\n model: inherit\n permissionMode: default\n ---\n \n Review code carefully." ,
450+ )
451+ . unwrap ( ) ;
452+
453+ let agent = read_agent_file ( & path, "project" ) . expect ( "agent should parse" ) ;
454+
455+ assert_eq ! ( agent. name, "reviewer" ) ;
456+ assert_eq ! ( agent. description, "Code reviewer" ) ;
457+ assert_eq ! ( agent. tools, vec![ "Read" , "Grep" ] ) ;
458+ assert_eq ! ( agent. scope, "project" ) ;
459+ assert_eq ! ( agent. prompt, "Review code carefully." ) ;
460+ }
461+ }
0 commit comments