Skip to content

Commit 3306f86

Browse files
fix: Implement missing FileSearch tool methods
- Remove unused imports (io/fs, os, path/filepath) - Implement executeFileSearchTool method with regex pattern matching - Implement validateFileSearchTool method with pattern validation - Add helper methods for file search functionality - Follow existing tool patterns for consistency - Respect security exclusions and file size limits Co-authored-by: Eden Reich <edenreich@users.noreply.github.com>
1 parent a32e9ad commit 3306f86

File tree

1 file changed

+256
-1
lines changed

1 file changed

+256
-1
lines changed

internal/services/tool.go

Lines changed: 256 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package services
33
import (
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

Comments
 (0)