Packages
nous
0.15.3
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/tools/file_edit.ex
defmodule Nous.Tools.FileEdit do
@moduledoc """
String replacement editing tool.
Edits files by finding and replacing exact string matches.
By default requires `old_string` to be unique in the file
to prevent unintended changes.
"""
@behaviour Nous.Tool.Behaviour
@impl true
def metadata do
%{
name: "file_edit",
description:
"Edit a file by replacing exact string matches. The old_string must be unique in the file unless replace_all is true.",
category: :write,
requires_approval: true,
parameters: %{
"type" => "object",
"properties" => %{
"file_path" => %{
"type" => "string",
"description" => "Path to the file to edit"
},
"old_string" => %{
"type" => "string",
"description" => "The exact text to find and replace"
},
"new_string" => %{
"type" => "string",
"description" => "The replacement text"
},
"replace_all" => %{
"type" => "boolean",
"description" =>
"Replace all occurrences instead of requiring uniqueness. Defaults to false."
}
},
"required" => ["file_path", "old_string", "new_string"]
}
}
end
@impl true
def execute(
ctx,
%{"file_path" => file_path, "old_string" => old_string, "new_string" => new_string} =
args
) do
replace_all = Map.get(args, "replace_all", false)
with {:ok, safe_path} <- Nous.Tools.PathGuard.validate(file_path, ctx),
{:ok, content} <- File.read(safe_path) do
occurrences = count_occurrences(content, old_string)
cond do
occurrences == 0 ->
{:error, "old_string not found in #{safe_path}"}
occurrences > 1 and not replace_all ->
{:error,
"old_string found #{occurrences} times in #{safe_path}. " <>
"Use replace_all: true to replace all occurrences."}
true ->
new_content =
if replace_all do
String.replace(content, old_string, new_string)
else
replace_first(content, old_string, new_string)
end
case File.write(safe_path, new_content) do
:ok ->
replaced = if replace_all, do: occurrences, else: 1
{:ok,
"Edited #{safe_path} (#{replaced} replacement#{if replaced > 1, do: "s", else: ""})"}
{:error, reason} ->
{:error, "Failed to write #{safe_path}: #{inspect(reason)}"}
end
end
else
{:error, reason} when is_binary(reason) -> {:error, reason}
{:error, reason} -> {:error, "Failed to access #{file_path}: #{inspect(reason)}"}
end
end
defp count_occurrences(string, substring) do
parts = String.split(string, substring)
length(parts) - 1
end
defp replace_first(string, old, new) do
case String.split(string, old, parts: 2) do
[before, rest] -> before <> new <> rest
[_no_match] -> string
end
end
end