Simplify file tool interfaces

This commit is contained in:
Ollie Agent 2026-08-18 08:19:48 +02:00
parent 9bd9539b6a
commit 35c6ee6b44
6 changed files with 34 additions and 27 deletions

View File

@ -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=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", 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=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", 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)"
}
}
},

View File

@ -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=\"<ABSOLUTE_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=\"<ABSOLUTE_SEARCH_DIR>\")\nfile_glob(pattern=\"**/*.go\")\n```",
"args": {
"type": "object",
"required": [

View File

@ -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=\"<ABSOLUTE_SEARCH_DIR>\")\nfile_grep(pattern=\"TODO\", path=\"src/\", glob=\"*.go\", case_insensitive=true)\nfile_grep(pattern=\"func main\", path=\"<ABSOLUTE_SEARCH_DIR>\")\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=\"<ABSOLUTE_SEARCH_DIR>\")\nfile_grep(pattern=\"TODO\", path=\"<ABSOLUTE_SEARCH_DIR>\", 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",

View File

@ -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=\"<ABSOLUTE_PATH_FROM_PWD>\", 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=\"<ABSOLUTE_PATH_FROM_PWD>\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.",
"args": {
"type": "object",
"required": [

View File

@ -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))
}
}
}
}

View File

@ -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)