package builtin import ( "bufio" "context" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "regexp" "strings" "nub/internal/tool" ) type GrepTool struct{} func (GrepTool) Name() string { return "grep" } func (GrepTool) Description() string { return "Durchsucht Dateien per Regex, in-process, mit Kontext-Zeilen." } func (GrepTool) Schema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "pattern": {"type": "string"}, "path": {"type": "string", "description": "Startverzeichnis, relativ zu RepoRoot, Default '.'"}, "context": {"type": "integer", "description": "Kontext-Zeilen vor/nach jedem Treffer"} }, "required": ["pattern"] }`) } type grepInput struct { Pattern string `json:"pattern"` Path string `json:"path"` Context int `json:"context"` } type grepMatch struct { path string line int text string } func (GrepTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) { var in grepInput if err := json.Unmarshal(input, &in); err != nil { return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil } re, err := regexp.Compile(in.Pattern) if err != nil { return tool.Result{ForModel: "invalid pattern: " + err.Error(), IsError: true}, nil } startRel := in.Path if startRel == "" { startRel = "." } start, err := resolvePath(env.RepoRoot, env.Cwd, startRel) if err != nil { return tool.Result{ForModel: err.Error(), IsError: true}, nil } ignore := loadGitignore(env.RepoRoot) var matches []grepMatch err = filepath.WalkDir(start, func(p string, d fs.DirEntry, err error) error { if err != nil { return nil } rel, relErr := filepath.Rel(env.RepoRoot, p) if relErr == nil && ignore.match(rel) { if d.IsDir() { return filepath.SkipDir } return nil } if d.IsDir() { return nil } grepFile(p, re, in.Context, &matches) return nil }) if err != nil { return tool.Result{ForModel: fmt.Sprintf("grep: %v", err), IsError: true}, nil } if len(matches) == 0 { return tool.Result{ForModel: fmt.Sprintf("no matches for %q", in.Pattern)}, nil } var b strings.Builder for _, m := range matches { relPath, _ := filepath.Rel(env.RepoRoot, m.path) fmt.Fprintf(&b, "%s:%d:%s\n", relPath, m.line, m.text) } return tool.Result{ForModel: b.String(), ForUI: GrepResult{Count: len(matches)}}, nil } func grepFile(path string, re *regexp.Regexp, contextLines int, out *[]grepMatch) { f, err := os.Open(path) if err != nil { return } defer f.Close() var lines []string scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 64*1024), 1024*1024) for scanner.Scan() { lines = append(lines, scanner.Text()) } if scanner.Err() != nil { return } for i, line := range lines { if !re.MatchString(line) { continue } lo := max(0, i-contextLines) hi := min(len(lines)-1, i+contextLines) for j := lo; j <= hi; j++ { *out = append(*out, grepMatch{path: path, line: j + 1, text: lines[j]}) } } } type GrepResult struct { Count int }