Current section
Files
Jump to
Current section
Files
lib/loom_ex/tools/grep.ex
defmodule LoomEx.Tools.Grep do
use LoomEx.Tool
@max_results 100
@impl true
def name, do: "grep"
@impl true
def description,
do:
"Search file contents for a pattern. Returns matching lines with file paths and line numbers."
@impl true
def parameters do
%{
type: "object",
properties: %{
pattern: %{type: "string", description: "Search pattern (regex supported)"},
path: %{
type: "string",
description: "Directory or file to search in (default: current directory)"
},
glob: %{
type: "string",
description: "File glob pattern to filter, e.g. '*.ex' (optional)"
},
case_insensitive: %{
type: "boolean",
description: "Case insensitive search (default: false)"
}
},
required: ["pattern"]
}
end
@impl true
def execute(%{"pattern" => pattern} = args, ctx) do
search_path = args["path"] || "."
full_path = resolve_path(search_path, ctx)
case_insensitive = args["case_insensitive"] || false
# Build grep arguments
grep_args = ["-rn"]
grep_args = if case_insensitive, do: grep_args ++ ["-i"], else: grep_args
grep_args = if glob = args["glob"], do: grep_args ++ ["--include", glob], else: grep_args
grep_args = grep_args ++ [pattern, full_path]
try do
{output, exit_code} = System.cmd("grep", grep_args, stderr_to_stdout: true)
case exit_code do
0 ->
lines = String.split(output, "\n", trim: true)
total = length(lines)
truncated = Enum.take(lines, @max_results)
result = Enum.join(truncated, "\n")
result =
if total > @max_results do
result <> "\n... [#{total - @max_results} more matches truncated]"
else
result
end
{:ok, %{"matches" => result, "count" => min(total, @max_results), "total" => total}}
1 ->
{:ok, %{"matches" => "", "count" => 0, "total" => 0}}
_ ->
{:error, "grep failed (exit #{exit_code}): #{String.trim(output)}"}
end
rescue
e -> {:error, "grep error: #{Exception.message(e)}"}
end
end
defp resolve_path(path, ctx) do
if Path.type(path) == :absolute do
path
else
Path.join(ctx[:cwd] || ".", path)
end
end
end