glob.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io/fs"
  7. "os"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "time"
  12. "github.com/bmatcuk/doublestar/v4"
  13. "github.com/kujtimiihoxha/termai/internal/config"
  14. )
  15. type globTool struct{}
  16. const (
  17. GlobToolName = "glob"
  18. )
  19. type fileInfo struct {
  20. path string
  21. modTime time.Time
  22. }
  23. type GlobParams struct {
  24. Pattern string `json:"pattern"`
  25. Path string `json:"path"`
  26. }
  27. func (g *globTool) Info() ToolInfo {
  28. return ToolInfo{
  29. Name: GlobToolName,
  30. Description: globDescription(),
  31. Parameters: map[string]any{
  32. "pattern": map[string]any{
  33. "type": "string",
  34. "description": "The glob pattern to match files against",
  35. },
  36. "path": map[string]any{
  37. "type": "string",
  38. "description": "The directory to search in. Defaults to the current working directory.",
  39. },
  40. },
  41. Required: []string{"pattern"},
  42. }
  43. }
  44. // Run implements Tool.
  45. func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
  46. var params GlobParams
  47. if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
  48. return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
  49. }
  50. if params.Pattern == "" {
  51. return NewTextErrorResponse("pattern is required"), nil
  52. }
  53. // If path is empty, use current working directory
  54. searchPath := params.Path
  55. if searchPath == "" {
  56. searchPath = config.WorkingDirectory()
  57. }
  58. files, truncated, err := globFiles(params.Pattern, searchPath, 100)
  59. if err != nil {
  60. return NewTextErrorResponse(fmt.Sprintf("error performing glob search: %s", err)), nil
  61. }
  62. // Format the output for the assistant
  63. var output string
  64. if len(files) == 0 {
  65. output = "No files found"
  66. } else {
  67. output = strings.Join(files, "\n")
  68. if truncated {
  69. output += "\n\n(Results are truncated. Consider using a more specific path or pattern.)"
  70. }
  71. }
  72. return NewTextResponse(output), nil
  73. }
  74. func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
  75. // Make sure pattern starts with the search path if not absolute
  76. if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
  77. // If searchPath doesn't end with a slash, add one before appending the pattern
  78. if !strings.HasSuffix(searchPath, "/") {
  79. searchPath += "/"
  80. }
  81. pattern = searchPath + pattern
  82. }
  83. // Open the filesystem for walking
  84. fsys := os.DirFS("/")
  85. // Convert the absolute pattern to a relative one for the DirFS
  86. // DirFS uses the root directory ("/") so we should strip leading "/"
  87. relPattern := strings.TrimPrefix(pattern, "/")
  88. // Collect matching files
  89. var matches []fileInfo
  90. // Use doublestar to walk the filesystem and find matches
  91. err := doublestar.GlobWalk(fsys, relPattern, func(path string, d fs.DirEntry) error {
  92. // Skip directories from results
  93. if d.IsDir() {
  94. return nil
  95. }
  96. if skipHidden(path) {
  97. return nil
  98. }
  99. // Get file info for modification time
  100. info, err := d.Info()
  101. if err != nil {
  102. return nil // Skip files we can't access
  103. }
  104. // Add to matches
  105. absPath := "/" + path // Restore absolute path
  106. matches = append(matches, fileInfo{
  107. path: absPath,
  108. modTime: info.ModTime(),
  109. })
  110. // Check limit
  111. if len(matches) >= limit*2 { // Collect more than needed for sorting
  112. return fs.SkipAll
  113. }
  114. return nil
  115. })
  116. if err != nil {
  117. return nil, false, fmt.Errorf("glob walk error: %w", err)
  118. }
  119. // Sort files by modification time (newest first)
  120. sort.Slice(matches, func(i, j int) bool {
  121. return matches[i].modTime.After(matches[j].modTime)
  122. })
  123. // Check if we need to truncate the results
  124. truncated := len(matches) > limit
  125. if truncated {
  126. matches = matches[:limit]
  127. }
  128. // Extract just the paths
  129. results := make([]string, len(matches))
  130. for i, m := range matches {
  131. results[i] = m.path
  132. }
  133. return results, truncated, nil
  134. }
  135. func skipHidden(path string) bool {
  136. base := filepath.Base(path)
  137. return base != "." && strings.HasPrefix(base, ".")
  138. }
  139. func globDescription() string {
  140. return `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
  141. WHEN TO USE THIS TOOL:
  142. - Use when you need to find files by name patterns or extensions
  143. - Great for finding specific file types across a directory structure
  144. - Useful for discovering files that match certain naming conventions
  145. HOW TO USE:
  146. - Provide a glob pattern to match against file paths
  147. - Optionally specify a starting directory (defaults to current working directory)
  148. - Results are sorted with most recently modified files first
  149. GLOB PATTERN SYNTAX:
  150. - '*' matches any sequence of non-separator characters
  151. - '**' matches any sequence of characters, including separators
  152. - '?' matches any single non-separator character
  153. - '[...]' matches any character in the brackets
  154. - '[!...]' matches any character not in the brackets
  155. COMMON PATTERN EXAMPLES:
  156. - '*.js' - Find all JavaScript files in the current directory
  157. - '**/*.js' - Find all JavaScript files in any subdirectory
  158. - 'src/**/*.{ts,tsx}' - Find all TypeScript files in the src directory
  159. - '*.{html,css,js}' - Find all HTML, CSS, and JS files
  160. LIMITATIONS:
  161. - Results are limited to 100 files (newest first)
  162. - Does not search file contents (use Grep tool for that)
  163. - Hidden files (starting with '.') are skipped
  164. TIPS:
  165. - For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
  166. - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
  167. - Always check if results are truncated and refine your search pattern if needed`
  168. }
  169. func NewGlobTool() BaseTool {
  170. return &globTool{}
  171. }