@@ -1,11 +1,13 @@
1package tools
1package tools
5 "encoding/json"
6 "encoding/json"
9 "path/filepath"
11 "path/filepath"
@@ -132,6 +134,73 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
134func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
136func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
137+ matches, err := globWithRipgrep(pattern, searchPath, limit)
139+ return matches, len(matches) >= limit, nil
142+ return globWithDoublestar(pattern, searchPath, limit)
145+func globWithRipgrep(
146+ pattern, searchRoot string,
148+) ([]string, error) {
150+ if searchRoot == "" {
154+ rgBin, err := exec.LookPath("rg")
156+ return nil, fmt.Errorf("ripgrep not found in $PATH: %w", err)
159+ if !filepath.IsAbs(pattern) && !strings.HasPrefix(pattern, "/") {
160+ pattern = "/" + pattern
170+ cmd := exec.Command(rgBin, args...)
171+ cmd.Dir = searchRoot
173+ out, err := cmd.CombinedOutput()
175+ if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
178+ return nil, fmt.Errorf("ripgrep: %w\n%s", err, out)
181+ var matches []string
182+ for _, p := range bytes.Split(out, []byte{0}) {
186+ abs := filepath.Join(searchRoot, string(p))
187+ if skipHidden(abs) {
190+ matches = append(matches, abs)
193+ sort.SliceStable(matches, func(i, j int) bool {
194+ return len(matches[i]) < len(matches[j])
197+ if len(matches) > limit {
198+ matches = matches[:limit]
203+func globWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
135 if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
204 if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
136 if !strings.HasSuffix(searchPath, "/") {
205 if !strings.HasSuffix(searchPath, "/") {
137 searchPath += "/"
206 searchPath += "/"