@@ -6,7 +6,10 @@ use reqwest;
66use rusqlite:: { params, Connection , Result as SqliteResult } ;
77use serde:: { Deserialize , Serialize } ;
88use serde_json:: Value as JsonValue ;
9+ use serde_yaml;
10+ use std:: fs;
911use std:: io:: { BufRead , BufReader } ;
12+ use std:: path:: Path ;
1013use std:: process:: Stdio ;
1114use std:: sync:: Mutex ;
1215use tauri:: { AppHandle , Emitter , Manager , State } ;
@@ -20,7 +23,7 @@ fn find_claude_binary(app_handle: &AppHandle) -> Result<String, String> {
2023 crate :: claude_binary:: find_claude_binary ( app_handle)
2124}
2225
23- /// Represents a CC Agent stored in the database
26+ /// Represents a CC Agent stored in the database or loaded from filesystem
2427#[ derive( Debug , Serialize , Deserialize , Clone ) ]
2528pub struct Agent {
2629 pub id : Option < i64 > ,
@@ -35,6 +38,10 @@ pub struct Agent {
3538 pub hooks : Option < String > , // JSON string of hooks configuration
3639 pub created_at : String ,
3740 pub updated_at : String ,
41+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
42+ pub source : Option < String > , // "database" or "filesystem"
43+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
44+ pub file_path : Option < String > , // Path if loaded from filesystem
3845}
3946
4047/// Represents an agent execution run
@@ -92,6 +99,16 @@ pub struct AgentData {
9299 pub hooks : Option < String > ,
93100}
94101
102+ /// Frontmatter structure for agent markdown files
103+ #[ derive( Debug , Serialize , Deserialize ) ]
104+ struct AgentFrontmatter {
105+ name : String ,
106+ description : Option < String > ,
107+ tools : Option < String > ,
108+ model : Option < String > ,
109+ icon : Option < String > ,
110+ }
111+
95112/// Database connection state
96113pub struct AgentDb ( pub Mutex < Connection > ) ;
97114
@@ -345,6 +362,119 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
345362 Ok ( conn)
346363}
347364
365+ /// Parse a markdown file with YAML frontmatter
366+ fn parse_agent_markdown ( file_path : & Path ) -> Result < Agent , String > {
367+ let content = fs:: read_to_string ( file_path)
368+ . map_err ( |e| format ! ( "Failed to read file {}: {}" , file_path. display( ) , e) ) ?;
369+
370+ // Split frontmatter and content
371+ let parts: Vec < & str > = content. splitn ( 3 , "---" ) . collect ( ) ;
372+ if parts. len ( ) < 3 {
373+ return Err ( format ! ( "Invalid markdown format in {}" , file_path. display( ) ) ) ;
374+ }
375+
376+ // Parse frontmatter
377+ let frontmatter: AgentFrontmatter = serde_yaml:: from_str ( parts[ 1 ] )
378+ . map_err ( |e| format ! ( "Failed to parse frontmatter in {}: {}" , file_path. display( ) , e) ) ?;
379+
380+ // Extract system prompt from markdown content
381+ let system_prompt = parts[ 2 ] . trim ( ) . to_string ( ) ;
382+
383+ // Determine icon based on name or use default
384+ let icon = frontmatter. icon . unwrap_or_else ( || {
385+ // Map common agent names to icons
386+ match frontmatter. name . as_str ( ) {
387+ name if name. contains ( "ai" ) => "bot" ,
388+ name if name. contains ( "api" ) => "globe" ,
389+ name if name. contains ( "cloud" ) => "cloud" ,
390+ name if name. contains ( "data" ) => "database" ,
391+ name if name. contains ( "test" ) || name. contains ( "qa" ) => "shield" ,
392+ name if name. contains ( "deploy" ) => "package" ,
393+ name if name. contains ( "architect" ) => "layout" ,
394+ name if name. contains ( "security" ) => "shield" ,
395+ name if name. contains ( "debug" ) => "bug" ,
396+ name if name. contains ( "doc" ) => "file-text" ,
397+ name if name. contains ( "review" ) => "eye" ,
398+ _ => "bot" ,
399+ } . to_string ( )
400+ } ) ;
401+
402+ let now = chrono:: Local :: now ( ) . to_rfc3339 ( ) ;
403+
404+ Ok ( Agent {
405+ id : None , // File-based agents don't have database IDs
406+ name : frontmatter. name . clone ( ) ,
407+ icon,
408+ system_prompt,
409+ default_task : frontmatter. description ,
410+ model : frontmatter. model . unwrap_or_else ( || "sonnet" . to_string ( ) ) ,
411+ enable_file_read : true ,
412+ enable_file_write : true ,
413+ enable_network : false ,
414+ hooks : None ,
415+ created_at : now. clone ( ) ,
416+ updated_at : now,
417+ source : Some ( "filesystem" . to_string ( ) ) ,
418+ file_path : Some ( file_path. to_string_lossy ( ) . to_string ( ) ) ,
419+ } )
420+ }
421+
422+ /// Load agents from the .claude/agents directory
423+ fn load_filesystem_agents ( ) -> Vec < Agent > {
424+ let mut agents = Vec :: new ( ) ;
425+
426+ // Get the .claude/agents directory
427+ let home = match dirs:: home_dir ( ) {
428+ Some ( h) => h,
429+ None => {
430+ warn ! ( "Could not determine home directory" ) ;
431+ return agents;
432+ }
433+ } ;
434+
435+ let agents_dir = home. join ( ".claude" ) . join ( "agents" ) ;
436+ if !agents_dir. exists ( ) {
437+ debug ! ( "No .claude/agents directory found" ) ;
438+ return agents;
439+ }
440+
441+ // Recursively walk through the agents directory
442+ fn scan_directory ( dir : & Path , agents : & mut Vec < Agent > ) {
443+ if let Ok ( entries) = fs:: read_dir ( dir) {
444+ for entry in entries. flatten ( ) {
445+ let path = entry. path ( ) ;
446+ if path. is_dir ( ) {
447+ // Skip hidden directories
448+ if let Some ( name) = path. file_name ( ) {
449+ if !name. to_string_lossy ( ) . starts_with ( '.' ) {
450+ scan_directory ( & path, agents) ;
451+ }
452+ }
453+ } else if path. is_file ( ) {
454+ // Check if it's a markdown file
455+ if let Some ( ext) = path. extension ( ) {
456+ if ext == "md" || ext == "markdown" {
457+ match parse_agent_markdown ( & path) {
458+ Ok ( agent) => {
459+ debug ! ( "Loaded agent from file: {}" , agent. name) ;
460+ agents. push ( agent) ;
461+ }
462+ Err ( e) => {
463+ warn ! ( "Failed to parse agent file {}: {}" , path. display( ) , e) ;
464+ }
465+ }
466+ }
467+ }
468+ }
469+ }
470+ }
471+ }
472+
473+ scan_directory ( & agents_dir, & mut agents) ;
474+ info ! ( "Loaded {} agents from filesystem" , agents. len( ) ) ;
475+ agents
476+ }
477+
348478/// List all agents
349479#[ tauri:: command]
350480pub async fn list_agents ( db : State < ' _ , AgentDb > ) -> Result < Vec < Agent > , String > {
@@ -354,7 +484,7 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
354484 . prepare ( "SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, hooks, created_at, updated_at FROM agents ORDER BY created_at DESC" )
355485 . map_err ( |e| e. to_string ( ) ) ?;
356486
357- let agents = stmt
487+ let mut agents = stmt
358488 . query_map ( [ ] , |row| {
359489 Ok ( Agent {
360490 id : Some ( row. get ( 0 ) ?) ,
@@ -371,12 +501,20 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
371501 hooks : row. get ( 9 ) ?,
372502 created_at : row. get ( 10 ) ?,
373503 updated_at : row. get ( 11 ) ?,
504+ source : Some ( "database" . to_string ( ) ) ,
505+ file_path : None ,
374506 } )
375507 } )
376508 . map_err ( |e| e. to_string ( ) ) ?
377509 . collect :: < Result < Vec < _ > , _ > > ( )
378510 . map_err ( |e| e. to_string ( ) ) ?;
379511
512+ // Load agents from filesystem
513+ let filesystem_agents = load_filesystem_agents ( ) ;
514+
515+ // Combine agents from both sources
516+ agents. extend ( filesystem_agents) ;
517+
380518 Ok ( agents)
381519}
382520
@@ -427,6 +565,8 @@ pub async fn create_agent(
427565 hooks : row. get ( 9 ) ?,
428566 created_at : row. get ( 10 ) ?,
429567 updated_at : row. get ( 11 ) ?,
568+ source : Some ( "database" . to_string ( ) ) ,
569+ file_path : None ,
430570 } )
431571 } ,
432572 )
@@ -512,6 +652,8 @@ pub async fn update_agent(
512652 hooks : row. get ( 9 ) ?,
513653 created_at : row. get ( 10 ) ?,
514654 updated_at : row. get ( 11 ) ?,
655+ source : Some ( "database" . to_string ( ) ) ,
656+ file_path : None ,
515657 } )
516658 } ,
517659 )
@@ -554,6 +696,8 @@ pub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String>
554696 hooks : row. get ( 9 ) ?,
555697 created_at : row. get ( 10 ) ?,
556698 updated_at : row. get ( 11 ) ?,
699+ source : Some ( "database" . to_string ( ) ) ,
700+ file_path : None ,
557701 } )
558702 } ,
559703 )
@@ -1768,6 +1912,8 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
17681912 hooks : row. get ( 9 ) ?,
17691913 created_at : row. get ( 10 ) ?,
17701914 updated_at : row. get ( 11 ) ?,
1915+ source : Some ( "database" . to_string ( ) ) ,
1916+ file_path : None ,
17711917 } )
17721918 } ,
17731919 )
0 commit comments