From 35c6ee6b44087484fcc57611881039795dc23fa3 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 18 Aug 2026 08:19:48 +0200 Subject: [PATCH] Simplify file tool interfaces --- data/tools/file_edit.meta | 6 ++--- data/tools/file_glob.meta | 4 ++-- data/tools/file_grep.meta | 18 +++------------ data/tools/file_write.meta | 4 ++-- .../filetools/internal/filetools/filetools.go | 23 +++++++++++++++---- .../internal/filetools/filetools_test.go | 6 ++++- 6 files changed, 34 insertions(+), 27 deletions(-) diff --git a/data/tools/file_edit.meta b/data/tools/file_edit.meta index 887db8f..e1b7428 100644 --- a/data/tools/file_edit.meta +++ b/data/tools/file_edit.meta @@ -1,7 +1,7 @@ { "description": "Replace text in a file. Has fuzzy matching (whitespace/indent flexible).", "outputFormat": "diff", - "prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations \u2014 add surrounding context to disambiguate, or use `replace_all=true`.", + "prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional boolean)\n\n```\nfile_edit(path=\"\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"\", old_string=\"old text\", new_string=\"new text\", replace_all=true)\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations \u2014 add surrounding context to disambiguate, or use `replace_all=true`.", "args": { "type": "object", "required": [ @@ -23,8 +23,8 @@ "description": "Replacement text" }, "replace_all": { - "type": "string", - "description": "Set to true/1/yes to replace all occurrences" + "type": "boolean", + "description": "Replace all occurrences (default: false)" } } }, diff --git a/data/tools/file_glob.meta b/data/tools/file_glob.meta index 96a3ddc..186d765 100644 --- a/data/tools/file_glob.meta +++ b/data/tools/file_glob.meta @@ -1,6 +1,6 @@ { - "description": "Find files by glob pattern, sorted by mtime (newest first).", - "prompt": "## file_glob\n\nFind files by glob pattern, sorted by mtime (newest first).\n\n**Args**: `[pattern]` or `[pattern, search_dir]`\n\n- First arg: glob pattern (e.g. `\"*.go\"`, `\"**/*.ts\"`).\n- Second arg (optional): absolute search directory. Defaults to cwd.\n\n```\nfile_glob(pattern=\"*.go\", search_dir=\"\")\nfile_glob(pattern=\"**/*.go\")\n```", + "description": "Find files by Go filepath pattern, sorted by mtime (newest first).", + "prompt": "## file_glob\n\nFind files by Go filepath pattern, sorted by mtime (newest first).\n\n**Args**: `pattern` (required), optional `search_dir`\n\n- `pattern`: Go filepath matching syntax, such as `*.go` or `**/*.ts`. Shell brace expansion is not supported.\n- `search_dir`: absolute search directory. Defaults to cwd.\n\n```\nfile_glob(pattern=\"*.go\", search_dir=\"\")\nfile_glob(pattern=\"**/*.go\")\n```", "args": { "type": "object", "required": [ diff --git a/data/tools/file_grep.meta b/data/tools/file_grep.meta index 766f621..c3d08c7 100644 --- a/data/tools/file_grep.meta +++ b/data/tools/file_grep.meta @@ -1,6 +1,6 @@ { - "description": "Search file contents with ripgrep. Capped at 50 matches by default.", - "prompt": "## file_grep\n\nSearch file contents with ripgrep. Capped at 50 matches by default.\n\n**Args**: `pattern` (required), plus optional named params.\n\n```\nfile_grep(pattern=\"pattern\", path=\"\")\nfile_grep(pattern=\"TODO\", path=\"src/\", glob=\"*.go\", case_insensitive=true)\nfile_grep(pattern=\"func main\", path=\"\")\n```\n\n- `path`: search directory (not a positional arg)\n- `glob`: file glob filter\n- `case_insensitive`: boolean\n\n**Params**: `path`, `mode` (content|files_with_matches|count), `glob`, `type`, `case_insensitive`, `before`, `after`, `context`, `multiline`, `head`, `offset`.", + "description": "Search file contents with a regular expression.", + "prompt": "## file_grep\n\nSearch file contents with a regular expression.\n\n**Args**: `pattern` (required), plus optional named params.\n\n```\nfile_grep(pattern=\"pattern\", path=\"\")\nfile_grep(pattern=\"TODO\", path=\"\", glob=\"*.go\", case_insensitive=true)\n```\n\n- `path`: absolute search directory. Defaults to cwd.\n- `glob`: file glob filter.\n- `case_insensitive`: boolean.\n- `multiline`: boolean. When true, the regex can match across line breaks; content results show the match and its starting line.\n- `mode`: `content` (default), `files_with_matches`, or `count`.\n- `head`: maximum results to return (default: 50).\n- `offset`: number of results to skip.", "args": { "type": "object", "required": [ @@ -31,21 +31,9 @@ "type": "boolean", "description": "Case-insensitive search" }, - "after": { - "type": "integer", - "description": "Lines after match (-A)" - }, - "before": { - "type": "integer", - "description": "Lines before match (-B)" - }, - "context": { - "type": "integer", - "description": "Lines around match (-C)" - }, "multiline": { "type": "boolean", - "description": "Enable multiline matching" + "description": "Allow matches to span multiple lines" }, "head": { "type": "integer", diff --git a/data/tools/file_write.meta b/data/tools/file_write.meta index 40fda31..f748b7e 100644 --- a/data/tools/file_write.meta +++ b/data/tools/file_write.meta @@ -1,7 +1,7 @@ { - "description": "Create or overwrite a file. Produces a unified diff.", + "description": "Create or overwrite a file. Use for new files or intentional full-file replacement; use file_edit for targeted changes.", "outputFormat": "diff", - "prompt": "## file_write\n\nCreate or overwrite a file. Produces a unified diff.\n\n**Args**: `[path, content]`\n\n```\nfile_write(path=\"\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.", + "prompt": "## file_write\n\nCreate or overwrite a file. Use for new files or intentional full-file replacement. Use `file_edit` for targeted changes.\n\n**Args**: `path`, `content` (required)\n\n```\nfile_write(path=\"\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.", "args": { "type": "object", "required": [ diff --git a/tools/filetools/internal/filetools/filetools.go b/tools/filetools/internal/filetools/filetools.go index 6c2bc87..8a09e6f 100644 --- a/tools/filetools/internal/filetools/filetools.go +++ b/tools/filetools/internal/filetools/filetools.go @@ -522,13 +522,28 @@ func Grep(m map[string]any) (string, error) { if er != nil { return nil } - lines := strings.Split(string(data), "\n") matches := 0 - for i, line := range lines { - if re.MatchString(line) { + multiline, er := Bool(m, "multiline") + if er != nil { + return er + } + if multiline { + for _, match := range re.FindAllStringIndex(string(data), -1) { matches++ if mode == "content" { - results = append(results, fmt.Sprintf("%s:%d:%s", p, i+1, line)) + text := string(data[match[0]:match[1]]) + line := 1 + strings.Count(string(data[:match[0]]), "\n") + results = append(results, fmt.Sprintf("%s:%d:%s", p, line, text)) + } + } + } else { + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if re.MatchString(line) { + matches++ + if mode == "content" { + results = append(results, fmt.Sprintf("%s:%d:%s", p, i+1, line)) + } } } } diff --git a/tools/filetools/internal/filetools/filetools_test.go b/tools/filetools/internal/filetools/filetools_test.go index f66e1ef..38b853c 100644 --- a/tools/filetools/internal/filetools/filetools_test.go +++ b/tools/filetools/internal/filetools/filetools_test.go @@ -69,7 +69,7 @@ func TestDiffOmitsUnchangedFileContent(t *testing.T) { func TestGlobAndGrep(t *testing.T) { d := t.TempDir() os.MkdirAll(filepath.Join(d, "sub"), 0755) - os.WriteFile(filepath.Join(d, "a.go"), []byte("TODO: yes\n"), 0644) + os.WriteFile(filepath.Join(d, "a.go"), []byte("TODO: yes\nnext line\n"), 0644) os.WriteFile(filepath.Join(d, "sub", "b.go"), []byte("todo: no\n"), 0644) out, err := Glob(map[string]any{"pattern": "**/*.go", "search_dir": d}) if err != nil || !strings.Contains(out, "b.go") { @@ -79,6 +79,10 @@ func TestGlobAndGrep(t *testing.T) { if err != nil || !strings.Contains(out, "a.go") || !strings.Contains(out, "b.go") { t.Fatal(out, err) } + out, err = Grep(map[string]any{"pattern": "TODO: yes\\nnext line", "path": d, "glob": "a.go", "multiline": true}) + if err != nil || !strings.Contains(out, ":1:TODO: yes\nnext line") { + t.Fatalf("multiline grep=%q err=%v", out, err) + } _, err = Grep(map[string]any{"pattern": "[", "path": d}) if err == nil || !strings.Contains(err.Error(), "regular expression") { t.Fatal(err)