@@ -3,7 +3,6 @@ package services
33import (
44 "context"
55 "fmt"
6- "io/fs"
76 "os"
87 "os/exec"
98 "path/filepath"
@@ -680,3 +679,259 @@ func (s *LLMToolService) validateWebSearchTool(args map[string]interface{}) erro
680679
681680 return nil
682681}
682+
683+ // executeFileSearchTool handles FileSearch tool execution
684+ func (s * LLMToolService ) executeFileSearchTool (args map [string ]interface {}) (* domain.ToolExecutionResult , error ) {
685+ start := time .Now ()
686+ pattern , ok := args ["pattern" ].(string )
687+ if ! ok {
688+ return & domain.ToolExecutionResult {
689+ ToolName : "FileSearch" ,
690+ Arguments : args ,
691+ Success : false ,
692+ Duration : time .Since (start ),
693+ Error : "pattern parameter is required and must be a string" ,
694+ }, nil
695+ }
696+
697+ // Get optional parameters with defaults
698+ includeDirs := false
699+ if includeDirsVal , ok := args ["include_dirs" ].(bool ); ok {
700+ includeDirs = includeDirsVal
701+ }
702+
703+ caseSensitive := true
704+ if caseSensitiveVal , ok := args ["case_sensitive" ].(bool ); ok {
705+ caseSensitive = caseSensitiveVal
706+ }
707+
708+ searchResult , err := s .searchFiles (pattern , includeDirs , caseSensitive )
709+ success := err == nil
710+
711+ result := & domain.ToolExecutionResult {
712+ ToolName : "FileSearch" ,
713+ Arguments : args ,
714+ Success : success ,
715+ Duration : time .Since (start ),
716+ Data : searchResult ,
717+ }
718+
719+ if err != nil {
720+ result .Error = err .Error ()
721+ }
722+
723+ return result , nil
724+ }
725+
726+ // validateFileSearchTool validates FileSearch tool arguments
727+ func (s * LLMToolService ) validateFileSearchTool (args map [string ]interface {}) error {
728+ pattern , ok := args ["pattern" ].(string )
729+ if ! ok {
730+ return fmt .Errorf ("pattern parameter is required and must be a string" )
731+ }
732+
733+ if strings .TrimSpace (pattern ) == "" {
734+ return fmt .Errorf ("pattern cannot be empty" )
735+ }
736+
737+ // Validate regex pattern
738+ if _ , err := regexp .Compile (pattern ); err != nil {
739+ return fmt .Errorf ("invalid regex pattern: %w" , err )
740+ }
741+
742+ return nil
743+ }
744+
745+ // searchFiles performs the actual file search using regex patterns
746+ func (s * LLMToolService ) searchFiles (pattern string , includeDirs bool , caseSensitive bool ) (* FileSearchResult , error ) {
747+ start := time .Now ()
748+
749+ // Compile regex pattern with proper flags
750+ var regex * regexp.Regexp
751+ var err error
752+ if caseSensitive {
753+ regex , err = regexp .Compile (pattern )
754+ } else {
755+ regex , err = regexp .Compile ("(?i)" + pattern )
756+ }
757+ if err != nil {
758+ return nil , fmt .Errorf ("failed to compile regex: %w" , err )
759+ }
760+
761+ var matches []FileSearchMatch
762+ cwd , err := os .Getwd ()
763+ if err != nil {
764+ return nil , fmt .Errorf ("failed to get current directory: %w" , err )
765+ }
766+
767+ // Walk the directory tree
768+ err = filepath .WalkDir (cwd , func (path string , d os.DirEntry , err error ) error {
769+ if err != nil {
770+ return nil // Skip errors and continue
771+ }
772+
773+ relPath , err := filepath .Rel (cwd , path )
774+ if err != nil {
775+ return nil
776+ }
777+
778+ // Handle directories
779+ if d .IsDir () {
780+ return s .handleSearchDirectory (d , relPath , regex , includeDirs , & matches )
781+ }
782+
783+ // Handle files
784+ if s .shouldIncludeInSearch (d , relPath , regex ) {
785+ info , err := d .Info ()
786+ if err == nil {
787+ matches = append (matches , FileSearchMatch {
788+ Path : path ,
789+ Size : info .Size (),
790+ IsDir : false ,
791+ RelPath : relPath ,
792+ })
793+ }
794+ }
795+
796+ return nil
797+ })
798+
799+ if err != nil {
800+ return nil , fmt .Errorf ("failed to search files: %w" , err )
801+ }
802+
803+ return & FileSearchResult {
804+ Pattern : pattern ,
805+ Matches : matches ,
806+ Total : len (matches ),
807+ Duration : time .Since (start ).String (),
808+ }, nil
809+ }
810+
811+ // handleSearchDirectory handles directory processing during search
812+ func (s * LLMToolService ) handleSearchDirectory (d os.DirEntry , relPath string , regex * regexp.Regexp , includeDirs bool , matches * []FileSearchMatch ) error {
813+ // Check depth limit (reuse same logic as FileService)
814+ depth := strings .Count (relPath , string (filepath .Separator ))
815+ if depth >= 10 { // maxDepth from LocalFileService
816+ return filepath .SkipDir
817+ }
818+
819+ // Check if directory should be excluded (same logic as FileService)
820+ excludeDirs := map [string ]bool {
821+ ".git" : true ,
822+ ".github" : true ,
823+ "node_modules" : true ,
824+ ".infer" : true ,
825+ "vendor" : true ,
826+ ".flox" : true ,
827+ "dist" : true ,
828+ "build" : true ,
829+ "bin" : true ,
830+ ".vscode" : true ,
831+ ".idea" : true ,
832+ }
833+
834+ if excludeDirs [d .Name ()] {
835+ return filepath .SkipDir
836+ }
837+
838+ if strings .HasPrefix (d .Name (), "." ) && relPath != "." {
839+ return filepath .SkipDir
840+ }
841+
842+ // Check if path is excluded by configuration
843+ if s .isPathExcluded (relPath ) {
844+ return filepath .SkipDir
845+ }
846+
847+ // If includeDirs is true and directory name matches pattern, add it
848+ if includeDirs && regex .MatchString (d .Name ()) {
849+ * matches = append (* matches , FileSearchMatch {
850+ Path : filepath .Join (filepath .Dir (relPath ), d .Name ()),
851+ Size : 0 , // Directories have size 0
852+ IsDir : true ,
853+ RelPath : relPath ,
854+ })
855+ }
856+
857+ return nil
858+ }
859+
860+ // shouldIncludeInSearch determines if a file should be included in search results
861+ func (s * LLMToolService ) shouldIncludeInSearch (d os.DirEntry , relPath string , regex * regexp.Regexp ) bool {
862+ if ! d .Type ().IsRegular () {
863+ return false
864+ }
865+
866+ // Skip hidden files
867+ if strings .HasPrefix (d .Name (), "." ) {
868+ return false
869+ }
870+
871+ // Check if path is excluded by configuration
872+ if s .isPathExcluded (relPath ) {
873+ return false
874+ }
875+
876+ // Check file extension exclusions (same as FileService)
877+ excludeExts := map [string ]bool {
878+ ".exe" : true , ".bin" : true , ".dll" : true , ".so" : true , ".dylib" : true ,
879+ ".a" : true , ".o" : true , ".obj" : true , ".pyc" : true , ".class" : true ,
880+ ".jar" : true , ".war" : true , ".zip" : true , ".tar" : true , ".gz" : true ,
881+ ".rar" : true , ".7z" : true , ".png" : true , ".jpg" : true , ".jpeg" : true ,
882+ ".gif" : true , ".bmp" : true , ".ico" : true , ".svg" : true , ".pdf" : true ,
883+ ".mov" : true , ".mp4" : true , ".avi" : true , ".mp3" : true , ".wav" : true ,
884+ }
885+
886+ ext := strings .ToLower (filepath .Ext (relPath ))
887+ if excludeExts [ext ] {
888+ return false
889+ }
890+
891+ // Check file size (same as FileService)
892+ if info , err := d .Info (); err == nil && info .Size () > 100 * 1024 { // 100KB limit
893+ return false
894+ }
895+
896+ // Check if filename or relative path matches the regex pattern
897+ return regex .MatchString (d .Name ()) || regex .MatchString (relPath )
898+ }
899+
900+ // isPathExcluded checks if a file path should be excluded based on configuration
901+ func (s * LLMToolService ) isPathExcluded (path string ) bool {
902+ if s .config == nil {
903+ return false
904+ }
905+
906+ cleanPath := filepath .Clean (path )
907+ normalizedPath := filepath .ToSlash (cleanPath )
908+
909+ for _ , excludePattern := range s .config .Tools .ExcludePaths {
910+ cleanPattern := filepath .Clean (excludePattern )
911+ normalizedPattern := filepath .ToSlash (cleanPattern )
912+
913+ if normalizedPath == normalizedPattern {
914+ return true
915+ }
916+
917+ if strings .HasSuffix (normalizedPattern , "/*" ) {
918+ dirPattern := strings .TrimSuffix (normalizedPattern , "/*" )
919+ if strings .HasPrefix (normalizedPath , dirPattern + "/" ) || normalizedPath == dirPattern {
920+ return true
921+ }
922+ }
923+
924+ if strings .HasSuffix (normalizedPattern , "/" ) {
925+ dirPattern := strings .TrimSuffix (normalizedPattern , "/" )
926+ if strings .HasPrefix (normalizedPath , dirPattern + "/" ) || normalizedPath == dirPattern {
927+ return true
928+ }
929+ }
930+
931+ if strings .HasPrefix (normalizedPath , normalizedPattern ) {
932+ return true
933+ }
934+ }
935+
936+ return false
937+ }
0 commit comments