1package tools
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "regexp"
12 "sort"
13 "strings"
14 "time"
15
16 "github.com/cloudwego/eino/components/tool"
17 "github.com/cloudwego/eino/schema"
18)
19
20type grepTool struct {
21 workingDir string
22}
23
24const (
25 GrepToolName = "grep"
26
27 MaxGrepResults = 100
28)
29
30type GrepParams struct {
31 Pattern string `json:"pattern"`
32 Path string `json:"path"`
33 Include string `json:"include"`
34}
35
36type grepMatch struct {
37 path string
38 modTime time.Time
39}
40
41func (b *grepTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
42 return &schema.ToolInfo{
43 Name: GrepToolName,
44 Desc: `- Fast content search tool that works with any codebase size
45- Searches file contents using regular expressions
46- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
47- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
48- Returns matching file paths sorted by modification time
49- Use this tool when you need to find files containing specific patterns
50- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead`,
51 ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
52 "command": {
53 Type: "string",
54 Desc: "The command to execute",
55 Required: true,
56 },
57 "timeout": {
58 Type: "number",
59 Desc: "Optional timeout in milliseconds (max 600000)",
60 },
61 }),
62 }, nil
63}
64
65func (b *grepTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
66 var params GrepParams
67 if err := json.Unmarshal([]byte(args), ¶ms); err != nil {
68 return "", err
69 }
70
71 searchPath := params.Path
72 if searchPath == "" {
73 var err error
74 searchPath, err = os.Getwd()
75 if err != nil {
76 return fmt.Sprintf("unable to get current working directory: %s", err), nil
77 }
78 }
79
80 matches, err := searchWithRipgrep(params.Pattern, searchPath, params.Include)
81 if err != nil {
82 matches, err = searchFilesWithRegex(params.Pattern, searchPath, params.Include)
83 if err != nil {
84 return fmt.Sprintf("error searching files: %s", err), nil
85 }
86 }
87
88 sort.Slice(matches, func(i, j int) bool {
89 return matches[i].modTime.After(matches[j].modTime)
90 })
91
92 truncated := false
93 if len(matches) > MaxGrepResults {
94 truncated = true
95 matches = matches[:MaxGrepResults]
96 }
97
98 filenames := make([]string, len(matches))
99 for i, m := range matches {
100 filenames[i] = m.path
101 }
102
103 var output string
104 if len(filenames) == 0 {
105 output = "No files found"
106 } else {
107 output = fmt.Sprintf("Found %d file%s\n%s",
108 len(filenames),
109 pluralize(len(filenames)),
110 strings.Join(filenames, "\n"))
111
112 if truncated {
113 output += "\n(Results are truncated. Consider using a more specific path or pattern.)"
114 }
115 }
116
117 return output, nil
118}
119
120func pluralize(count int) string {
121 if count == 1 {
122 return ""
123 }
124 return "s"
125}
126
127func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
128 _, err := exec.LookPath("rg")
129 if err != nil {
130 return nil, fmt.Errorf("ripgrep not found: %w", err)
131 }
132
133 args := []string{"-l", pattern}
134 if include != "" {
135 args = append(args, "--glob", include)
136 }
137 args = append(args, path)
138
139 cmd := exec.Command("rg", args...)
140 output, err := cmd.Output()
141 if err != nil {
142 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
143 return []grepMatch{}, nil
144 }
145 return nil, err
146 }
147
148 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
149 matches := make([]grepMatch, 0, len(lines))
150
151 for _, line := range lines {
152 if line == "" {
153 continue
154 }
155
156 fileInfo, err := os.Stat(line)
157 if err != nil {
158 continue
159 }
160
161 matches = append(matches, grepMatch{
162 path: line,
163 modTime: fileInfo.ModTime(),
164 })
165 }
166
167 return matches, nil
168}
169
170func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
171 matches := []grepMatch{}
172
173 regex, err := regexp.Compile(pattern)
174 if err != nil {
175 return nil, fmt.Errorf("invalid regex pattern: %w", err)
176 }
177
178 var includePattern *regexp.Regexp
179 if include != "" {
180 regexPattern := globToRegex(include)
181 includePattern, err = regexp.Compile(regexPattern)
182 if err != nil {
183 return nil, fmt.Errorf("invalid include pattern: %w", err)
184 }
185 }
186
187 err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
188 if err != nil {
189 return nil
190 }
191
192 if info.IsDir() {
193 return nil
194 }
195
196 if includePattern != nil && !includePattern.MatchString(path) {
197 return nil
198 }
199
200 match, err := fileContainsPattern(path, regex)
201 if err != nil {
202 return nil
203 }
204
205 if match {
206 matches = append(matches, grepMatch{
207 path: path,
208 modTime: info.ModTime(),
209 })
210 }
211
212 return nil
213 })
214 if err != nil {
215 return nil, err
216 }
217
218 return matches, nil
219}
220
221func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, error) {
222 file, err := os.Open(filePath)
223 if err != nil {
224 return false, err
225 }
226 defer file.Close()
227
228 scanner := bufio.NewScanner(file)
229 for scanner.Scan() {
230 if pattern.MatchString(scanner.Text()) {
231 return true, nil
232 }
233 }
234
235 if err := scanner.Err(); err != nil {
236 return false, err
237 }
238
239 return false, nil
240}
241
242func globToRegex(glob string) string {
243 regexPattern := strings.ReplaceAll(glob, ".", "\\.")
244 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
245 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
246
247 re := regexp.MustCompile(`\{([^}]+)\}`)
248 regexPattern = re.ReplaceAllStringFunc(regexPattern, func(match string) string {
249 inner := match[1 : len(match)-1]
250 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
251 })
252
253 return "^" + regexPattern + "$"
254}
255
256func NewGrepTool(workingDir string) tool.InvokableTool {
257 return &grepTool{
258 workingDir,
259 }
260}