Packages

fnord

0.7.21

AI code archaeology

Current section

Files

Jump to
fnord lib ai tools ripgrep.ex
Raw

lib/ai/tools/ripgrep.ex

defmodule AI.Tools.Ripgrep do
@doc """
This tool requires that ripgrep (rg) is installed and available in the PATH.
"""
def is_available?() do
System.find_executable("rg") |> is_nil() |> Kernel.not()
end
@behaviour AI.Tools
@impl AI.Tools
def ui_note_on_request(args) do
{"Searching project files", Jason.encode!(args)}
end
@impl AI.Tools
def ui_note_on_result(args, result) do
{"Searched project files",
"""
Searched with: #{Jason.encode!(args)}
Result:
#{result}
"""}
end
@impl AI.Tools
def read_args(args), do: {:ok, args}
@impl AI.Tools
def spec() do
%{
type: "function",
function: %{
name: "ripgrep_search",
description:
"Searches the project directory using ripgrep. This tool is ideal when you need to find an exact match.",
parameters: %{
type: "object",
required: ["pattern"],
properties: %{
pattern: %{
type: "string",
description:
"The pattern to search for (regex by default, or literal with fixed_strings)."
},
fixed_strings: %{
type: "boolean",
description:
"Treat the pattern as a literal string instead of a regular expression.",
default: false
},
ignore_case: %{
type: "boolean",
description: "Perform case-insensitive matching.",
default: false
},
invert_match: %{
type: "boolean",
description: "Select non-matching lines.",
default: false
},
glob: %{
type: "array",
items: %{type: "string"},
description:
"Include or exclude files using glob patterns (e.g., '*.rs', '!Cargo.lock')."
},
hidden: %{
type: "boolean",
description: "Search hidden files and directories.",
default: false
},
no_ignore: %{
type: "boolean",
description: "Don't respect .gitignore and other ignore files.",
default: false
}
}
}
}
}
end
@impl AI.Tools
def call(args) do
root = Store.get_project() |> Map.get(:source_root)
args = atomize_keys(args)
cmd = build_ripgrep_cmd(args)
try do
System.cmd("rg", cmd ++ [root], stderr_to_stdout: true)
|> case do
{output, 0} -> {:ok, output}
{_output, 1} -> {:ok, "No files were matched"}
{output, code} -> {:error, "ripgrep failed with exit code #{code}:\n#{output}"}
end
rescue
e in ErlangError ->
case e.original do
:enoent ->
{
:error,
"""
`rg` (ripgrep) is not installed or not in PATH.
In your final response, instruct the user to install it to enhance your search capabilities.
"""
}
other ->
{:error, "ripgrep execution error: #{inspect(other)}"}
end
end
end
defp atomize_keys(map) when is_map(map) do
Map.new(map, fn {k, v} -> {String.to_atom(k), v} end)
end
defp build_ripgrep_cmd(%{pattern: pattern} = args) do
[]
|> append_flag(args[:fixed_strings], "--fixed-strings")
|> append_flag(args[:ignore_case], "--ignore-case")
|> append_flag(args[:invert_match], "--invert-match")
|> append_flag(args[:hidden], "--hidden")
|> append_flag(args[:no_ignore], "--no-ignore")
|> append_list("--glob", args[:glob])
|> Kernel.++([pattern])
end
defp append_flag(cmd, true, flag), do: cmd ++ [flag]
defp append_flag(cmd, _, _), do: cmd
defp append_list(cmd, _flag, nil), do: cmd
defp append_list(cmd, flag, list) when is_binary(list) do
cmd ++ [flag, list]
end
defp append_list(cmd, flag, list) when is_list(list) do
Enum.reduce(list, cmd, fn item, acc -> acc ++ [flag, item] end)
end
end