ai_shell

Safe, token-aware filesystem tools for LLM agents.

ai_shell provides familiar shell-like tools (cat, ls, grep, find, head/tail, cut, sed, git, ...) reimplemented in pure Python, jailed to a root folder and tuned to return useful, token-bounded output. They are provider-agnostic: wire them into any agent via the generated JSON Schemas and the neutral dispatch table (ToolKit).

ai_shell

Safe, token-aware filesystem tools for LLM agents.

ai_shell is a library of familiar shell-like tools — cat, ls, grep, find, head/tail, cut, sed, git, unified-diff patching, and a few more — reimplemented in pure Python, jailed to a root folder, and tuned to return useful, token-bounded output (with optional markdown variants). They are provider-agnostic: wire them into any agent via the generated JSON Schemas and a neutral dispatch table. Bring your own agent loop.

History: this started in 2023 as an OpenAI-Assistant shell, before Claude Code / aider / open-interpreter existed. That bot runtime has been removed; what remains is the part that was actually worth keeping — the safe tools.

Install

pip install ai-shell

Optional linters/formatters/test-runners used by the goal-checker helpers:

pip install "ai-shell[checkers]"

Use as a library

Each tool is a small class scoped to a root folder. Tools refuse to read or write outside that folder.

import ai_shell

config = ai_shell.Config()

cat = ai_shell.CatTool(".", config)
print(cat.cat_markdown(["pyproject.toml"]))

ls = ai_shell.LsTool(".", config)
print(ls.ls_markdown(path="."))

Use with any tool-calling model

ai_shell exposes JSON Schemas for the tools and a neutral dispatcher. Register the schemas with your model, then route each tool call through ToolKit.dispatch(name, arguments):

import ai_shell
from ai_shell.tools_registry import ALL_TOOLS, initialize_all_tools

# Pick the tools you want to expose.
tool_names = ["ls", "cat_markdown", "grep", "apply_git_patch"]
initialize_all_tools(keeps=tool_names)

toolkit = ai_shell.ToolKit(
    root_folder=".", token_model="gpt-4o", global_max_lines=500,
    permitted_tools=tool_names, config=ai_shell.Config(),
)

# `ALL_TOOLS` holds the JSON Schemas to hand to your model.
# When the model asks for a tool, dispatch it:
result_json = toolkit.dispatch("ls", {"path": "."})

dispatch enforces the per-session tool allowlist, tracks usage stats, applies optional media-type conversion, and converts errors to RFC7807 JSON so a model can read and recover from them.

CLI (sanity harness)

A generated CLI mirrors the tools — handy for checking a tool behaves before giving it to a model:

ais cat_markdown --file-paths pyproject.toml
ais grep --regex "def " --glob-pattern "ai_shell/*.py"

Tools

  • Read: ls, find, cat, grep, head/tail, cut, pycat (python-aware), count_tokens, and read-only git (status/diff/log/show/branch).
  • Edit: apply_git_patch (unified diff — the primary edit path), plus replace, insert, rewrite_file / write_new_file for non-diff edits.
  • Tasking: a small TODO store (ai_shell.todo) for splitting work into verifiable items — see ai_shell/todo/README.md.

Every file is read and written as UTF-8.

 1"""
 2Safe, token-aware filesystem tools for LLM agents.
 3
 4`ai_shell` provides familiar shell-like tools (cat, ls, grep, find, head/tail,
 5cut, sed, git, ...) reimplemented in pure Python, jailed to a root folder and
 6tuned to return useful, token-bounded output. They are provider-agnostic: wire
 7them into any agent via the generated JSON Schemas and the neutral dispatch
 8table (`ToolKit`).
 9
10.. include:: ../README.md
11"""
12
13from ai_shell.ai_logs.logging_utils import configure_logging
14from ai_shell.answer_tool import AnswerCollectorTool
15from ai_shell.cat_tool import CatTool
16from ai_shell.cut_tool import CutTool
17from ai_shell.externals import pytest_call
18from ai_shell.externals.black_call import invoke_black
19from ai_shell.externals.pygount_call import count_lines_of_code
20from ai_shell.externals.pylint_call import invoke_pylint
21from ai_shell.find_tool import FindTool
22from ai_shell.git_tool import GitTool
23from ai_shell.grep_tool import GrepTool
24from ai_shell.head_tail_tool import HeadTailTool
25from ai_shell.insert_tool import InsertTool
26from ai_shell.ls_tool import LsTool
27from ai_shell.patch_tool import PatchTool
28from ai_shell.pycat_tool import PyCatTool
29from ai_shell.pytest_tool import PytestTool
30from ai_shell.replace_tool import ReplaceTool
31from ai_shell.rewrite_tool import RewriteTool
32from ai_shell.sed_tool import SedTool
33from ai_shell.todo_tool import TodoTool
34from ai_shell.token_tool import TokenCounterTool
35from ai_shell.toolkit import ToolKit
36from ai_shell.tools_registry import ALL_TOOLS, initialize_all_tools, initialize_recommended_tools
37from ai_shell.utils.config_manager import Config
38from ai_shell.utils.cwd_utils import change_directory
39
40__all__ = [
41    # tools
42    "CatTool",
43    "CutTool",
44    "FindTool",
45    "GrepTool",
46    "HeadTailTool",
47    "LsTool",
48    "GitTool",
49    "TokenCounterTool",
50    "PatchTool",
51    "RewriteTool",
52    "PyCatTool",
53    "SedTool",
54    "ReplaceTool",
55    "InsertTool",
56    "TodoTool",
57    "AnswerCollectorTool",
58    "PytestTool",
59    # registry / dispatch
60    "ToolKit",
61    "ALL_TOOLS",
62    "initialize_all_tools",
63    "initialize_recommended_tools",
64    "Config",
65    # logging
66    "configure_logging",
67    # goal-checker helpers (optional)
68    "invoke_pylint",
69    "pytest_call",
70    "invoke_black",
71    "count_lines_of_code",
72    # misc
73    "change_directory",
74]
class CatTool:
 21class CatTool:
 22    """
 23    Simulates `cat` cli tool.
 24    """
 25
 26    def __init__(self, root_folder: str, config: Config) -> None:
 27        """
 28        Initialize the CatTool class.
 29
 30        Args:
 31            root_folder (str): The root folder path for file operations.
 32            config (Config): The developer input that bot shouldn't set.
 33        """
 34        self.root_folder = root_folder
 35        self.config = config
 36
 37    @log()
 38    def cat_markdown(
 39        self,
 40        file_paths: list[str],
 41        number_lines: bool = True,
 42        squeeze_blank: bool = False,
 43    ) -> str:
 44        """
 45        Concatenates the content of given file paths and formats them as markdown.
 46
 47        Args:
 48            file_paths (list[str]): List of file paths to concatenate.
 49            number_lines (bool, optional): If True, number all output lines.
 50            squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
 51
 52        Returns:
 53            str: The concatenated and formatted content as a string.
 54        """
 55        output = StringIO()
 56        for line in self.cat(file_paths, number_lines, squeeze_blank):
 57            output.write(line)
 58            # output.write("\n")
 59        output.seek(0)
 60        return output.read()
 61
 62    @log()
 63    def cat(
 64        self,
 65        file_paths: list[str],
 66        number_lines: bool = True,
 67        squeeze_blank: bool = False,
 68    ) -> Generator[str, None, None]:
 69        """
 70        Mimics the basic functionalities of the 'cat' command in Unix.
 71
 72        Args:
 73            file_paths (list[str]): A list of file paths to concatenate.
 74            number_lines (bool, optional): If True, number all output lines.
 75            squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
 76
 77        Returns:
 78            Generator[str, None, None]
 79
 80        Yields:
 81            str: Each line of the concatenated files.
 82        """
 83        file_paths = convert_to_list(file_paths)
 84        for location, file_path in enumerate(file_paths):
 85            if file_path.startswith("./"):
 86                file_paths[location] = file_path[2:]
 87
 88        logger.info(f"cat --file_paths {file_paths} " f"--number_lines {number_lines} --squeeze_blank {squeeze_blank}")
 89        for file_path in file_paths:
 90            if not is_file_in_root_folder(file_path, self.root_folder):
 91                raise TypeError("No parent folder traversals allowed")
 92
 93        line_number = 1
 94        for glob_pattern in file_paths:
 95            for file_path in safe_glob(glob_pattern, self.root_folder):
 96                if not os.path.isabs(file_path):
 97                    file_path = self.root_folder + "/" + file_path
 98                try:
 99                    with open(file_path, "rb") as file:
100                        for line in self._process_cat_file(file, line_number, number_lines, squeeze_blank):
101                            yield line
102                            line_number += 1
103                except PermissionError:
104                    logger.warning(f"Permission denied: {file_path}, suppressing from output.")
105
106    def _process_cat_file(
107        self,
108        file: IO[bytes],
109        line_number: int,
110        number_lines: bool,
111        squeeze_blank: bool,
112    ) -> Generator[str, None, None]:
113        """
114        Processes a file for concatenation, applying the specified formatting.
115
116        Args:
117            file: The file object to process.
118            line_number (int): Current line number for numbering lines.
119            number_lines (bool): If True, number all output lines.
120            squeeze_blank (bool): If True, consecutive blank lines are squeezed to one.
121
122        Returns:
123            Generator[str, None, None]: A generator of processed lines.
124
125        Yields:
126            str: Each processed line of the file.
127        """
128        was_blank = False
129        for byte_lines in file:
130            # if isinstance(byte_lines, bytes):
131            line = byte_lines.decode("utf-8")  # Decode bytes to string
132
133            # Use StringIO for memory-efficient line processing
134            with StringIO() as line_buffer:
135                # Normalize line endings to \n
136                line = line.replace("\r\n", "\n")
137                line_buffer.write(line)
138
139                if squeeze_blank and was_blank and line.strip() == "":
140                    continue  # Skip consecutive blank lines
141
142                was_blank = line.strip() == ""
143
144                if number_lines:
145                    line_buffer.seek(0)
146                    line = f"{line_number}\t{line_buffer.read()}"
147                    line_number += 1
148                else:
149                    line = line_buffer.getvalue()
150
151                yield line

Simulates cat cli tool.

CatTool(root_folder: str, config: Config)
26    def __init__(self, root_folder: str, config: Config) -> None:
27        """
28        Initialize the CatTool class.
29
30        Args:
31            root_folder (str): The root folder path for file operations.
32            config (Config): The developer input that bot shouldn't set.
33        """
34        self.root_folder = root_folder
35        self.config = config

Initialize the CatTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
@log()
def cat_markdown( self, file_paths: list[str], number_lines: bool = True, squeeze_blank: bool = False) -> str:
37    @log()
38    def cat_markdown(
39        self,
40        file_paths: list[str],
41        number_lines: bool = True,
42        squeeze_blank: bool = False,
43    ) -> str:
44        """
45        Concatenates the content of given file paths and formats them as markdown.
46
47        Args:
48            file_paths (list[str]): List of file paths to concatenate.
49            number_lines (bool, optional): If True, number all output lines.
50            squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
51
52        Returns:
53            str: The concatenated and formatted content as a string.
54        """
55        output = StringIO()
56        for line in self.cat(file_paths, number_lines, squeeze_blank):
57            output.write(line)
58            # output.write("\n")
59        output.seek(0)
60        return output.read()

Concatenates the content of given file paths and formats them as markdown.

Args: file_paths (list[str]): List of file paths to concatenate. number_lines (bool, optional): If True, number all output lines. squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.

Returns: str: The concatenated and formatted content as a string.

@log()
def cat( self, file_paths: list[str], number_lines: bool = True, squeeze_blank: bool = False) -> Generator[str, None, None]:
 62    @log()
 63    def cat(
 64        self,
 65        file_paths: list[str],
 66        number_lines: bool = True,
 67        squeeze_blank: bool = False,
 68    ) -> Generator[str, None, None]:
 69        """
 70        Mimics the basic functionalities of the 'cat' command in Unix.
 71
 72        Args:
 73            file_paths (list[str]): A list of file paths to concatenate.
 74            number_lines (bool, optional): If True, number all output lines.
 75            squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
 76
 77        Returns:
 78            Generator[str, None, None]
 79
 80        Yields:
 81            str: Each line of the concatenated files.
 82        """
 83        file_paths = convert_to_list(file_paths)
 84        for location, file_path in enumerate(file_paths):
 85            if file_path.startswith("./"):
 86                file_paths[location] = file_path[2:]
 87
 88        logger.info(f"cat --file_paths {file_paths} " f"--number_lines {number_lines} --squeeze_blank {squeeze_blank}")
 89        for file_path in file_paths:
 90            if not is_file_in_root_folder(file_path, self.root_folder):
 91                raise TypeError("No parent folder traversals allowed")
 92
 93        line_number = 1
 94        for glob_pattern in file_paths:
 95            for file_path in safe_glob(glob_pattern, self.root_folder):
 96                if not os.path.isabs(file_path):
 97                    file_path = self.root_folder + "/" + file_path
 98                try:
 99                    with open(file_path, "rb") as file:
100                        for line in self._process_cat_file(file, line_number, number_lines, squeeze_blank):
101                            yield line
102                            line_number += 1
103                except PermissionError:
104                    logger.warning(f"Permission denied: {file_path}, suppressing from output.")

Mimics the basic functionalities of the 'cat' command in Unix.

Args: file_paths (list[str]): A list of file paths to concatenate. number_lines (bool, optional): If True, number all output lines. squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.

Returns: Generator[str, None, None]

Yields: str: Each line of the concatenated files.

@dataclasses.dataclass
class CutTool:
 67@dataclasses.dataclass
 68class CutTool:
 69    """
 70    Simulates `cut` cli tool.
 71    """
 72
 73    def __init__(self, root_folder: str, config: Config) -> None:
 74        """
 75        Initialize the CatTool class.
 76
 77        Args:
 78            root_folder (str): The root folder path for file operations.
 79            config (Config): The developer input that bot shouldn't set.
 80        """
 81        self.root_folder = root_folder
 82        self.config = config
 83        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 84
 85    @log()
 86    def cut_characters(self, file_path: str, character_ranges: str) -> str:
 87        """Reads a file and extracts characters based on specified ranges.
 88
 89        Args:
 90            file_path: The name of the file to process.
 91            character_ranges: A string representing character ranges, e.g., "1-5,10".
 92
 93        Returns:
 94            A string containing the selected characters from the file.
 95        """
 96        if not is_file_in_root_folder(file_path, self.root_folder):
 97            raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.")
 98        ranges = parse_ranges(character_ranges)
 99        output = io.StringIO()
100
101        try:
102            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
103                for line in file:
104                    for i, char in enumerate(line, start=1):
105                        if is_in_ranges(i, ranges):
106                            output.write(char)
107
108                    # Optionally add a newline character after each line
109                    output.write("\n")
110        except FileNotFoundError:
111            tree_text = tree(Path(os.getcwd()))
112            markdown_content = f"# File {file_path} not found. Here are all the files you can see\n\n{tree_text}"
113            return markdown_content
114
115        return output.getvalue()
116
117    @log()
118    def cut_fields(self, filename: str, field_ranges: str, delimiter: str = ",") -> str:
119        """Reads a file and extracts fields based on specified ranges using the given delimiter.
120
121        Args:
122            filename: The name of the file to process.
123            field_ranges: A string representing field ranges, e.g., "1-3,5".
124            delimiter: A single character used as the field delimiter.
125
126        Returns:
127            A string containing the selected fields from the file.
128        """
129        if not is_file_in_root_folder(filename, self.root_folder):
130            raise ValueError(f"File {filename} is not in root folder {self.root_folder}.")
131        ranges = parse_ranges(field_ranges)
132        output = io.StringIO()
133        try:
134            with open(filename, encoding="utf-8", errors=self.utf8_errors) as file:
135                reader = csv.reader(file, delimiter=delimiter)
136
137                for row in reader:
138                    selected_fields = [field for i, field in enumerate(row, start=1) if is_in_ranges(i, ranges)]
139                    output.write(delimiter.join(selected_fields) + "\n")
140        except FileNotFoundError:
141            # Host app should always have cwd == root dir.
142            tree_text = tree(Path(os.getcwd()))
143            markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}"
144            return markdown_content
145
146        return output.getvalue()
147
148    @log()
149    def cut_fields_by_name(self, filename: str, field_names: list[str], delimiter: str = ",") -> str:
150        """Reads a file and extracts fields based on specified field names using the given delimiter.
151
152        Args:
153            filename(str): The name of the file to process.
154            field_names(list[str]): A list of field names to extract.
155            delimiter(str): A single character used as the field delimiter.
156
157        Returns:
158            A string containing the selected fields from the file.
159        """
160        if not is_file_in_root_folder(filename, self.root_folder):
161            raise ValueError(f"File {filename} is not in root folder {self.root_folder}.")
162        output = io.StringIO()
163
164        try:
165            with open(filename, encoding="utf-8", errors=self.utf8_errors) as file:
166                reader = csv.DictReader(file, delimiter=delimiter)
167                # field_indices = {field: i for i, field in enumerate(reader.fieldnames)}
168
169                for row in reader:
170                    selected_fields = [row[field] for field in field_names if field in row]
171                    output.write(delimiter.join(selected_fields) + "\n")
172        except FileNotFoundError:
173            tree_text = tree(Path(os.getcwd()))
174            markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}"
175            return markdown_content
176
177        return output.getvalue()

Simulates cut cli tool.

CutTool(root_folder: str, config: Config)
73    def __init__(self, root_folder: str, config: Config) -> None:
74        """
75        Initialize the CatTool class.
76
77        Args:
78            root_folder (str): The root folder path for file operations.
79            config (Config): The developer input that bot shouldn't set.
80        """
81        self.root_folder = root_folder
82        self.config = config
83        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the CatTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
utf8_errors
@log()
def cut_characters(self, file_path: str, character_ranges: str) -> str:
 85    @log()
 86    def cut_characters(self, file_path: str, character_ranges: str) -> str:
 87        """Reads a file and extracts characters based on specified ranges.
 88
 89        Args:
 90            file_path: The name of the file to process.
 91            character_ranges: A string representing character ranges, e.g., "1-5,10".
 92
 93        Returns:
 94            A string containing the selected characters from the file.
 95        """
 96        if not is_file_in_root_folder(file_path, self.root_folder):
 97            raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.")
 98        ranges = parse_ranges(character_ranges)
 99        output = io.StringIO()
100
101        try:
102            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
103                for line in file:
104                    for i, char in enumerate(line, start=1):
105                        if is_in_ranges(i, ranges):
106                            output.write(char)
107
108                    # Optionally add a newline character after each line
109                    output.write("\n")
110        except FileNotFoundError:
111            tree_text = tree(Path(os.getcwd()))
112            markdown_content = f"# File {file_path} not found. Here are all the files you can see\n\n{tree_text}"
113            return markdown_content
114
115        return output.getvalue()

Reads a file and extracts characters based on specified ranges.

Args: file_path: The name of the file to process. character_ranges: A string representing character ranges, e.g., "1-5,10".

Returns: A string containing the selected characters from the file.

@log()
def cut_fields(self, filename: str, field_ranges: str, delimiter: str = ',') -> str:
117    @log()
118    def cut_fields(self, filename: str, field_ranges: str, delimiter: str = ",") -> str:
119        """Reads a file and extracts fields based on specified ranges using the given delimiter.
120
121        Args:
122            filename: The name of the file to process.
123            field_ranges: A string representing field ranges, e.g., "1-3,5".
124            delimiter: A single character used as the field delimiter.
125
126        Returns:
127            A string containing the selected fields from the file.
128        """
129        if not is_file_in_root_folder(filename, self.root_folder):
130            raise ValueError(f"File {filename} is not in root folder {self.root_folder}.")
131        ranges = parse_ranges(field_ranges)
132        output = io.StringIO()
133        try:
134            with open(filename, encoding="utf-8", errors=self.utf8_errors) as file:
135                reader = csv.reader(file, delimiter=delimiter)
136
137                for row in reader:
138                    selected_fields = [field for i, field in enumerate(row, start=1) if is_in_ranges(i, ranges)]
139                    output.write(delimiter.join(selected_fields) + "\n")
140        except FileNotFoundError:
141            # Host app should always have cwd == root dir.
142            tree_text = tree(Path(os.getcwd()))
143            markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}"
144            return markdown_content
145
146        return output.getvalue()

Reads a file and extracts fields based on specified ranges using the given delimiter.

Args: filename: The name of the file to process. field_ranges: A string representing field ranges, e.g., "1-3,5". delimiter: A single character used as the field delimiter.

Returns: A string containing the selected fields from the file.

@log()
def cut_fields_by_name(self, filename: str, field_names: list[str], delimiter: str = ',') -> str:
148    @log()
149    def cut_fields_by_name(self, filename: str, field_names: list[str], delimiter: str = ",") -> str:
150        """Reads a file and extracts fields based on specified field names using the given delimiter.
151
152        Args:
153            filename(str): The name of the file to process.
154            field_names(list[str]): A list of field names to extract.
155            delimiter(str): A single character used as the field delimiter.
156
157        Returns:
158            A string containing the selected fields from the file.
159        """
160        if not is_file_in_root_folder(filename, self.root_folder):
161            raise ValueError(f"File {filename} is not in root folder {self.root_folder}.")
162        output = io.StringIO()
163
164        try:
165            with open(filename, encoding="utf-8", errors=self.utf8_errors) as file:
166                reader = csv.DictReader(file, delimiter=delimiter)
167                # field_indices = {field: i for i, field in enumerate(reader.fieldnames)}
168
169                for row in reader:
170                    selected_fields = [row[field] for field in field_names if field in row]
171                    output.write(delimiter.join(selected_fields) + "\n")
172        except FileNotFoundError:
173            tree_text = tree(Path(os.getcwd()))
174            markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}"
175            return markdown_content
176
177        return output.getvalue()

Reads a file and extracts fields based on specified field names using the given delimiter.

Args: filename(str): The name of the file to process. field_names(list[str]): A list of field names to extract. delimiter(str): A single character used as the field delimiter.

Returns: A string containing the selected fields from the file.

class FindTool:
 22class FindTool:
 23    def __init__(self, root_folder: str, config: Config) -> None:
 24        """
 25        Initialize the FindTool class.
 26
 27        Args:
 28            root_folder (str): The root folder path for file operations.
 29            config (Config): The developer input that bot shouldn't set.
 30        """
 31        self.root_folder = root_folder
 32        self.config = config
 33        self.auto_cat = config.get_flag("auto_cat", True)
 34
 35    @log()
 36    def find_files(
 37        self,
 38        name: str | None = None,
 39        regex: str | None = None,
 40        file_type: str | None = None,
 41        size: str | None = None,
 42    ) -> list[str]:
 43        """
 44        Recursively search for files or directories matching given criteria in a directory and its subdirectories.
 45
 46        Args:
 47            name (str | None, optional): The exact name to match filenames against.
 48            regex (str | None, optional): The regex pattern to match filenames against.
 49            file_type (str | None, optional): The type to filter ('file' or 'directory').
 50            size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
 51
 52        Returns:
 53            list[str]: A list of paths to files or directories that match the criteria.
 54        """
 55        logger.info(f"find --name {name} --regex {regex} --type {file_type} --size {size}")
 56        matching_files = []
 57        for root, dirs, files in os.walk(os.getcwd()):
 58            # Combine files and directories for type filtering
 59            combined = files
 60            if file_type == "directory":
 61                combined += dirs
 62
 63            for entry in combined:
 64                full_path = os.path.join(root, entry)
 65                # TODO: handle this differently
 66                if "__pycache__" not in full_path:
 67                    # TODO: handle differently. The bot
 68                    # is put into the root_folder as cwd, so as long as there isn't .. in path we should be good.
 69                    # if is_file_in_root_folder(full_path, self.root_folder):
 70                    short_path = remove_root_folder(full_path, self.root_folder)
 71                    # Check for name, regex, and size match
 72                    if (name and fnmatch.fnmatch(entry, name)) or name is None:
 73                        if self._match_type_and_size(full_path, file_type, size):
 74                            matching_files.append(short_path)
 75                    elif regex and re.search(regex, entry):
 76                        if self._match_type_and_size(full_path, file_type, size):
 77                            matching_files.append(short_path)
 78
 79        # Not the best way to remove hidden.
 80        return list(sorted(_ for _ in matching_files if not _.startswith(".")))
 81
 82    def _match_type_and_size(self, path: str, file_type: str | None, size: str | None) -> bool:
 83        """
 84        Check if a file/directory matches the specified type and size criteria.
 85
 86        Args:
 87            path (str): The path to the file/directory.
 88            file_type (Optional[str]): The type to filter ('file' or 'directory').
 89            size (Optional[str]): The size to filter files by.
 90
 91        Returns:
 92            bool: True if the file/directory matches the criteria, False otherwise.
 93        """
 94        if file_type:
 95            if file_type == "file" and not os.path.isfile(path):
 96                return False
 97            if file_type == "directory" and not os.path.isdir(path):
 98                return False
 99
100        if size:
101            size_prefix = size[0]
102            size_value = int(size[1:])
103            file_size = os.path.getsize(path)
104
105            if size_prefix == "+" and file_size <= size_value:
106                return False
107            if size_prefix == "-" and file_size >= size_value:
108                return False
109        return True
110
111    @log()
112    def find_files_markdown(
113        self,
114        name: str | None = None,
115        regex: str | None = None,
116        file_type: str | None = None,
117        size: str | None = None,
118    ) -> str:
119        """
120        Recursively search for files or directories matching given criteria in a directory and its subdirectories.
121
122        Args:
123            name (str | None, optional): The exact name to match filenames against.
124            regex (str | None, optional): The regex pattern to match filenames against.
125            file_type (str | None, optional): The type to filter ('file' or 'directory').
126            size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
127
128        Returns:
129            str: Markdown of paths to files or directories that match the criteria.
130        """
131        output = StringIO()
132        results = self.find_files(name, regex, file_type, size)
133        for item in results:
134            output.write(item)
135            output.write("\n")
136        output.seek(0)
137        return output.read()
FindTool(root_folder: str, config: Config)
23    def __init__(self, root_folder: str, config: Config) -> None:
24        """
25        Initialize the FindTool class.
26
27        Args:
28            root_folder (str): The root folder path for file operations.
29            config (Config): The developer input that bot shouldn't set.
30        """
31        self.root_folder = root_folder
32        self.config = config
33        self.auto_cat = config.get_flag("auto_cat", True)

Initialize the FindTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
@log()
def find_files( self, name: str | None = None, regex: str | None = None, file_type: str | None = None, size: str | None = None) -> list[str]:
35    @log()
36    def find_files(
37        self,
38        name: str | None = None,
39        regex: str | None = None,
40        file_type: str | None = None,
41        size: str | None = None,
42    ) -> list[str]:
43        """
44        Recursively search for files or directories matching given criteria in a directory and its subdirectories.
45
46        Args:
47            name (str | None, optional): The exact name to match filenames against.
48            regex (str | None, optional): The regex pattern to match filenames against.
49            file_type (str | None, optional): The type to filter ('file' or 'directory').
50            size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
51
52        Returns:
53            list[str]: A list of paths to files or directories that match the criteria.
54        """
55        logger.info(f"find --name {name} --regex {regex} --type {file_type} --size {size}")
56        matching_files = []
57        for root, dirs, files in os.walk(os.getcwd()):
58            # Combine files and directories for type filtering
59            combined = files
60            if file_type == "directory":
61                combined += dirs
62
63            for entry in combined:
64                full_path = os.path.join(root, entry)
65                # TODO: handle this differently
66                if "__pycache__" not in full_path:
67                    # TODO: handle differently. The bot
68                    # is put into the root_folder as cwd, so as long as there isn't .. in path we should be good.
69                    # if is_file_in_root_folder(full_path, self.root_folder):
70                    short_path = remove_root_folder(full_path, self.root_folder)
71                    # Check for name, regex, and size match
72                    if (name and fnmatch.fnmatch(entry, name)) or name is None:
73                        if self._match_type_and_size(full_path, file_type, size):
74                            matching_files.append(short_path)
75                    elif regex and re.search(regex, entry):
76                        if self._match_type_and_size(full_path, file_type, size):
77                            matching_files.append(short_path)
78
79        # Not the best way to remove hidden.
80        return list(sorted(_ for _ in matching_files if not _.startswith(".")))

Recursively search for files or directories matching given criteria in a directory and its subdirectories.

Args: name (str | None, optional): The exact name to match filenames against. regex (str | None, optional): The regex pattern to match filenames against. file_type (str | None, optional): The type to filter ('file' or 'directory'). size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.

Returns: list[str]: A list of paths to files or directories that match the criteria.

@log()
def find_files_markdown( self, name: str | None = None, regex: str | None = None, file_type: str | None = None, size: str | None = None) -> str:
111    @log()
112    def find_files_markdown(
113        self,
114        name: str | None = None,
115        regex: str | None = None,
116        file_type: str | None = None,
117        size: str | None = None,
118    ) -> str:
119        """
120        Recursively search for files or directories matching given criteria in a directory and its subdirectories.
121
122        Args:
123            name (str | None, optional): The exact name to match filenames against.
124            regex (str | None, optional): The regex pattern to match filenames against.
125            file_type (str | None, optional): The type to filter ('file' or 'directory').
126            size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
127
128        Returns:
129            str: Markdown of paths to files or directories that match the criteria.
130        """
131        output = StringIO()
132        results = self.find_files(name, regex, file_type, size)
133        for item in results:
134            output.write(item)
135            output.write("\n")
136        output.seek(0)
137        return output.read()

Recursively search for files or directories matching given criteria in a directory and its subdirectories.

Args: name (str | None, optional): The exact name to match filenames against. regex (str | None, optional): The regex pattern to match filenames against. file_type (str | None, optional): The type to filter ('file' or 'directory'). size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.

Returns: str: Markdown of paths to files or directories that match the criteria.

class GrepTool:
 46class GrepTool:
 47    """A tool for searching files using regular expressions."""
 48
 49    def __init__(self, root_folder: str, config: Config) -> None:
 50        """
 51        Initialize the GrepTool with a root folder.
 52
 53        Args:
 54            root_folder (str): The root folder to search within.
 55            config (Config): The developer input that bot shouldn't set.
 56        """
 57        self.root_folder: str = root_folder
 58        self.config = config
 59        self.auto_cat = config.get_flag("auto_cat", True)
 60        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 61
 62    @log()
 63    def grep_markdown(
 64        self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches: int = -1
 65    ) -> str:
 66        """
 67        Search for lines matching a regular expression in files and returns markdown formatted results.
 68
 69        Args:
 70            regex (str): A regular expression string to search for.
 71            glob_pattern (str): A glob pattern string to specify files.
 72            skip_first_matches (int, optional): Number of initial matches to skip.
 73            maximum_matches (int, optional): Maximum number of matches to return.
 74
 75        Returns:
 76            str: Markdown formatted string of grep results.
 77        """
 78        results = self.grep(regex, glob_pattern, skip_first_matches, maximum_matches)
 79        matches_found = results.matches_found
 80
 81        output = StringIO()
 82        for file_match in results.data:
 83            output.write(file_match.filename + "\n")
 84            for match in file_match.found:
 85                output.write(f"line {match.line_number}: {match.line}\n")
 86        output.write(
 87            f"{matches_found} matches found and {min(matches_found, maximum_matches) if maximum_matches != -1 else matches_found} displayed. "
 88            f"Skipped {skip_first_matches}\n"
 89        )
 90        output.seek(0)
 91        return output.read()
 92
 93    @log()
 94    def grep(
 95        self,
 96        regex: str,
 97        glob_pattern: str,
 98        skip_first_matches: int = -1,
 99        maximum_matches_per_file: int = -1,
100        maximum_matches_total: int = -1,
101    ) -> GrepResults:
102        """
103        Search for lines matching a regular expression in files specified by a glob pattern.
104
105        Args:
106            regex (str): A regular expression string to search for.
107            glob_pattern (str): A glob pattern string to specify files.
108            skip_first_matches (int, optional): Number of initial matches to skip.
109            maximum_matches_per_file (int, optional): Maximum number of matches to return for one file.
110            maximum_matches_total (int, optional): Maximum number of matches to return total.
111
112        Returns:
113            GrepResults: The results of the grep operation.
114        """
115        logger.info(
116            f"grep --regex {regex} --glob_pattern {glob_pattern} "
117            f"--skip_first_matches {skip_first_matches} "
118            f"--maximum_matches_total {maximum_matches_total} "
119            f"--maximum_matches_per_file {maximum_matches_per_file}"
120        )
121        pattern = re.compile(regex)
122        matches_total = 0
123        skip_count = 0 if skip_first_matches < 0 else skip_first_matches
124
125        results = GrepResults(matches_found=-1)
126
127        for filename in glob.glob(glob_pattern, root_dir=self.root_folder, recursive=True):
128            matches_per_file = 0
129            if os.path.isdir(filename):
130                logging.warning(f"Skipping directory {filename}, because it isn't a file.")
131                continue
132            if not os.path.exists(filename):
133                # What a hack
134                open_path = self.root_folder + "/" + filename
135            else:
136                open_path = filename
137            with open(open_path, encoding="utf-8", errors=self.utf8_errors) as file:
138                if not is_file_in_root_folder(filename, self.root_folder):
139                    logging.warning(f"Skipping file {filename}, because it isn't in the root folder.")
140                    continue
141                line_number = 0
142                for line in file:
143                    below_maximum = matches_per_file < maximum_matches_per_file
144                    maximum_not_set = maximum_matches_per_file == -1
145                    if below_maximum or maximum_not_set:
146                        line_number += 1
147                        if pattern.search(line):
148                            matches_total += 1
149                            matches_per_file += 1
150
151                            if matches_total <= (matches_total + skip_count) or matches_total == -1:
152                                if (0 < skip_first_matches < matches_total) or skip_first_matches == -1:
153                                    # This creates names like \..\..\..\ etc.
154                                    minimal_filename = remove_root_folder(filename, self.root_folder)
155                                    # avoid double count
156                                    found = next((fm for fm in results.data if fm.filename == minimal_filename), None)
157                                    if not found:
158                                        found = FileMatches(filename=minimal_filename)
159                                        results.data.append(found)
160
161                                    found.found.append(Match(line_number=line_number, line=line.strip()))
162        results.data = list(sorted(results.data, key=lambda x: x.filename))
163        results.matches_found = matches_total
164        return results

A tool for searching files using regular expressions.

GrepTool(root_folder: str, config: Config)
49    def __init__(self, root_folder: str, config: Config) -> None:
50        """
51        Initialize the GrepTool with a root folder.
52
53        Args:
54            root_folder (str): The root folder to search within.
55            config (Config): The developer input that bot shouldn't set.
56        """
57        self.root_folder: str = root_folder
58        self.config = config
59        self.auto_cat = config.get_flag("auto_cat", True)
60        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the GrepTool with a root folder.

Args: root_folder (str): The root folder to search within. config (Config): The developer input that bot shouldn't set.

root_folder: str
config
auto_cat
utf8_errors
@log()
def grep_markdown( self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches: int = -1) -> str:
62    @log()
63    def grep_markdown(
64        self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches: int = -1
65    ) -> str:
66        """
67        Search for lines matching a regular expression in files and returns markdown formatted results.
68
69        Args:
70            regex (str): A regular expression string to search for.
71            glob_pattern (str): A glob pattern string to specify files.
72            skip_first_matches (int, optional): Number of initial matches to skip.
73            maximum_matches (int, optional): Maximum number of matches to return.
74
75        Returns:
76            str: Markdown formatted string of grep results.
77        """
78        results = self.grep(regex, glob_pattern, skip_first_matches, maximum_matches)
79        matches_found = results.matches_found
80
81        output = StringIO()
82        for file_match in results.data:
83            output.write(file_match.filename + "\n")
84            for match in file_match.found:
85                output.write(f"line {match.line_number}: {match.line}\n")
86        output.write(
87            f"{matches_found} matches found and {min(matches_found, maximum_matches) if maximum_matches != -1 else matches_found} displayed. "
88            f"Skipped {skip_first_matches}\n"
89        )
90        output.seek(0)
91        return output.read()

Search for lines matching a regular expression in files and returns markdown formatted results.

Args: regex (str): A regular expression string to search for. glob_pattern (str): A glob pattern string to specify files. skip_first_matches (int, optional): Number of initial matches to skip. maximum_matches (int, optional): Maximum number of matches to return.

Returns: str: Markdown formatted string of grep results.

@log()
def grep( self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches_per_file: int = -1, maximum_matches_total: int = -1) -> ai_shell.grep_tool.GrepResults:
 93    @log()
 94    def grep(
 95        self,
 96        regex: str,
 97        glob_pattern: str,
 98        skip_first_matches: int = -1,
 99        maximum_matches_per_file: int = -1,
100        maximum_matches_total: int = -1,
101    ) -> GrepResults:
102        """
103        Search for lines matching a regular expression in files specified by a glob pattern.
104
105        Args:
106            regex (str): A regular expression string to search for.
107            glob_pattern (str): A glob pattern string to specify files.
108            skip_first_matches (int, optional): Number of initial matches to skip.
109            maximum_matches_per_file (int, optional): Maximum number of matches to return for one file.
110            maximum_matches_total (int, optional): Maximum number of matches to return total.
111
112        Returns:
113            GrepResults: The results of the grep operation.
114        """
115        logger.info(
116            f"grep --regex {regex} --glob_pattern {glob_pattern} "
117            f"--skip_first_matches {skip_first_matches} "
118            f"--maximum_matches_total {maximum_matches_total} "
119            f"--maximum_matches_per_file {maximum_matches_per_file}"
120        )
121        pattern = re.compile(regex)
122        matches_total = 0
123        skip_count = 0 if skip_first_matches < 0 else skip_first_matches
124
125        results = GrepResults(matches_found=-1)
126
127        for filename in glob.glob(glob_pattern, root_dir=self.root_folder, recursive=True):
128            matches_per_file = 0
129            if os.path.isdir(filename):
130                logging.warning(f"Skipping directory {filename}, because it isn't a file.")
131                continue
132            if not os.path.exists(filename):
133                # What a hack
134                open_path = self.root_folder + "/" + filename
135            else:
136                open_path = filename
137            with open(open_path, encoding="utf-8", errors=self.utf8_errors) as file:
138                if not is_file_in_root_folder(filename, self.root_folder):
139                    logging.warning(f"Skipping file {filename}, because it isn't in the root folder.")
140                    continue
141                line_number = 0
142                for line in file:
143                    below_maximum = matches_per_file < maximum_matches_per_file
144                    maximum_not_set = maximum_matches_per_file == -1
145                    if below_maximum or maximum_not_set:
146                        line_number += 1
147                        if pattern.search(line):
148                            matches_total += 1
149                            matches_per_file += 1
150
151                            if matches_total <= (matches_total + skip_count) or matches_total == -1:
152                                if (0 < skip_first_matches < matches_total) or skip_first_matches == -1:
153                                    # This creates names like \..\..\..\ etc.
154                                    minimal_filename = remove_root_folder(filename, self.root_folder)
155                                    # avoid double count
156                                    found = next((fm for fm in results.data if fm.filename == minimal_filename), None)
157                                    if not found:
158                                        found = FileMatches(filename=minimal_filename)
159                                        results.data.append(found)
160
161                                    found.found.append(Match(line_number=line_number, line=line.strip()))
162        results.data = list(sorted(results.data, key=lambda x: x.filename))
163        results.matches_found = matches_total
164        return results

Search for lines matching a regular expression in files specified by a glob pattern.

Args: regex (str): A regular expression string to search for. glob_pattern (str): A glob pattern string to specify files. skip_first_matches (int, optional): Number of initial matches to skip. maximum_matches_per_file (int, optional): Maximum number of matches to return for one file. maximum_matches_total (int, optional): Maximum number of matches to return total.

Returns: GrepResults: The results of the grep operation.

class HeadTailTool:
 16class HeadTailTool:
 17    def __init__(self, root_folder: str, config: Config) -> None:
 18        """Initialize the HeadTailTool with a root folder.
 19
 20        Args:
 21            root_folder (str): The root folder where files will be checked.
 22            config (Config): The developer input that bot shouldn't set.
 23        """
 24        self.root_folder = root_folder
 25        self.config = config
 26        self.auto_cat = config.get_flag("auto_cat", True)
 27
 28    @log()
 29    def head_markdown(self, file_path: str, lines: int = 10) -> str:
 30        """Return the first 'lines' lines of a file formatted as markdown.
 31
 32        Args:
 33            file_path (str): Path to the file.
 34            lines (int, optional): Number of lines to return. Defaults to 10.
 35
 36        Returns:
 37            str: String containing the first 'lines' lines of the file.
 38        """
 39        return "\n".join(self.head(file_path, lines))
 40
 41    @log()
 42    def head(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
 43        """Return the first 'lines' or 'byte_count' from a file.
 44
 45        Args:
 46            file_path (str): Path to the file.
 47            lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10.
 48            byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
 49
 50        Returns:
 51            list[str]: Lines or byte_count of bytes from the start of the file.
 52        """
 53        return self.head_tail(file_path, lines, "head", byte_count)
 54
 55    @log()
 56    def tail_markdown(self, file_path: str, lines: int = 10) -> str:
 57        """Return the last 'lines' lines of a file formatted as markdown.
 58
 59        Args:
 60            file_path (str): Path to the file.
 61            lines (int, optional): Number of lines to return. Defaults to 10.
 62
 63        Returns:
 64            str: String containing the last 'lines' lines of the file.
 65        """
 66        return "\n".join(self.tail(file_path, lines))
 67
 68    @log()
 69    def tail(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
 70        """Return the last 'lines' or 'bytes' from a file.
 71
 72        Args:
 73            file_path (str): Path to the file.
 74            lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10.
 75            byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
 76
 77        Returns:
 78            list[str]: Lines or bytes from the end of the file.
 79        """
 80        return self.head_tail(file_path, lines, "tail", byte_count)
 81
 82    def head_tail(
 83        self, file_path: str, lines: int = 10, mode: str = "head", byte_count: int | None = None
 84    ) -> list[str]:
 85        """Read lines or bytes from the start ('head') or end ('tail') of a file.
 86
 87        Args:
 88            file_path (str): Path to the file.
 89            lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10.
 90            mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'.
 91            byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines.
 92
 93        Returns:
 94            list[str]: Requested lines or bytes from the file.
 95
 96        Raises:
 97            ValueError: If mode is not 'head' or 'tail'.
 98            FileNotFoundError: If the file is not found in the root folder.
 99        """
100        if mode == "head":
101            logger.info(f"head --file_path {file_path} --lines {lines}")
102        else:
103            logger.info(f"tail --file_path {file_path} --lines {lines}")
104        if mode not in ["head", "tail"]:
105            raise ValueError("Mode must be 'head' or 'tail'")
106
107        if not is_file_in_root_folder(file_path, self.root_folder):
108            raise FileNotFoundError(f"File {file_path} not found in root folder {self.root_folder}")
109
110        with open(file_path, "rb") as file:
111            if byte_count is not None:
112                if mode == "head":
113                    return [file.read(byte_count).decode()]
114                # mode == 'tail'
115                file.seek(-byte_count, 2)  # Seek from end of file
116                return [file.read(byte_count).decode()]
117
118            # Read by lines if byte_count is not specified
119            if mode == "head":
120                head_lines = []
121                for _ in range(lines):
122                    try:
123                        line = next(file).decode("utf-8")
124                        head_lines.append(line.rstrip("\r\n"))
125                    except StopIteration:
126                        break
127                return head_lines
128                # return [next(file).decode("utf-8").rstrip("\r\n") for _ in range(lines)]
129            # mode == 'tail'
130            return [line.decode("utf-8").rstrip("\r\n") for line in list(file)[-lines:]]
HeadTailTool(root_folder: str, config: Config)
17    def __init__(self, root_folder: str, config: Config) -> None:
18        """Initialize the HeadTailTool with a root folder.
19
20        Args:
21            root_folder (str): The root folder where files will be checked.
22            config (Config): The developer input that bot shouldn't set.
23        """
24        self.root_folder = root_folder
25        self.config = config
26        self.auto_cat = config.get_flag("auto_cat", True)

Initialize the HeadTailTool with a root folder.

Args: root_folder (str): The root folder where files will be checked. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
@log()
def head_markdown(self, file_path: str, lines: int = 10) -> str:
28    @log()
29    def head_markdown(self, file_path: str, lines: int = 10) -> str:
30        """Return the first 'lines' lines of a file formatted as markdown.
31
32        Args:
33            file_path (str): Path to the file.
34            lines (int, optional): Number of lines to return. Defaults to 10.
35
36        Returns:
37            str: String containing the first 'lines' lines of the file.
38        """
39        return "\n".join(self.head(file_path, lines))

Return the first 'lines' lines of a file formatted as markdown.

Args: file_path (str): Path to the file. lines (int, optional): Number of lines to return. Defaults to 10.

Returns: str: String containing the first 'lines' lines of the file.

@log()
def head( self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
41    @log()
42    def head(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
43        """Return the first 'lines' or 'byte_count' from a file.
44
45        Args:
46            file_path (str): Path to the file.
47            lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10.
48            byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
49
50        Returns:
51            list[str]: Lines or byte_count of bytes from the start of the file.
52        """
53        return self.head_tail(file_path, lines, "head", byte_count)

Return the first 'lines' or 'byte_count' from a file.

Args: file_path (str): Path to the file. lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.

Returns: list[str]: Lines or byte_count of bytes from the start of the file.

@log()
def tail_markdown(self, file_path: str, lines: int = 10) -> str:
55    @log()
56    def tail_markdown(self, file_path: str, lines: int = 10) -> str:
57        """Return the last 'lines' lines of a file formatted as markdown.
58
59        Args:
60            file_path (str): Path to the file.
61            lines (int, optional): Number of lines to return. Defaults to 10.
62
63        Returns:
64            str: String containing the last 'lines' lines of the file.
65        """
66        return "\n".join(self.tail(file_path, lines))

Return the last 'lines' lines of a file formatted as markdown.

Args: file_path (str): Path to the file. lines (int, optional): Number of lines to return. Defaults to 10.

Returns: str: String containing the last 'lines' lines of the file.

@log()
def tail( self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
68    @log()
69    def tail(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]:
70        """Return the last 'lines' or 'bytes' from a file.
71
72        Args:
73            file_path (str): Path to the file.
74            lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10.
75            byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
76
77        Returns:
78            list[str]: Lines or bytes from the end of the file.
79        """
80        return self.head_tail(file_path, lines, "tail", byte_count)

Return the last 'lines' or 'bytes' from a file.

Args: file_path (str): Path to the file. lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.

Returns: list[str]: Lines or bytes from the end of the file.

def head_tail( self, file_path: str, lines: int = 10, mode: str = 'head', byte_count: int | None = None) -> list[str]:
 82    def head_tail(
 83        self, file_path: str, lines: int = 10, mode: str = "head", byte_count: int | None = None
 84    ) -> list[str]:
 85        """Read lines or bytes from the start ('head') or end ('tail') of a file.
 86
 87        Args:
 88            file_path (str): Path to the file.
 89            lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10.
 90            mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'.
 91            byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines.
 92
 93        Returns:
 94            list[str]: Requested lines or bytes from the file.
 95
 96        Raises:
 97            ValueError: If mode is not 'head' or 'tail'.
 98            FileNotFoundError: If the file is not found in the root folder.
 99        """
100        if mode == "head":
101            logger.info(f"head --file_path {file_path} --lines {lines}")
102        else:
103            logger.info(f"tail --file_path {file_path} --lines {lines}")
104        if mode not in ["head", "tail"]:
105            raise ValueError("Mode must be 'head' or 'tail'")
106
107        if not is_file_in_root_folder(file_path, self.root_folder):
108            raise FileNotFoundError(f"File {file_path} not found in root folder {self.root_folder}")
109
110        with open(file_path, "rb") as file:
111            if byte_count is not None:
112                if mode == "head":
113                    return [file.read(byte_count).decode()]
114                # mode == 'tail'
115                file.seek(-byte_count, 2)  # Seek from end of file
116                return [file.read(byte_count).decode()]
117
118            # Read by lines if byte_count is not specified
119            if mode == "head":
120                head_lines = []
121                for _ in range(lines):
122                    try:
123                        line = next(file).decode("utf-8")
124                        head_lines.append(line.rstrip("\r\n"))
125                    except StopIteration:
126                        break
127                return head_lines
128                # return [next(file).decode("utf-8").rstrip("\r\n") for _ in range(lines)]
129            # mode == 'tail'
130            return [line.decode("utf-8").rstrip("\r\n") for line in list(file)[-lines:]]

Read lines or bytes from the start ('head') or end ('tail') of a file.

Args: file_path (str): Path to the file. lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10. mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'. byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines.

Returns: list[str]: Requested lines or bytes from the file.

Raises: ValueError: If mode is not 'head' or 'tail'. FileNotFoundError: If the file is not found in the root folder.

class LsTool:
 21class LsTool:
 22    def __init__(self, root_folder: str, config: Config) -> None:
 23        """
 24        Initialize the FindTool class.
 25
 26        Args:
 27            root_folder (str): The root folder path for file operations.
 28            config (Config): The developer input that bot shouldn't set.
 29        """
 30        self.root_folder = root_folder
 31        self.config = config
 32        self.auto_cat = config.get_flag("auto_cat", True)
 33
 34    @log()
 35    def ls_markdown(self, path: str | None = ".", all_files: bool = False, long: bool = False) -> str:
 36        """List directory contents, with options to include all files and detailed view.
 37
 38        Args:
 39            path (str | None, optional): The directory path to list. Defaults to the current directory '.'.
 40            all_files (bool, optional): If True, include hidden files. Defaults to False.
 41            long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
 42
 43        Returns:
 44            str: The markdown representation of the ls command output.
 45        """
 46        try:
 47            entries_info = self.ls(path, all_files, long)
 48        except (FileNotFoundError, NotADirectoryError):
 49            tree_text = tree(Path(os.getcwd()))
 50            markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}"
 51            return markdown_content
 52
 53        output = StringIO()
 54
 55        is_first = True
 56        for line in entries_info:
 57            if not is_first:
 58                output.write("\n")
 59            is_first = False
 60            output.write(line)
 61
 62        output.seek(0)
 63        return output.read()
 64
 65    @log()
 66    def ls(self, path: str | None = None, all_files: bool = False, long: bool = False) -> Union[list[str], str]:
 67        """
 68        List directory contents, with options to include all files and detailed view.
 69
 70        Args:
 71            path (str | None, optional): The directory path to list. Defaults to the current directory '.'.
 72            all_files (bool, optional): If True, include hidden files. Defaults to False.
 73            long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
 74
 75        Returns:
 76            list[str] | str: List of files and directories, optionally with details.
 77        """
 78        logger.info(f"ls --path {path} --all_files {all_files} --long  {long}")
 79
 80        if path is None:
 81            path = ""
 82
 83        if path is not None and ("?" in path or "*" in path or "[" in path or "]" in path):
 84            # Globs behave very different from non-globs. :(
 85            #  or "{" in path or "}"  <-- is this a glob pattern?
 86            entries = safe_glob(path, self.root_folder)
 87        else:
 88            try:
 89                # enumerate list to check if the path exists
 90                # os.listdir order is OS-dependent (arbitrary on Linux), so sort
 91                # for deterministic output that matches real `ls` behavior.
 92                entries = sorted(
 93                    (_ for _ in os.listdir(path))
 94                    if all_files
 95                    else (entry for entry in os.listdir(path) if not entry.startswith("."))
 96                )
 97            except (FileNotFoundError, NotADirectoryError):
 98                # if not, just tell the bot everything.
 99                tree_text = tree(Path(os.getcwd()))
100                markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}"
101                return markdown_content
102        entries_info = []
103
104        for entry in entries:
105            # is this None-safety here correct?
106            full_path = entry if path is None else os.path.join(path, entry)
107            if not is_file_in_root_folder(full_path, self.root_folder):
108                continue
109            if os.path.isdir(full_path) and entry.endswith("__pycache__"):
110                continue
111            if long:
112                stats = os.stat(full_path)
113                # Always human readable, too many tokens for byte count.
114                size = human_readable_size(stats.st_size)
115                mod_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(stats.st_mtime))
116                entries_info.append(f"{size:} {mod_time} {entry}")
117            else:
118                entries_info.append(entry)
119        if logger.level == logging.DEBUG:
120            for line in entries_info:
121                logger.debug(line)
122        return entries_info
LsTool(root_folder: str, config: Config)
22    def __init__(self, root_folder: str, config: Config) -> None:
23        """
24        Initialize the FindTool class.
25
26        Args:
27            root_folder (str): The root folder path for file operations.
28            config (Config): The developer input that bot shouldn't set.
29        """
30        self.root_folder = root_folder
31        self.config = config
32        self.auto_cat = config.get_flag("auto_cat", True)

Initialize the FindTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
@log()
def ls_markdown( self, path: str | None = '.', all_files: bool = False, long: bool = False) -> str:
34    @log()
35    def ls_markdown(self, path: str | None = ".", all_files: bool = False, long: bool = False) -> str:
36        """List directory contents, with options to include all files and detailed view.
37
38        Args:
39            path (str | None, optional): The directory path to list. Defaults to the current directory '.'.
40            all_files (bool, optional): If True, include hidden files. Defaults to False.
41            long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
42
43        Returns:
44            str: The markdown representation of the ls command output.
45        """
46        try:
47            entries_info = self.ls(path, all_files, long)
48        except (FileNotFoundError, NotADirectoryError):
49            tree_text = tree(Path(os.getcwd()))
50            markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}"
51            return markdown_content
52
53        output = StringIO()
54
55        is_first = True
56        for line in entries_info:
57            if not is_first:
58                output.write("\n")
59            is_first = False
60            output.write(line)
61
62        output.seek(0)
63        return output.read()

List directory contents, with options to include all files and detailed view.

Args: path (str | None, optional): The directory path to list. Defaults to the current directory '.'. all_files (bool, optional): If True, include hidden files. Defaults to False. long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.

Returns: str: The markdown representation of the ls command output.

@log()
def ls( self, path: str | None = None, all_files: bool = False, long: bool = False) -> Union[list[str], str]:
 65    @log()
 66    def ls(self, path: str | None = None, all_files: bool = False, long: bool = False) -> Union[list[str], str]:
 67        """
 68        List directory contents, with options to include all files and detailed view.
 69
 70        Args:
 71            path (str | None, optional): The directory path to list. Defaults to the current directory '.'.
 72            all_files (bool, optional): If True, include hidden files. Defaults to False.
 73            long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
 74
 75        Returns:
 76            list[str] | str: List of files and directories, optionally with details.
 77        """
 78        logger.info(f"ls --path {path} --all_files {all_files} --long  {long}")
 79
 80        if path is None:
 81            path = ""
 82
 83        if path is not None and ("?" in path or "*" in path or "[" in path or "]" in path):
 84            # Globs behave very different from non-globs. :(
 85            #  or "{" in path or "}"  <-- is this a glob pattern?
 86            entries = safe_glob(path, self.root_folder)
 87        else:
 88            try:
 89                # enumerate list to check if the path exists
 90                # os.listdir order is OS-dependent (arbitrary on Linux), so sort
 91                # for deterministic output that matches real `ls` behavior.
 92                entries = sorted(
 93                    (_ for _ in os.listdir(path))
 94                    if all_files
 95                    else (entry for entry in os.listdir(path) if not entry.startswith("."))
 96                )
 97            except (FileNotFoundError, NotADirectoryError):
 98                # if not, just tell the bot everything.
 99                tree_text = tree(Path(os.getcwd()))
100                markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}"
101                return markdown_content
102        entries_info = []
103
104        for entry in entries:
105            # is this None-safety here correct?
106            full_path = entry if path is None else os.path.join(path, entry)
107            if not is_file_in_root_folder(full_path, self.root_folder):
108                continue
109            if os.path.isdir(full_path) and entry.endswith("__pycache__"):
110                continue
111            if long:
112                stats = os.stat(full_path)
113                # Always human readable, too many tokens for byte count.
114                size = human_readable_size(stats.st_size)
115                mod_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(stats.st_mtime))
116                entries_info.append(f"{size:} {mod_time} {entry}")
117            else:
118                entries_info.append(entry)
119        if logger.level == logging.DEBUG:
120            for line in entries_info:
121                logger.debug(line)
122        return entries_info

List directory contents, with options to include all files and detailed view.

Args: path (str | None, optional): The directory path to list. Defaults to the current directory '.'. all_files (bool, optional): If True, include hidden files. Defaults to False. long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.

Returns: list[str] | str: List of files and directories, optionally with details.

class GitTool:
 23class GitTool:
 24    def __init__(self, root_folder: str, config: Config) -> None:
 25        """
 26        Initialize the GitTool class.
 27
 28        Args:
 29            root_folder (str): The root folder path for repo operations.
 30            config (Config): The developer input that bot shouldn't set.
 31        """
 32        self.repo_path = root_folder
 33        self.config = config
 34        self.auto_cat = config.get_flag("auto_cat", True)
 35        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 36
 37    def _git(self, args: str) -> CommandResult:
 38        """Run a git command against the repo and return its result.
 39
 40        Args:
 41            args (str): The git subcommand and arguments (already shell-safe).
 42
 43        Returns:
 44            CommandResult: stdout/stderr/return_code of the command.
 45        """
 46        repo = shlex.quote(os.path.abspath(self.repo_path))
 47        result = safe_subprocess("git", f"-C {repo} {args}")
 48        if result.return_code != 0:
 49            logger.warning("git %s failed: %s", args, result.stderr)
 50        return result
 51
 52    def is_ignored_by_gitignore(self, file_path: str, gitignore_path: str = ".gitignore") -> bool:
 53        """
 54        Check if a file is ignored by .gitignore.
 55
 56        Args:
 57            file_path (str): The path of the file to check.
 58            gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory.
 59
 60        Returns:
 61            bool: True if the file is ignored, False otherwise.
 62
 63        Raises:
 64            FileNotFoundError: If the .gitignore file is not found.
 65        """
 66        full_gitignore_path = os.path.join(self.repo_path, gitignore_path)
 67
 68        if not os.path.isfile(full_gitignore_path):
 69            raise FileNotFoundError(f"No .gitignore file found at {full_gitignore_path}")
 70
 71        file_path = os.path.abspath(file_path)
 72
 73        with open(full_gitignore_path, encoding="utf-8", errors=self.utf8_errors) as gitignore:
 74            for line in gitignore:
 75                line = line.strip()
 76                if not line or line.startswith("#"):
 77                    continue
 78                gitignore_pattern = os.path.join(os.path.dirname(gitignore_path), line)
 79                if fnmatch.fnmatch(file_path, gitignore_pattern):
 80                    return True
 81        return False
 82
 83    @log()
 84    def git_status(self) -> dict[str, Any]:
 85        """Returns the status of the repository.
 86
 87        Returns:
 88            dict[str, Any]: Structured `git status` response
 89        """
 90        result = self._git("status --porcelain")
 91        changed_files: list[str] = []
 92        untracked_files: list[str] = []
 93        for line in result.stdout.splitlines():
 94            if not line:
 95                continue
 96            # porcelain format: 2-char status code, a space, then the path
 97            path = line[3:].strip()
 98            if line.startswith("??"):
 99                untracked_files.append(path)
100            else:
101                changed_files.append(path)
102        return {"changed_files": changed_files, "untracked_files": untracked_files}
103
104    @log()
105    def get_current_branch(self) -> str:
106        """
107        Retrieves the current branch name of the repository.
108
109        Returns:
110            str: The current branch name.
111        """
112        return self._git("branch --show-current").stdout.strip()
113
114    @log()
115    def get_recent_commits(self, n: int = 10, short_hash: bool = False) -> list[dict[str, Any]]:
116        """
117        Retrieves the most recent commit hashes from the current branch.
118
119        Args:
120            n (int, optional): The number of recent commits to retrieve. Defaults to 10.
121            short_hash (bool, optional): If True, also return short hashes. Defaults to False.
122
123        Returns:
124            list[dict[str, Any]]: One dict per commit with a 'full_hash' (and
125                                  'short_hash' when requested).
126        """
127        result = self._git(f"log --pretty=format:%H -n {int(n)}")
128        hashes = [h for h in result.stdout.splitlines() if h]
129        if short_hash:
130            return [{"short_hash": h[:7], "full_hash": h} for h in hashes]
131        return [{"full_hash": h} for h in hashes]
132
133    @log()
134    def git_diff(self) -> list[dict[str, Any]]:
135        """Returns the differences in the working directory.
136
137        Returns:
138            list[dict[str, Any]]: Structured `git diff` response
139        """
140        diffs = self._git("diff HEAD --name-only").stdout.splitlines()
141        return [{"file": diff} for diff in diffs if diff]
142
143    @log()
144    def git_log_file(self, filename: str) -> list[dict[str, Any]]:
145        """Returns the commit history for a specific file.
146
147        Args:
148            filename (str): The path to the file.
149
150        Returns:
151            list[dict[str, Any]]: Structured `git log` response
152        """
153        result = self._git(f"log --pretty=format:%H - %an, %ar : %s -- {shlex.quote(filename)}")
154        commits = [c for c in result.stdout.splitlines() if c]
155        return [{"commit": commit} for commit in commits]
156
157    @log()
158    def git_log_search(self, search_string: str) -> list[dict[str, Any]]:
159        """Returns the commit history that matches the search string.
160
161        Args:
162            search_string (str): The search string.
163
164        Returns:
165            list of dict: Structured `git log` response
166        """
167        result = self._git(f"log -S {shlex.quote(search_string)} --pretty=format:%H - %an, %ar : %s")
168        commits = [c for c in result.stdout.splitlines() if c]
169        return [{"commit": commit} for commit in commits]
170
171    @log()
172    def git_show(self) -> list[dict[str, Any]]:
173        """Shows various types of objects (commits, tags, etc.).
174
175        Returns:
176            list[dict[str, Any]]: Structured `git show` response
177        """
178        result = self._git("show --no-patch --pretty=format:%H - %an, %ar : %s")
179        show_data = [d for d in result.stdout.splitlines() if d]
180        return [{"data": data} for data in show_data]
181
182    @log()
183    def git_diff_commit(self, commit1: str, commit2: str) -> list[dict[str, Any]]:
184        """Shows changes between two commits.
185
186        Args:
187            commit1 (str): First commit
188            commit2 (str): Second commit
189
190        Returns:
191            list[dict[str, Any]]: Structured `git diff` response
192        """
193        result = self._git(f"diff {shlex.quote(commit1)} {shlex.quote(commit2)} --name-only")
194        diffs = [d for d in result.stdout.splitlines() if d]
195        return [{"file": diff} for diff in diffs]
GitTool(root_folder: str, config: Config)
24    def __init__(self, root_folder: str, config: Config) -> None:
25        """
26        Initialize the GitTool class.
27
28        Args:
29            root_folder (str): The root folder path for repo operations.
30            config (Config): The developer input that bot shouldn't set.
31        """
32        self.repo_path = root_folder
33        self.config = config
34        self.auto_cat = config.get_flag("auto_cat", True)
35        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the GitTool class.

Args: root_folder (str): The root folder path for repo operations. config (Config): The developer input that bot shouldn't set.

repo_path
config
auto_cat
utf8_errors
def is_ignored_by_gitignore(self, file_path: str, gitignore_path: str = '.gitignore') -> bool:
52    def is_ignored_by_gitignore(self, file_path: str, gitignore_path: str = ".gitignore") -> bool:
53        """
54        Check if a file is ignored by .gitignore.
55
56        Args:
57            file_path (str): The path of the file to check.
58            gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory.
59
60        Returns:
61            bool: True if the file is ignored, False otherwise.
62
63        Raises:
64            FileNotFoundError: If the .gitignore file is not found.
65        """
66        full_gitignore_path = os.path.join(self.repo_path, gitignore_path)
67
68        if not os.path.isfile(full_gitignore_path):
69            raise FileNotFoundError(f"No .gitignore file found at {full_gitignore_path}")
70
71        file_path = os.path.abspath(file_path)
72
73        with open(full_gitignore_path, encoding="utf-8", errors=self.utf8_errors) as gitignore:
74            for line in gitignore:
75                line = line.strip()
76                if not line or line.startswith("#"):
77                    continue
78                gitignore_pattern = os.path.join(os.path.dirname(gitignore_path), line)
79                if fnmatch.fnmatch(file_path, gitignore_pattern):
80                    return True
81        return False

Check if a file is ignored by .gitignore.

Args: file_path (str): The path of the file to check. gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory.

Returns: bool: True if the file is ignored, False otherwise.

Raises: FileNotFoundError: If the .gitignore file is not found.

@log()
def git_status(self) -> dict[str, typing.Any]:
 83    @log()
 84    def git_status(self) -> dict[str, Any]:
 85        """Returns the status of the repository.
 86
 87        Returns:
 88            dict[str, Any]: Structured `git status` response
 89        """
 90        result = self._git("status --porcelain")
 91        changed_files: list[str] = []
 92        untracked_files: list[str] = []
 93        for line in result.stdout.splitlines():
 94            if not line:
 95                continue
 96            # porcelain format: 2-char status code, a space, then the path
 97            path = line[3:].strip()
 98            if line.startswith("??"):
 99                untracked_files.append(path)
100            else:
101                changed_files.append(path)
102        return {"changed_files": changed_files, "untracked_files": untracked_files}

Returns the status of the repository.

Returns: dict[str, Any]: Structured git status response

@log()
def get_current_branch(self) -> str:
104    @log()
105    def get_current_branch(self) -> str:
106        """
107        Retrieves the current branch name of the repository.
108
109        Returns:
110            str: The current branch name.
111        """
112        return self._git("branch --show-current").stdout.strip()

Retrieves the current branch name of the repository.

Returns: str: The current branch name.

@log()
def get_recent_commits( self, n: int = 10, short_hash: bool = False) -> list[dict[str, typing.Any]]:
114    @log()
115    def get_recent_commits(self, n: int = 10, short_hash: bool = False) -> list[dict[str, Any]]:
116        """
117        Retrieves the most recent commit hashes from the current branch.
118
119        Args:
120            n (int, optional): The number of recent commits to retrieve. Defaults to 10.
121            short_hash (bool, optional): If True, also return short hashes. Defaults to False.
122
123        Returns:
124            list[dict[str, Any]]: One dict per commit with a 'full_hash' (and
125                                  'short_hash' when requested).
126        """
127        result = self._git(f"log --pretty=format:%H -n {int(n)}")
128        hashes = [h for h in result.stdout.splitlines() if h]
129        if short_hash:
130            return [{"short_hash": h[:7], "full_hash": h} for h in hashes]
131        return [{"full_hash": h} for h in hashes]

Retrieves the most recent commit hashes from the current branch.

Args: n (int, optional): The number of recent commits to retrieve. Defaults to 10. short_hash (bool, optional): If True, also return short hashes. Defaults to False.

Returns: list[dict[str, Any]]: One dict per commit with a 'full_hash' (and 'short_hash' when requested).

@log()
def git_diff(self) -> list[dict[str, typing.Any]]:
133    @log()
134    def git_diff(self) -> list[dict[str, Any]]:
135        """Returns the differences in the working directory.
136
137        Returns:
138            list[dict[str, Any]]: Structured `git diff` response
139        """
140        diffs = self._git("diff HEAD --name-only").stdout.splitlines()
141        return [{"file": diff} for diff in diffs if diff]

Returns the differences in the working directory.

Returns: list[dict[str, Any]]: Structured git diff response

@log()
def git_log_file(self, filename: str) -> list[dict[str, typing.Any]]:
143    @log()
144    def git_log_file(self, filename: str) -> list[dict[str, Any]]:
145        """Returns the commit history for a specific file.
146
147        Args:
148            filename (str): The path to the file.
149
150        Returns:
151            list[dict[str, Any]]: Structured `git log` response
152        """
153        result = self._git(f"log --pretty=format:%H - %an, %ar : %s -- {shlex.quote(filename)}")
154        commits = [c for c in result.stdout.splitlines() if c]
155        return [{"commit": commit} for commit in commits]

Returns the commit history for a specific file.

Args: filename (str): The path to the file.

Returns: list[dict[str, Any]]: Structured git log response

@log()
def git_show(self) -> list[dict[str, typing.Any]]:
171    @log()
172    def git_show(self) -> list[dict[str, Any]]:
173        """Shows various types of objects (commits, tags, etc.).
174
175        Returns:
176            list[dict[str, Any]]: Structured `git show` response
177        """
178        result = self._git("show --no-patch --pretty=format:%H - %an, %ar : %s")
179        show_data = [d for d in result.stdout.splitlines() if d]
180        return [{"data": data} for data in show_data]

Shows various types of objects (commits, tags, etc.).

Returns: list[dict[str, Any]]: Structured git show response

@log()
def git_diff_commit(self, commit1: str, commit2: str) -> list[dict[str, typing.Any]]:
182    @log()
183    def git_diff_commit(self, commit1: str, commit2: str) -> list[dict[str, Any]]:
184        """Shows changes between two commits.
185
186        Args:
187            commit1 (str): First commit
188            commit2 (str): Second commit
189
190        Returns:
191            list[dict[str, Any]]: Structured `git diff` response
192        """
193        result = self._git(f"diff {shlex.quote(commit1)} {shlex.quote(commit2)} --name-only")
194        diffs = [d for d in result.stdout.splitlines() if d]
195        return [{"file": diff} for diff in diffs]

Shows changes between two commits.

Args: commit1 (str): First commit commit2 (str): Second commit

Returns: list[dict[str, Any]]: Structured git diff response

class TokenCounterTool:
13class TokenCounterTool:
14    """Count the number of tokens in a string."""
15
16    def __init__(self, root_folder: str, config: Config) -> None:
17        """
18        Initialize the FindTool class.
19
20        Args:
21            root_folder (str): The root folder path for file operations.
22            config (Config): The developer input that bot shouldn't set.
23        """
24        self.root_folder = root_folder
25        self.config = config
26        model = config.get_value("token_model")
27        if not model:
28            raise ValueError("token_model must be set in the config")
29        self.token_model = model
30
31    def count_tokens(self, text: str) -> int:
32        """Count the number of tokens in a string.
33
34        Args:
35            text (str): The text to count the tokens in.
36
37        Returns:
38            int: The number of tokens.
39        """
40        if not text:
41            return 0
42        # gpt3 turbo - cl100k_base
43        # gpt2 (or r50k_base) 	Most GPT-3 models
44        # p50k_base 	Code models, text-davinci-002, text-davinci-003
45        # cl100k_base 	text-embedding-ada-002
46        # enc = tiktoken.get_encoding("cl100k_base")
47
48        encoding = tiktoken.encoding_for_model(self.token_model)
49        tokens = encoding.encode(text)
50        token_count = len(tokens)
51        return token_count

Count the number of tokens in a string.

TokenCounterTool(root_folder: str, config: Config)
16    def __init__(self, root_folder: str, config: Config) -> None:
17        """
18        Initialize the FindTool class.
19
20        Args:
21            root_folder (str): The root folder path for file operations.
22            config (Config): The developer input that bot shouldn't set.
23        """
24        self.root_folder = root_folder
25        self.config = config
26        model = config.get_value("token_model")
27        if not model:
28            raise ValueError("token_model must be set in the config")
29        self.token_model = model

Initialize the FindTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
token_model
def count_tokens(self, text: str) -> int:
31    def count_tokens(self, text: str) -> int:
32        """Count the number of tokens in a string.
33
34        Args:
35            text (str): The text to count the tokens in.
36
37        Returns:
38            int: The number of tokens.
39        """
40        if not text:
41            return 0
42        # gpt3 turbo - cl100k_base
43        # gpt2 (or r50k_base) 	Most GPT-3 models
44        # p50k_base 	Code models, text-davinci-002, text-davinci-003
45        # cl100k_base 	text-embedding-ada-002
46        # enc = tiktoken.get_encoding("cl100k_base")
47
48        encoding = tiktoken.encoding_for_model(self.token_model)
49        tokens = encoding.encode(text)
50        token_count = len(tokens)
51        return token_count

Count the number of tokens in a string.

Args: text (str): The text to count the tokens in.

Returns: int: The number of tokens.

class PatchTool:
 24class PatchTool:
 25    """Edit files by applying a unified (git) diff."""
 26
 27    def __init__(self, root_folder: str, config: Config) -> None:
 28        """
 29        Initialize the PatchTool with a root folder.
 30
 31        Args:
 32            root_folder (str): The root folder for valid patchable files.
 33            config (Config): The developer input that bot shouldn't set.
 34        """
 35        self.root_folder: str = root_folder
 36        self.config = config
 37        self.auto_cat = config.get_flag("auto_cat", True)
 38
 39    @log()
 40    def apply_git_patch(self, patch_content: str) -> str:
 41        """
 42        Apply a unified (git) diff to the files in the root folder.
 43
 44        Args:
 45            patch_content (str): The content of the unified/git diff.
 46
 47        Returns:
 48            str: A message indicating the patch applied, plus the patched file
 49                 contents when auto_cat is enabled.
 50
 51        Raises:
 52            ValueError: If the patch targets a file outside the root folder.
 53            RuntimeError: If the patch application fails.
 54        """
 55        target_files = self._extract_files_from_patch(patch_content)
 56        if not target_files:
 57            raise ValueError("No target files found in patch. Is this a valid unified diff?")
 58        for file_name in target_files:
 59            if not is_file_in_root_folder(file_name, self.root_folder):
 60                raise ValueError(f"Patch targets '{file_name}' outside the root folder, which is not allowed.")
 61
 62        # Write the patch to a temp file and let `git apply` do the work; it is
 63        # more forgiving of context offsets than an in-process strict apply.
 64        with tempfile.NamedTemporaryFile(suffix=".patch", delete=False) as tmp_patch:
 65            tmp_patch_name = tmp_patch.name
 66            tmp_patch.write(patch_content.encode("utf-8"))
 67            tmp_patch.flush()
 68
 69        cmd = ["git", "apply", tmp_patch_name, "--reject", "--verbose"]
 70        try:
 71            result = subprocess.run(cmd, capture_output=True, text=True, check=True, shell=False)  # nosec
 72            logger.info("STDOUT:\n%s", result.stdout)
 73            logger.info("STDERR:\n%s", result.stderr)
 74            if result.returncode != 0:
 75                raise RuntimeError(f"Failed to apply patch: {result.stderr}")
 76        except subprocess.CalledProcessError as cpe:
 77            raise RuntimeError(f"Failed to apply patch: {cpe.stderr or cpe.stdout}") from cpe
 78        finally:
 79            try:
 80                os.remove(tmp_patch_name)
 81            except OSError:
 82                pass
 83
 84        if self.auto_cat:
 85            existing = [f for f in sorted(target_files) if os.path.exists(f)]
 86            if existing:
 87                contents = CatTool(self.root_folder, self.config).cat_markdown(existing)
 88                return (
 89                    "Patch applied without exception, please verify the contents below are what you intended.\n\n"
 90                    f"{contents}"
 91                )
 92        return "Patch applied without exception, please verify by other means to see if it was successful."
 93
 94    def _extract_files_from_patch(self, patch_content: str) -> set[str]:
 95        """
 96        Extract target file names from the patch content.
 97
 98        Args:
 99            patch_content (str): The content of the unified/git diff.
100
101        Returns:
102            set[str]: The set of file names the patch touches.
103        """
104        file_names = set()
105        for line in patch_content.split("\n"):
106            if line.startswith("diff --git "):
107                # `diff --git a/path b/path`
108                for token in line.split()[2:]:
109                    file_names.add(self._strip_ab_prefix(token))
110            elif line.startswith("--- ") or line.startswith("+++ "):
111                parts = line.split()
112                if len(parts) > 1 and parts[1] != "/dev/null":
113                    file_names.add(self._strip_ab_prefix(parts[1]))
114        return file_names
115
116    @staticmethod
117    def _strip_ab_prefix(file_name: str) -> str:
118        """Strip a leading ``a/`` or ``b/`` from a diff path."""
119        if file_name.startswith(("a/", "b/")):
120            return file_name[2:]
121        return file_name

Edit files by applying a unified (git) diff.

PatchTool(root_folder: str, config: Config)
27    def __init__(self, root_folder: str, config: Config) -> None:
28        """
29        Initialize the PatchTool with a root folder.
30
31        Args:
32            root_folder (str): The root folder for valid patchable files.
33            config (Config): The developer input that bot shouldn't set.
34        """
35        self.root_folder: str = root_folder
36        self.config = config
37        self.auto_cat = config.get_flag("auto_cat", True)

Initialize the PatchTool with a root folder.

Args: root_folder (str): The root folder for valid patchable files. config (Config): The developer input that bot shouldn't set.

root_folder: str
config
auto_cat
@log()
def apply_git_patch(self, patch_content: str) -> str:
39    @log()
40    def apply_git_patch(self, patch_content: str) -> str:
41        """
42        Apply a unified (git) diff to the files in the root folder.
43
44        Args:
45            patch_content (str): The content of the unified/git diff.
46
47        Returns:
48            str: A message indicating the patch applied, plus the patched file
49                 contents when auto_cat is enabled.
50
51        Raises:
52            ValueError: If the patch targets a file outside the root folder.
53            RuntimeError: If the patch application fails.
54        """
55        target_files = self._extract_files_from_patch(patch_content)
56        if not target_files:
57            raise ValueError("No target files found in patch. Is this a valid unified diff?")
58        for file_name in target_files:
59            if not is_file_in_root_folder(file_name, self.root_folder):
60                raise ValueError(f"Patch targets '{file_name}' outside the root folder, which is not allowed.")
61
62        # Write the patch to a temp file and let `git apply` do the work; it is
63        # more forgiving of context offsets than an in-process strict apply.
64        with tempfile.NamedTemporaryFile(suffix=".patch", delete=False) as tmp_patch:
65            tmp_patch_name = tmp_patch.name
66            tmp_patch.write(patch_content.encode("utf-8"))
67            tmp_patch.flush()
68
69        cmd = ["git", "apply", tmp_patch_name, "--reject", "--verbose"]
70        try:
71            result = subprocess.run(cmd, capture_output=True, text=True, check=True, shell=False)  # nosec
72            logger.info("STDOUT:\n%s", result.stdout)
73            logger.info("STDERR:\n%s", result.stderr)
74            if result.returncode != 0:
75                raise RuntimeError(f"Failed to apply patch: {result.stderr}")
76        except subprocess.CalledProcessError as cpe:
77            raise RuntimeError(f"Failed to apply patch: {cpe.stderr or cpe.stdout}") from cpe
78        finally:
79            try:
80                os.remove(tmp_patch_name)
81            except OSError:
82                pass
83
84        if self.auto_cat:
85            existing = [f for f in sorted(target_files) if os.path.exists(f)]
86            if existing:
87                contents = CatTool(self.root_folder, self.config).cat_markdown(existing)
88                return (
89                    "Patch applied without exception, please verify the contents below are what you intended.\n\n"
90                    f"{contents}"
91                )
92        return "Patch applied without exception, please verify by other means to see if it was successful."

Apply a unified (git) diff to the files in the root folder.

Args: patch_content (str): The content of the unified/git diff.

Returns: str: A message indicating the patch applied, plus the patched file contents when auto_cat is enabled.

Raises: ValueError: If the patch targets a file outside the root folder. RuntimeError: If the patch application fails.

class RewriteTool:
 68class RewriteTool:
 69    def __init__(self, root_folder: str, config: Config) -> None:
 70        """
 71        Initialize the RewriteTool class.
 72
 73        Args:
 74            root_folder (str): The root folder path for file operations.
 75            config (Config): The developer input that bot shouldn't set.
 76        """
 77        self.root_folder = root_folder
 78        self.config = config
 79        self.auto_cat = config.get_flag("auto_cat", True)
 80        self.python_module = config.get_value("python_module")
 81        self.only_add_text = config.get_flag("only_add_text", False)
 82
 83    @log()
 84    def write_new_file(self, file_path: str, text: str) -> str:
 85        """
 86        Write a new file at file_path within the root_folder.
 87
 88        Args:
 89            file_path (str): The relative path to the file to be written.
 90            text (str): The content to write into the file.
 91
 92        Returns:
 93            str: A success message with the file path.
 94
 95        Raises:
 96            ValueError: If the file already exists or if the file_path is outside the root_folder.
 97        """
 98        file_path = sanitize_path(file_path)
 99        # Don't prepend root folder, we will have already cd'd to it.
100        full_path = file_path
101        if not is_file_in_root_folder(full_path, self.root_folder):
102            raise ValueError("File path must be within the root folder.")
103
104        try:
105            if os.path.exists(full_path):
106                raise FileExistsError("File already exists.")
107
108            with open(full_path, "w", encoding="utf-8") as file:
109                file.write(text)
110
111            validation = self._validate_code(full_path)
112
113            if validation:
114                os.remove(full_path)
115                return f"File not written because of problems.\n{validation.message}"
116
117            return f"File written to {full_path}"
118        except FileExistsError as e:
119            tree_text = tree(Path(os.getcwd()))
120            markdown_content = f"# File {full_path} already exists. Here are all the files you can see\n\n{tree_text}"
121            raise ValueError(
122                str(e) + f" {markdown_content}\n Consider using rewrite_file method if you want to overwrite."
123            ) from e
124
125    @log()
126    def rewrite_file(self, file_path: str, text: str) -> str:
127        """
128        Backup and rewrite an existing file at file_path within the root_folder.
129        This will completely replace the contents of the file with the new text.
130
131        Args:
132            file_path (str): The relative path to the file to be rewritten.
133            text (str): The new content to write into the file.
134
135        Returns:
136            str: A success message with the file path.
137
138        Raises:
139            ValueError: If the file does not exist or if the file_path is outside the root_folder.
140        """
141        if not text:
142            raise TypeError("This would delete everything in the file. This is probably not what you want.")
143
144        file_path = sanitize_path(file_path)
145
146        # Don't prepend root folder, we will have already cd'd to it.
147        full_path = file_path
148        if not is_file_in_root_folder(full_path, self.root_folder):
149            raise ValueError("File path must be within the root folder.")
150
151        if not os.path.exists(full_path):
152            raise FileNotFoundError("File does not exist, use ls tool to see what files there are.")
153
154        _unchanged_proportion, initial, _unchanged, _added, removed = file_similarity(full_path, text.split("\n"))
155        if self.only_add_text and removed > 0:
156            raise TypeError("This would delete lines. Only add lines, do not remove them.")
157        if self.only_add_text and len(text.split("\n")) < initial:
158            raise TypeError("Line count decreased. Only add text, do not remove it.")
159        # if 5 < initial <= removed:
160        #     # concern is taking a large file, and deleting everything (ie. confusing full rewrite for an insert or edit)
161        #     raise TypeError(
162        #         "Removed lines is equal initial number of lines. "
163        #         "When rewriting files, you have to re-write the previous lines, too."
164        #     )
165        # if unchanged > 0 and initial > 0 and added == 0 and removed == 0:
166        #     raise TypeError(
167        #         "Nothing changed, nothing was added or removed. "
168        #         "When rewriting files, you have to re-write the whole file "
169        #         "with lines changed, added or removed."
170        #     )
171
172        try:
173            BackupRestore.backup_file(full_path)
174
175            with open(full_path, "w", encoding="utf-8") as file:
176                file.write(text)
177
178            validation = self._validate_code(full_path)
179
180            if validation:
181                BackupRestore.revert_to_latest_backup(full_path)
182                return f"File not rewritten because of problems.\n{validation.message}"
183
184            feedback = f"File rewritten to {full_path}"
185            if self.auto_cat:
186                feedback = "Changes made without exception, please verify by other means.\n"
187                contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
188                return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
189            return feedback + ", please view to verify contents."
190        except FileNotFoundError as e:
191            raise FileNotFoundError(
192                str(e) + " Consider using write_new_file method if you want to create a new file."
193            ) from e
194
195    def _validate_code(self, full_path: str) -> ValidationMessageForBot | None:
196        """
197        Validate python
198
199        Args:
200            full_path (str): The path to the file to validate.
201
202        Returns:
203            Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None.
204        """
205        if not is_python_file(full_path):
206            return None
207        if not self.python_module:
208            logger.warning("No python module set, skipping validation.")
209            return None
210        validator = ValidateModule(self.python_module)
211        results = validator.validate()
212        explanation = validator.explain_to_bot(results)
213        if explanation.is_valid:
214            return None
215        return explanation
RewriteTool(root_folder: str, config: Config)
69    def __init__(self, root_folder: str, config: Config) -> None:
70        """
71        Initialize the RewriteTool class.
72
73        Args:
74            root_folder (str): The root folder path for file operations.
75            config (Config): The developer input that bot shouldn't set.
76        """
77        self.root_folder = root_folder
78        self.config = config
79        self.auto_cat = config.get_flag("auto_cat", True)
80        self.python_module = config.get_value("python_module")
81        self.only_add_text = config.get_flag("only_add_text", False)

Initialize the RewriteTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
python_module
only_add_text
@log()
def write_new_file(self, file_path: str, text: str) -> str:
 83    @log()
 84    def write_new_file(self, file_path: str, text: str) -> str:
 85        """
 86        Write a new file at file_path within the root_folder.
 87
 88        Args:
 89            file_path (str): The relative path to the file to be written.
 90            text (str): The content to write into the file.
 91
 92        Returns:
 93            str: A success message with the file path.
 94
 95        Raises:
 96            ValueError: If the file already exists or if the file_path is outside the root_folder.
 97        """
 98        file_path = sanitize_path(file_path)
 99        # Don't prepend root folder, we will have already cd'd to it.
100        full_path = file_path
101        if not is_file_in_root_folder(full_path, self.root_folder):
102            raise ValueError("File path must be within the root folder.")
103
104        try:
105            if os.path.exists(full_path):
106                raise FileExistsError("File already exists.")
107
108            with open(full_path, "w", encoding="utf-8") as file:
109                file.write(text)
110
111            validation = self._validate_code(full_path)
112
113            if validation:
114                os.remove(full_path)
115                return f"File not written because of problems.\n{validation.message}"
116
117            return f"File written to {full_path}"
118        except FileExistsError as e:
119            tree_text = tree(Path(os.getcwd()))
120            markdown_content = f"# File {full_path} already exists. Here are all the files you can see\n\n{tree_text}"
121            raise ValueError(
122                str(e) + f" {markdown_content}\n Consider using rewrite_file method if you want to overwrite."
123            ) from e

Write a new file at file_path within the root_folder.

Args: file_path (str): The relative path to the file to be written. text (str): The content to write into the file.

Returns: str: A success message with the file path.

Raises: ValueError: If the file already exists or if the file_path is outside the root_folder.

@log()
def rewrite_file(self, file_path: str, text: str) -> str:
125    @log()
126    def rewrite_file(self, file_path: str, text: str) -> str:
127        """
128        Backup and rewrite an existing file at file_path within the root_folder.
129        This will completely replace the contents of the file with the new text.
130
131        Args:
132            file_path (str): The relative path to the file to be rewritten.
133            text (str): The new content to write into the file.
134
135        Returns:
136            str: A success message with the file path.
137
138        Raises:
139            ValueError: If the file does not exist or if the file_path is outside the root_folder.
140        """
141        if not text:
142            raise TypeError("This would delete everything in the file. This is probably not what you want.")
143
144        file_path = sanitize_path(file_path)
145
146        # Don't prepend root folder, we will have already cd'd to it.
147        full_path = file_path
148        if not is_file_in_root_folder(full_path, self.root_folder):
149            raise ValueError("File path must be within the root folder.")
150
151        if not os.path.exists(full_path):
152            raise FileNotFoundError("File does not exist, use ls tool to see what files there are.")
153
154        _unchanged_proportion, initial, _unchanged, _added, removed = file_similarity(full_path, text.split("\n"))
155        if self.only_add_text and removed > 0:
156            raise TypeError("This would delete lines. Only add lines, do not remove them.")
157        if self.only_add_text and len(text.split("\n")) < initial:
158            raise TypeError("Line count decreased. Only add text, do not remove it.")
159        # if 5 < initial <= removed:
160        #     # concern is taking a large file, and deleting everything (ie. confusing full rewrite for an insert or edit)
161        #     raise TypeError(
162        #         "Removed lines is equal initial number of lines. "
163        #         "When rewriting files, you have to re-write the previous lines, too."
164        #     )
165        # if unchanged > 0 and initial > 0 and added == 0 and removed == 0:
166        #     raise TypeError(
167        #         "Nothing changed, nothing was added or removed. "
168        #         "When rewriting files, you have to re-write the whole file "
169        #         "with lines changed, added or removed."
170        #     )
171
172        try:
173            BackupRestore.backup_file(full_path)
174
175            with open(full_path, "w", encoding="utf-8") as file:
176                file.write(text)
177
178            validation = self._validate_code(full_path)
179
180            if validation:
181                BackupRestore.revert_to_latest_backup(full_path)
182                return f"File not rewritten because of problems.\n{validation.message}"
183
184            feedback = f"File rewritten to {full_path}"
185            if self.auto_cat:
186                feedback = "Changes made without exception, please verify by other means.\n"
187                contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
188                return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
189            return feedback + ", please view to verify contents."
190        except FileNotFoundError as e:
191            raise FileNotFoundError(
192                str(e) + " Consider using write_new_file method if you want to create a new file."
193            ) from e

Backup and rewrite an existing file at file_path within the root_folder. This will completely replace the contents of the file with the new text.

Args: file_path (str): The relative path to the file to be rewritten. text (str): The new content to write into the file.

Returns: str: A success message with the file path.

Raises: ValueError: If the file does not exist or if the file_path is outside the root_folder.

class PyCatTool:
22class PyCatTool:
23    def __init__(self, root_folder: str, config: Config) -> None:
24        """
25        Initialize the PyCatTool with a root folder.
26
27        Args:
28            root_folder (str): The root folder path to start the file traversal from.
29            config (Config): The developer input that bot shouldn't set.
30        """
31        self.root_folder = root_folder
32        self.config = config
33        self.auto_cat = config.get_flag("auto_cat", True)
34        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
35
36    @log()
37    def format_code_as_markdown(
38        self,
39        base_path: str,
40        header: str,
41        no_docs: bool = False,  # pylint: disable=unused-argument
42        no_comments: bool = False,  # pylint: disable=unused-argument
43    ) -> str:
44        """
45        Combine all Python files in a directory into a single Markdown file.
46
47        This method traverses the directory starting from base_path, and for each Python file found,
48        its contents are formatted and appended to the Markdown file specified by output_file.
49
50        Args:
51            base_path (str): The base path of the directory to start traversing.
52            header (str): A header string to be included at the beginning of the Markdown file.
53            no_docs (bool): Whether to exclude docstrings from the output. Defaults to False.
54            no_comments (bool): Whether to exclude comments from the output. Defaults to False.
55
56        Returns:
57            str: The Markdown file contents.
58        """
59        output_file = StringIO()
60        if header == "tree":
61            tree_text = tree(Path(base_path))
62            markdown_content = f"# Source Code Filesystem Tree\n\n{tree_text}"
63            output_file.write(markdown_content)
64
65        markdown_content = f"# {header} Source Code\n\n"
66
67        for root, _dirs, files in os.walk(base_path):
68            for file in files:
69                if not is_file_in_root_folder(file, self.root_folder):
70                    continue
71                if is_python_file(file):
72                    full_path = os.path.join(root, file)
73                    relative_path = os.path.relpath(full_path, base_path)
74                    markdown_content += format_path_as_header(relative_path)
75                    markdown_content += "```python\n"
76                    with open(full_path, encoding="utf-8", errors=self.utf8_errors) as handle:
77                        text = handle.read()
78                    markdown_content += text
79                    markdown_content += "\n```\n\n"
80        output_file.write(markdown_content)
81        return output_file.getvalue()
PyCatTool(root_folder: str, config: Config)
23    def __init__(self, root_folder: str, config: Config) -> None:
24        """
25        Initialize the PyCatTool with a root folder.
26
27        Args:
28            root_folder (str): The root folder path to start the file traversal from.
29            config (Config): The developer input that bot shouldn't set.
30        """
31        self.root_folder = root_folder
32        self.config = config
33        self.auto_cat = config.get_flag("auto_cat", True)
34        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the PyCatTool with a root folder.

Args: root_folder (str): The root folder path to start the file traversal from. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
utf8_errors
@log()
def format_code_as_markdown( self, base_path: str, header: str, no_docs: bool = False, no_comments: bool = False) -> str:
36    @log()
37    def format_code_as_markdown(
38        self,
39        base_path: str,
40        header: str,
41        no_docs: bool = False,  # pylint: disable=unused-argument
42        no_comments: bool = False,  # pylint: disable=unused-argument
43    ) -> str:
44        """
45        Combine all Python files in a directory into a single Markdown file.
46
47        This method traverses the directory starting from base_path, and for each Python file found,
48        its contents are formatted and appended to the Markdown file specified by output_file.
49
50        Args:
51            base_path (str): The base path of the directory to start traversing.
52            header (str): A header string to be included at the beginning of the Markdown file.
53            no_docs (bool): Whether to exclude docstrings from the output. Defaults to False.
54            no_comments (bool): Whether to exclude comments from the output. Defaults to False.
55
56        Returns:
57            str: The Markdown file contents.
58        """
59        output_file = StringIO()
60        if header == "tree":
61            tree_text = tree(Path(base_path))
62            markdown_content = f"# Source Code Filesystem Tree\n\n{tree_text}"
63            output_file.write(markdown_content)
64
65        markdown_content = f"# {header} Source Code\n\n"
66
67        for root, _dirs, files in os.walk(base_path):
68            for file in files:
69                if not is_file_in_root_folder(file, self.root_folder):
70                    continue
71                if is_python_file(file):
72                    full_path = os.path.join(root, file)
73                    relative_path = os.path.relpath(full_path, base_path)
74                    markdown_content += format_path_as_header(relative_path)
75                    markdown_content += "```python\n"
76                    with open(full_path, encoding="utf-8", errors=self.utf8_errors) as handle:
77                        text = handle.read()
78                    markdown_content += text
79                    markdown_content += "\n```\n\n"
80        output_file.write(markdown_content)
81        return output_file.getvalue()

Combine all Python files in a directory into a single Markdown file.

This method traverses the directory starting from base_path, and for each Python file found, its contents are formatted and appended to the Markdown file specified by output_file.

Args: base_path (str): The base path of the directory to start traversing. header (str): A header string to be included at the beginning of the Markdown file. no_docs (bool): Whether to exclude docstrings from the output. Defaults to False. no_comments (bool): Whether to exclude comments from the output. Defaults to False.

Returns: str: The Markdown file contents.

class SedTool:
 17class SedTool:
 18    def __init__(self, root_folder: str, config: Config) -> None:
 19        """
 20        Initialize the SedTool class.
 21
 22        Args:
 23            root_folder (str): The root folder path for file operations.
 24            config (Config): The developer input that bot shouldn't set.
 25        """
 26        self.root_folder = root_folder
 27        self.config = config
 28        self.auto_cat = config.get_flag("auto_cat", True)
 29        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 30
 31    @log()
 32    def sed(self, file_path: str, commands: list[str]) -> str:
 33        r"""
 34        Transform the contents of a file located at file_path as per the provided sed-like commands.
 35
 36        Args:
 37            file_path (str): The path of the file to be transformed.
 38            commands (list[str]): A list of sed-like commands for text transformation.
 39
 40        Returns:
 41            str: The transformed text from the file.
 42
 43        Supported command syntax:
 44            - s/regex/replacement/flags: Regex substitution.
 45            - p: Print the current line.
 46            - a\text: Append text after the current line.
 47            - i\text: Insert text before the current line.
 48            - [number]c\text: Change the text of a specific line number.
 49            - [number]d: Delete a specific line number.
 50
 51        Note: This function reads from a file and returns the transformed text. It does not modify the file in-place.
 52        """
 53        if not is_file_in_root_folder(file_path, self.root_folder):
 54            raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.")
 55
 56        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
 57            input_text = file.read()
 58        output_text = SedTool._process_sed(input_text, commands)
 59        if is_python_file(file_path):
 60            is_valid, error = is_valid_python_source(output_text)
 61            if not is_valid and error is not None:
 62                return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}"
 63
 64        if input_text != output_text:
 65            with open(file_path, "w", encoding="utf-8") as output_file:
 66                output_file.write(output_text)
 67
 68            if self.auto_cat:
 69                feedback = "Changes without exception, please verify by other means.\n"
 70                contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
 71                return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
 72            return "Changes without exception, please verify by other means."
 73        return "No changes made."
 74
 75    @classmethod
 76    def _process_sed(cls, input_text: str, commands: list[str]) -> str:
 77        r"""
 78        Transform input_text as per the provided sed-like commands.
 79
 80        Args:
 81            input_text (str): The input text to be transformed.
 82            commands (list[str]): A list of sed-like commands for text transformation.
 83
 84        Returns:
 85            str: The transformed text.
 86
 87        Supported command syntax:
 88            - s/regex/replacement/flags: Regex substitution.
 89            - a\text: Append text after the current line.
 90            - i\text: Insert text before the current line.
 91            - [number]c\text: Change the text of a specific line number.
 92            - [number]d: Delete a specific line number.
 93
 94        Example:
 95            >>> SedTool._process_sed("Hello World\\nThis is a test", ["s/World/Universe/", "a\\Appended text"])
 96            'Hello Universe\\nThis is a test\nAppended text'
 97            >>> SedTool._process_sed("First Line\\nSecond Line", ["2d", "i\\Inserted at Start"])
 98            'Inserted at Start\nFirst Line\\nSecond Line'
 99        """
100        if isinstance(commands, str):
101            commands = [commands]
102
103        # don't know how to fix the covariant/invariant typing issue here
104        lines: list[str] = input_text.split("\n")
105
106        for i, _ in enumerate(lines):
107            for command in commands:
108                if command.startswith("s/") and re.match(r"s/.+/.*/", command):
109                    # Regex substitution: s/regex/replacement/flags
110                    parts = command[2:].rsplit("/", 2)
111                    regex, replacement, flags = parts[0], parts[1], parts[2] if len(parts) > 2 else ""
112                    count = 1 if "g" not in flags else 0  # replace all if 'g' is present
113                    lines[i] = re.sub(regex, replacement, lines[i], count=count)
114                elif command.startswith("a\\"):
115                    # Append: a\text
116                    append_text = command[2:]
117                    lines[i] += "\n" + append_text
118                elif re.match(r"\d+a\\", command):
119                    # insert after the specified line. a for after.
120                    target_line, change_text = command.split("a\\")
121                    if i + 1 == int(target_line):
122                        lines[i] = change_text
123                elif command.startswith("i\\") and i == 0:
124                    # Insert: i\text (only at the beginning of the text)
125                    insert_text = command[2:]
126                    lines[i] = insert_text + "\n" + lines[i]
127                elif re.match(r"\d+c\\", command):
128                    # Change specific line: [number]c\text
129                    target_line, change_text = command.split("c\\")
130                    if i + 1 == int(target_line):
131                        lines[i] = change_text
132                elif re.match(r"\d+d", command):
133                    # Delete specific line: [number]d
134                    delete_line = int(command[:-1])
135                    if i + 1 == delete_line:
136                        # None was a better deletion marker, but messes with mypy.
137                        lines[i] = "None  # Mark for deletion"
138                elif command == "p":
139                    # print? No action?
140                    pass
141                else:
142                    raise TypeError(
143                        "Unknown command, expected prefix of s/ or a\\ or digit + c or digit + d for replace, append, change, or delete respectively"
144                    )
145
146        # Rebuild the output from modified lines, excluding deleted ones
147        output = [line for line in lines if line is not None]
148
149        return "\n".join(output)
150
151    # # Rerun the regex substitution test with the corrected function
152    # test_regex_substitution_corrected = lambda: simulate_sed_corrected(input_text, commands) == expected_output
153    # test_regex_substitution_corrected()
SedTool(root_folder: str, config: Config)
18    def __init__(self, root_folder: str, config: Config) -> None:
19        """
20        Initialize the SedTool class.
21
22        Args:
23            root_folder (str): The root folder path for file operations.
24            config (Config): The developer input that bot shouldn't set.
25        """
26        self.root_folder = root_folder
27        self.config = config
28        self.auto_cat = config.get_flag("auto_cat", True)
29        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the SedTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
utf8_errors
@log()
def sed(self, file_path: str, commands: list[str]) -> str:
31    @log()
32    def sed(self, file_path: str, commands: list[str]) -> str:
33        r"""
34        Transform the contents of a file located at file_path as per the provided sed-like commands.
35
36        Args:
37            file_path (str): The path of the file to be transformed.
38            commands (list[str]): A list of sed-like commands for text transformation.
39
40        Returns:
41            str: The transformed text from the file.
42
43        Supported command syntax:
44            - s/regex/replacement/flags: Regex substitution.
45            - p: Print the current line.
46            - a\text: Append text after the current line.
47            - i\text: Insert text before the current line.
48            - [number]c\text: Change the text of a specific line number.
49            - [number]d: Delete a specific line number.
50
51        Note: This function reads from a file and returns the transformed text. It does not modify the file in-place.
52        """
53        if not is_file_in_root_folder(file_path, self.root_folder):
54            raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.")
55
56        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
57            input_text = file.read()
58        output_text = SedTool._process_sed(input_text, commands)
59        if is_python_file(file_path):
60            is_valid, error = is_valid_python_source(output_text)
61            if not is_valid and error is not None:
62                return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}"
63
64        if input_text != output_text:
65            with open(file_path, "w", encoding="utf-8") as output_file:
66                output_file.write(output_text)
67
68            if self.auto_cat:
69                feedback = "Changes without exception, please verify by other means.\n"
70                contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
71                return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
72            return "Changes without exception, please verify by other means."
73        return "No changes made."

Transform the contents of a file located at file_path as per the provided sed-like commands.

Args: file_path (str): The path of the file to be transformed. commands (list[str]): A list of sed-like commands for text transformation.

Returns: str: The transformed text from the file.

Supported command syntax: - s/regex/replacement/flags: Regex substitution. - p: Print the current line. - a\text: Append text after the current line. - i\text: Insert text before the current line. - [number]c\text: Change the text of a specific line number. - [number]d: Delete a specific line number.

Note: This function reads from a file and returns the transformed text. It does not modify the file in-place.

class ReplaceTool:
 20class ReplaceTool:
 21    def __init__(self, root_folder: str, config: Config) -> None:
 22        """
 23        Initialize the SedTool class.
 24
 25        Args:
 26            root_folder (str): The root folder path for file operations.
 27            config (Config): The developer input that bot shouldn't set.
 28        """
 29        self.root_folder = root_folder
 30        self.config = config
 31        self.auto_cat = config.get_flag("auto_cat", True)
 32        self.python_module = config.get_value("python_module")
 33        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 34
 35    @log()
 36    def replace_line_by_line(
 37        self, file_path: str, old_text: str, new_text: str, line_start: int = 0, line_end: int = -1
 38    ) -> str:
 39        """Replaces occurrences of a specified text with new text in a range of lines in a file.
 40
 41        Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified
 42        line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message
 43        indicating whether changes were successfully applied or not.
 44
 45        Args:
 46            file_path (str): The path to the file.
 47            old_text (str): The text to be replaced.
 48            new_text (str): The new text to replace the old text.
 49            line_start (int, optional): The starting line number (0-indexed) for the replacement.
 50                                        Defaults to 0.
 51            line_end (int, optional): The ending line number (0-indexed) for the replacement.
 52                                      If -1, it goes to the end of the file. Defaults to -1.
 53
 54        Returns:
 55            str: A message indicating the success of the operation.
 56
 57        Raises:
 58            TypeError: If file_path or old_text is None, or if no lines are left after replacement.
 59        """
 60        if not file_path:
 61            raise TypeError("No file_path, please provide file_path for each request.")
 62        if not old_text:
 63            raise TypeError("No old_text, please context so I can find the text to replace.")
 64        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
 65            input_text = file.read()
 66        lines = []
 67        input_lines = input_text.splitlines()
 68        if line_end == -1:
 69            line_end = len(input_lines)
 70        for line_no, line in enumerate(input_lines):
 71            if line_start <= line_no < line_end and old_text in line:
 72                line = line.replace(old_text, new_text)
 73            lines.append(line)
 74        if not lines:
 75            raise TypeError("Nothing left after replace, something went wrong, cancelling.")
 76        final = "\n".join(lines)
 77        return self._save_if_changed(file_path, final, input_text)
 78
 79    @log()
 80    def replace_all(self, file_path: str, old_text: str, new_text: str) -> str:
 81        """Replaces all occurrences of a specified text with new text in a file.
 82
 83        Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a
 84        message indicating whether changes were successfully applied or not.
 85
 86        Args:
 87            file_path (str): The path to the file.
 88            old_text (str): The text to be replaced.
 89            new_text (str): The new text to replace the old text.
 90
 91        Returns:
 92            str: A message indicating the success of the operation.
 93
 94        Raises:
 95            TypeError: If file_path or old_text is None.
 96        """
 97        if new_text is None:
 98            new_text = ""
 99        if not file_path:
100            raise TypeError("No file_path, please provide file_path for each request.")
101        if not old_text:
102            raise TypeError("No old_text, please context so I can find the text to replace.")
103        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
104            input_text = file.read()
105        final = input_text.replace(old_text, new_text)
106        return self._save_if_changed(file_path, final, input_text)
107
108    @log()
109    def replace_with_regex(self, file_path: str, regex_match_expression: str, replacement: str) -> str:
110        """Replaces text in a file based on a regular expression match.
111
112        Opens the file and replaces text that matches the regular expression 'regex_match_expression'
113        with the 'replacement' text. Returns a message indicating whether changes were successfully
114        applied or not.
115
116        Args:
117            file_path (str): The path to the file.
118            regex_match_expression (str): The regular expression pattern to match.
119            replacement (str): The text to replace the matched pattern.
120
121        Returns:
122            str: A message indicating the success of the operation.
123
124        Raises:
125            TypeError: If file_path or regex_match_expression is None.
126        """
127        if not file_path:
128            raise TypeError("No file_path, please provide file_path for each request.")
129        if not regex_match_expression:
130            raise TypeError("No regex_match_expression, please context so I can find the text to replace.")
131        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
132            input_text = file.read()
133        final = re.sub(regex_match_expression, replacement, input_text)
134        return self._save_if_changed(file_path, final, input_text)
135
136    def _save_if_changed(self, file_path: str, final: str, input_text: str) -> str:
137        """Saves the modified text to the file if changes have been made.
138
139        Compares the original text with the modified text and writes the modified text
140        to the file if there are changes. Returns a message indicating whether any changes
141        were made.
142
143        Args:
144            file_path (str): The path to the file.
145            final (str): The modified text.
146            input_text (str): The original text.
147
148        Returns:
149            str: A message indicating whether changes were made or not.
150
151        Raises:
152            TypeError: If file_path is None.
153        """
154        if not final:
155            raise TypeError("Something went wrong in replace and all text disappeared. Cancelling.")
156
157        if input_text != final:
158            BackupRestore.backup_file(file_path)
159            with open(file_path, "w", encoding="utf-8", errors=self.utf8_errors) as output_file:
160                output_file.write(final)
161
162            validation = self._validate_code(file_path)
163
164            if validation:
165                BackupRestore.revert_to_latest_backup(file_path)
166                return f"File not written because of problems.\n{validation.message}"
167
168            if self.auto_cat:
169                feedback = "Changes applied without exception, please verify by other means.\n"
170                contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
171                return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
172            return "Changes applied without exception, please verify by other means."
173        return (
174            "No changes made, this means the old file contents are the same as the new. This has nothing "
175            "to do with file permissions. Try again with a different match pattern."
176        )
177
178    def _validate_code(self, full_path: str) -> ValidationMessageForBot | None:
179        """
180        Validate python
181
182        Args:
183            full_path (str): The path to the file to validate.
184
185        Returns:
186            Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None.
187        """
188        if not is_python_file(full_path):
189            return None
190        if not self.python_module:
191            logger.warning("No python module set, skipping validation.")
192            return None
193        validator = ValidateModule(self.python_module)
194        results = validator.validate()
195        explanation = validator.explain_to_bot(results)
196        if explanation.is_valid:
197            return None
198        return explanation
ReplaceTool(root_folder: str, config: Config)
21    def __init__(self, root_folder: str, config: Config) -> None:
22        """
23        Initialize the SedTool class.
24
25        Args:
26            root_folder (str): The root folder path for file operations.
27            config (Config): The developer input that bot shouldn't set.
28        """
29        self.root_folder = root_folder
30        self.config = config
31        self.auto_cat = config.get_flag("auto_cat", True)
32        self.python_module = config.get_value("python_module")
33        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the SedTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
python_module
utf8_errors
@log()
def replace_line_by_line( self, file_path: str, old_text: str, new_text: str, line_start: int = 0, line_end: int = -1) -> str:
35    @log()
36    def replace_line_by_line(
37        self, file_path: str, old_text: str, new_text: str, line_start: int = 0, line_end: int = -1
38    ) -> str:
39        """Replaces occurrences of a specified text with new text in a range of lines in a file.
40
41        Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified
42        line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message
43        indicating whether changes were successfully applied or not.
44
45        Args:
46            file_path (str): The path to the file.
47            old_text (str): The text to be replaced.
48            new_text (str): The new text to replace the old text.
49            line_start (int, optional): The starting line number (0-indexed) for the replacement.
50                                        Defaults to 0.
51            line_end (int, optional): The ending line number (0-indexed) for the replacement.
52                                      If -1, it goes to the end of the file. Defaults to -1.
53
54        Returns:
55            str: A message indicating the success of the operation.
56
57        Raises:
58            TypeError: If file_path or old_text is None, or if no lines are left after replacement.
59        """
60        if not file_path:
61            raise TypeError("No file_path, please provide file_path for each request.")
62        if not old_text:
63            raise TypeError("No old_text, please context so I can find the text to replace.")
64        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
65            input_text = file.read()
66        lines = []
67        input_lines = input_text.splitlines()
68        if line_end == -1:
69            line_end = len(input_lines)
70        for line_no, line in enumerate(input_lines):
71            if line_start <= line_no < line_end and old_text in line:
72                line = line.replace(old_text, new_text)
73            lines.append(line)
74        if not lines:
75            raise TypeError("Nothing left after replace, something went wrong, cancelling.")
76        final = "\n".join(lines)
77        return self._save_if_changed(file_path, final, input_text)

Replaces occurrences of a specified text with new text in a range of lines in a file.

Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message indicating whether changes were successfully applied or not.

Args: file_path (str): The path to the file. old_text (str): The text to be replaced. new_text (str): The new text to replace the old text. line_start (int, optional): The starting line number (0-indexed) for the replacement. Defaults to 0. line_end (int, optional): The ending line number (0-indexed) for the replacement. If -1, it goes to the end of the file. Defaults to -1.

Returns: str: A message indicating the success of the operation.

Raises: TypeError: If file_path or old_text is None, or if no lines are left after replacement.

@log()
def replace_all(self, file_path: str, old_text: str, new_text: str) -> str:
 79    @log()
 80    def replace_all(self, file_path: str, old_text: str, new_text: str) -> str:
 81        """Replaces all occurrences of a specified text with new text in a file.
 82
 83        Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a
 84        message indicating whether changes were successfully applied or not.
 85
 86        Args:
 87            file_path (str): The path to the file.
 88            old_text (str): The text to be replaced.
 89            new_text (str): The new text to replace the old text.
 90
 91        Returns:
 92            str: A message indicating the success of the operation.
 93
 94        Raises:
 95            TypeError: If file_path or old_text is None.
 96        """
 97        if new_text is None:
 98            new_text = ""
 99        if not file_path:
100            raise TypeError("No file_path, please provide file_path for each request.")
101        if not old_text:
102            raise TypeError("No old_text, please context so I can find the text to replace.")
103        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
104            input_text = file.read()
105        final = input_text.replace(old_text, new_text)
106        return self._save_if_changed(file_path, final, input_text)

Replaces all occurrences of a specified text with new text in a file.

Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a message indicating whether changes were successfully applied or not.

Args: file_path (str): The path to the file. old_text (str): The text to be replaced. new_text (str): The new text to replace the old text.

Returns: str: A message indicating the success of the operation.

Raises: TypeError: If file_path or old_text is None.

@log()
def replace_with_regex( self, file_path: str, regex_match_expression: str, replacement: str) -> str:
108    @log()
109    def replace_with_regex(self, file_path: str, regex_match_expression: str, replacement: str) -> str:
110        """Replaces text in a file based on a regular expression match.
111
112        Opens the file and replaces text that matches the regular expression 'regex_match_expression'
113        with the 'replacement' text. Returns a message indicating whether changes were successfully
114        applied or not.
115
116        Args:
117            file_path (str): The path to the file.
118            regex_match_expression (str): The regular expression pattern to match.
119            replacement (str): The text to replace the matched pattern.
120
121        Returns:
122            str: A message indicating the success of the operation.
123
124        Raises:
125            TypeError: If file_path or regex_match_expression is None.
126        """
127        if not file_path:
128            raise TypeError("No file_path, please provide file_path for each request.")
129        if not regex_match_expression:
130            raise TypeError("No regex_match_expression, please context so I can find the text to replace.")
131        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
132            input_text = file.read()
133        final = re.sub(regex_match_expression, replacement, input_text)
134        return self._save_if_changed(file_path, final, input_text)

Replaces text in a file based on a regular expression match.

Opens the file and replaces text that matches the regular expression 'regex_match_expression' with the 'replacement' text. Returns a message indicating whether changes were successfully applied or not.

Args: file_path (str): The path to the file. regex_match_expression (str): The regular expression pattern to match. replacement (str): The text to replace the matched pattern.

Returns: str: A message indicating the success of the operation.

Raises: TypeError: If file_path or regex_match_expression is None.

class InsertTool:
 18class InsertTool:
 19    def __init__(self, root_folder: str, config: Config) -> None:
 20        """
 21        Initialize the InsertTool class.
 22
 23        Args:
 24            root_folder (str): The root folder path for file operations.
 25            config (Config): The developer input that bot shouldn't set.
 26        """
 27        self.root_folder = root_folder
 28        self.config = config
 29        self.auto_cat = config.get_flag("auto_cat", True)
 30        self.python_module = config.get_value("python_module")
 31        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
 32
 33    @log()
 34    def insert_text_after_context(self, file_path: str, context: str, text_to_insert: str) -> str:
 35        """Inserts a given text immediately after a specified context in a file.
 36
 37        This method opens the file, finds the line containing the specified context,
 38        and inserts the provided text immediately after this line. If the context
 39        matches multiple lines, it raises a ValueError due to ambiguity.
 40
 41        Args:
 42            file_path (str): The path of the file in which the text is to be inserted.
 43            context (str): The context string to search for in the file. The text is
 44                           inserted after the line containing this context.
 45            text_to_insert (str): The text to insert into the file.
 46
 47        Returns:
 48            str: A message for the bot with the result of the insert.
 49
 50        Raises:
 51            ValueError: If the provided context matches multiple lines in the file.
 52        """
 53        if not file_path:
 54            raise TypeError("No file_path, please provide file_path for each request.")
 55        if not context:
 56            raise TypeError("No context, please context so I can find where to insert the text.")
 57        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
 58            lines = file.readlines()
 59        original_lines = list(lines)
 60
 61        context_line_indices = [i for i, line in enumerate(lines) if context in line]
 62
 63        if len(context_line_indices) == 0:
 64            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
 65                plain_text = file.read()
 66            raise ValueError(
 67                f"No matches found, no changes made, context is not a substring of any row. "
 68                f"For reference, here is the contents of the file:\n{plain_text}"
 69            )
 70
 71        # Check for ambiguity in the context match
 72        if len(context_line_indices) > 1:
 73            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
 74                plain_text = file.read()
 75            found_at = ", ".join([str(i) for i in context_line_indices])
 76            raise ValueError(
 77                f"Ambiguous context: The provided context matches multiple lines, namely {found_at}. A context line the "
 78                "string or substring of the line just before your desired insertion point. It must "
 79                "uniquely identify a location. Either use a longer substring to match or switch to using"
 80                "the insert_text_after_multiline_context tool.\n"
 81                f"For reference, here is the contents of the file:\n{plain_text}"
 82            )
 83
 84        # Index of the line after the context line
 85        insert_index = context_line_indices[0] + 1
 86
 87        # Insert the text
 88        lines.insert(insert_index, text_to_insert + "\n")
 89
 90        return self._save_if_changed(file_path, original_lines, lines)
 91
 92    @log()
 93    def insert_text_at_start_or_end(self, file_path: str, text_to_insert: str, position: str = "end") -> str:
 94        """Inserts text at the start or end of a file.
 95
 96        Opens the file and inserts the specified text either at the beginning or the
 97        end of the file, based on the 'position' argument. If the position argument
 98        is neither 'start' nor 'end', it raises a ValueError.
 99
100        Args:
101            file_path (str): The path of the file in which the text is to be inserted.
102            text_to_insert (str): The text to insert into the file.
103            position (str, optional): The position where the text should be inserted.
104                                      Should be either 'start' or 'end'. Defaults to 'end'.
105
106        Raises:
107            ValueError: If the 'position' argument is not 'start' or 'end'.
108
109        """
110        if not file_path:
111            raise TypeError("No file_path, please provide file_path for each request.")
112        if not text_to_insert:
113            raise TypeError("No text_to_insert, please provide so I have something to insert.")
114        if position not in ("start", "end"):
115            raise ValueError("position must be start or end, so I know where to insert text.")
116        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
117            lines = file.readlines()
118        original_lines = list(lines)
119        if position == "start":
120            lines.insert(0, text_to_insert + "\n")
121        elif position == "end":
122            lines.append(text_to_insert + "\n")
123        else:
124            raise ValueError("Invalid position: choose 'start' or 'end'.")
125
126        return self._save_if_changed(file_path, original_lines, lines)
127
128    @log()
129    def insert_text_after_multiline_context(self, file_path: str, context_lines: list[str], text_to_insert: str) -> str:
130        """Inserts text immediately after a specified multiline context in a file.
131
132        Opens the file and searches for a sequence of lines (context). Once the context
133        is found, it inserts the specified text immediately after this context. If the
134        context is not found, it raises a ValueError.
135
136        Args:
137            file_path (str): The path of the file in which the text is to be inserted.
138            context_lines (list of str): A list of strings representing the multiline
139                                         context to search for in the file.
140            text_to_insert (str): The text to insert into the file after the context.
141
142        Raises:
143            ValueError: If the multiline context is not found in the file.
144
145        """
146        if not file_path:
147            raise TypeError("No file_path, please provide file_path for each request.")
148        if not context_lines:
149            raise TypeError("No context_lines, please context lines so I can find where to insert the new lines.")
150        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
151            lines = file.readlines()
152
153        try:
154            ends_with_n = lines[:-1][0].endswith("\n")
155        except IndexError:
156            ends_with_n = False
157
158        # this is going to make it hard to preserve whitespace.
159        # Convert context_lines to a string for easier matching
160        context_string = "".join([line + "\n" for line in context_lines]).rstrip("\n")
161
162        # Convert file lines to a string
163        file_string = "".join(lines)
164
165        starts_at = file_string.find(context_string)
166        if starts_at == -1:
167            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
168                plain_text = file.read()
169            raise ValueError(
170                f"No matches found, no changes made, context_lines are not found in this document. "
171                f"For reference, here is the contents of the file:\n{plain_text}"
172            )
173        # Find the index where the context ends
174        context_end_index = starts_at + len(context_string)
175
176        # Split the file_string back into lines at the context end
177        before_context = file_string[:context_end_index]
178        after_context = file_string[context_end_index:]
179
180        # Insert the new text
181        new_file_string = before_context + "\n" + text_to_insert + "\n" + after_context.strip("\n")
182
183        if ends_with_n:
184            new_file_string += "\n"
185
186        return self._save_if_changed(file_path, lines, new_file_string)
187
188    def _save_if_changed(self, file_path: str, original_lines, new_file_string: Union[str, list[str]]) -> str:
189        """
190        Save the file if it has changed.
191
192        Args:
193            file_path: The path of the file to save.
194            original_lines: The original file contents.
195            new_file_string: The new file contents.
196
197        Returns:
198            A message for the bot with the result of the save.
199        """
200        if not new_file_string:
201            raise TypeError("Something went wrong in insert and all text disappeared. Cancelling.")
202
203        if isinstance(new_file_string, str) and "\n".join(original_lines) == new_file_string:
204            return (
205                "File not changed this means the old file contents are the same as the new. This has nothing "
206                "to do with file permissions."
207            )
208        if isinstance(new_file_string, list) and original_lines == new_file_string:
209            return (
210                "File not changed, this means the old file contents are the same as the new. This has nothing "
211                "to do with file permissions."
212            )
213        # if is_python_file(file_path):
214        #     is_valid, error = is_valid_python_source(source)
215        #     if not is_valid and error:
216        #         return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}"
217        #     if not is_valid:
218        #         return f"Invalid Python source code. No changes made. {error}."
219
220        # Write back to the file
221        BackupRestore.backup_file(file_path)
222        with open(file_path, "w", encoding="utf-8", errors=self.utf8_errors) as file:
223            if isinstance(new_file_string, str):
224                file.write(new_file_string)
225            else:
226                file.writelines(new_file_string)
227
228        validation = self._validate_code(file_path)
229
230        if validation:
231            BackupRestore.revert_to_latest_backup(file_path)
232            return f"File not rewritten because of problems.\n{validation.message}"
233
234        if self.auto_cat:
235            feedback = "Insert completed and no exceptions thrown."
236            contents = CatTool(self.root_folder, self.config).cat_markdown([file_path])
237            return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}"
238        return "Insert completed and no exceptions thrown. Please verify by other means."
239
240    def _validate_code(self, full_path: str) -> ValidationMessageForBot | None:
241        """
242        Validate python
243
244        Args:
245            full_path (str): The path to the file to validate.
246
247        Returns:
248            Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None.
249        """
250        if not is_python_file(full_path):
251            return None
252        if not self.python_module:
253            logger.warning("No python module set, skipping validation.")
254            return None
255        validator = ValidateModule(self.python_module)
256        results = validator.validate()
257        explanation = validator.explain_to_bot(results)
258        if explanation.is_valid:
259            return None
260        return explanation
InsertTool(root_folder: str, config: Config)
19    def __init__(self, root_folder: str, config: Config) -> None:
20        """
21        Initialize the InsertTool class.
22
23        Args:
24            root_folder (str): The root folder path for file operations.
25            config (Config): The developer input that bot shouldn't set.
26        """
27        self.root_folder = root_folder
28        self.config = config
29        self.auto_cat = config.get_flag("auto_cat", True)
30        self.python_module = config.get_value("python_module")
31        self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")

Initialize the InsertTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
auto_cat
python_module
utf8_errors
@log()
def insert_text_after_context(self, file_path: str, context: str, text_to_insert: str) -> str:
33    @log()
34    def insert_text_after_context(self, file_path: str, context: str, text_to_insert: str) -> str:
35        """Inserts a given text immediately after a specified context in a file.
36
37        This method opens the file, finds the line containing the specified context,
38        and inserts the provided text immediately after this line. If the context
39        matches multiple lines, it raises a ValueError due to ambiguity.
40
41        Args:
42            file_path (str): The path of the file in which the text is to be inserted.
43            context (str): The context string to search for in the file. The text is
44                           inserted after the line containing this context.
45            text_to_insert (str): The text to insert into the file.
46
47        Returns:
48            str: A message for the bot with the result of the insert.
49
50        Raises:
51            ValueError: If the provided context matches multiple lines in the file.
52        """
53        if not file_path:
54            raise TypeError("No file_path, please provide file_path for each request.")
55        if not context:
56            raise TypeError("No context, please context so I can find where to insert the text.")
57        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
58            lines = file.readlines()
59        original_lines = list(lines)
60
61        context_line_indices = [i for i, line in enumerate(lines) if context in line]
62
63        if len(context_line_indices) == 0:
64            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
65                plain_text = file.read()
66            raise ValueError(
67                f"No matches found, no changes made, context is not a substring of any row. "
68                f"For reference, here is the contents of the file:\n{plain_text}"
69            )
70
71        # Check for ambiguity in the context match
72        if len(context_line_indices) > 1:
73            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
74                plain_text = file.read()
75            found_at = ", ".join([str(i) for i in context_line_indices])
76            raise ValueError(
77                f"Ambiguous context: The provided context matches multiple lines, namely {found_at}. A context line the "
78                "string or substring of the line just before your desired insertion point. It must "
79                "uniquely identify a location. Either use a longer substring to match or switch to using"
80                "the insert_text_after_multiline_context tool.\n"
81                f"For reference, here is the contents of the file:\n{plain_text}"
82            )
83
84        # Index of the line after the context line
85        insert_index = context_line_indices[0] + 1
86
87        # Insert the text
88        lines.insert(insert_index, text_to_insert + "\n")
89
90        return self._save_if_changed(file_path, original_lines, lines)

Inserts a given text immediately after a specified context in a file.

This method opens the file, finds the line containing the specified context, and inserts the provided text immediately after this line. If the context matches multiple lines, it raises a ValueError due to ambiguity.

Args: file_path (str): The path of the file in which the text is to be inserted. context (str): The context string to search for in the file. The text is inserted after the line containing this context. text_to_insert (str): The text to insert into the file.

Returns: str: A message for the bot with the result of the insert.

Raises: ValueError: If the provided context matches multiple lines in the file.

@log()
def insert_text_at_start_or_end(self, file_path: str, text_to_insert: str, position: str = 'end') -> str:
 92    @log()
 93    def insert_text_at_start_or_end(self, file_path: str, text_to_insert: str, position: str = "end") -> str:
 94        """Inserts text at the start or end of a file.
 95
 96        Opens the file and inserts the specified text either at the beginning or the
 97        end of the file, based on the 'position' argument. If the position argument
 98        is neither 'start' nor 'end', it raises a ValueError.
 99
100        Args:
101            file_path (str): The path of the file in which the text is to be inserted.
102            text_to_insert (str): The text to insert into the file.
103            position (str, optional): The position where the text should be inserted.
104                                      Should be either 'start' or 'end'. Defaults to 'end'.
105
106        Raises:
107            ValueError: If the 'position' argument is not 'start' or 'end'.
108
109        """
110        if not file_path:
111            raise TypeError("No file_path, please provide file_path for each request.")
112        if not text_to_insert:
113            raise TypeError("No text_to_insert, please provide so I have something to insert.")
114        if position not in ("start", "end"):
115            raise ValueError("position must be start or end, so I know where to insert text.")
116        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
117            lines = file.readlines()
118        original_lines = list(lines)
119        if position == "start":
120            lines.insert(0, text_to_insert + "\n")
121        elif position == "end":
122            lines.append(text_to_insert + "\n")
123        else:
124            raise ValueError("Invalid position: choose 'start' or 'end'.")
125
126        return self._save_if_changed(file_path, original_lines, lines)

Inserts text at the start or end of a file.

Opens the file and inserts the specified text either at the beginning or the end of the file, based on the 'position' argument. If the position argument is neither 'start' nor 'end', it raises a ValueError.

Args: file_path (str): The path of the file in which the text is to be inserted. text_to_insert (str): The text to insert into the file. position (str, optional): The position where the text should be inserted. Should be either 'start' or 'end'. Defaults to 'end'.

Raises: ValueError: If the 'position' argument is not 'start' or 'end'.

@log()
def insert_text_after_multiline_context( self, file_path: str, context_lines: list[str], text_to_insert: str) -> str:
128    @log()
129    def insert_text_after_multiline_context(self, file_path: str, context_lines: list[str], text_to_insert: str) -> str:
130        """Inserts text immediately after a specified multiline context in a file.
131
132        Opens the file and searches for a sequence of lines (context). Once the context
133        is found, it inserts the specified text immediately after this context. If the
134        context is not found, it raises a ValueError.
135
136        Args:
137            file_path (str): The path of the file in which the text is to be inserted.
138            context_lines (list of str): A list of strings representing the multiline
139                                         context to search for in the file.
140            text_to_insert (str): The text to insert into the file after the context.
141
142        Raises:
143            ValueError: If the multiline context is not found in the file.
144
145        """
146        if not file_path:
147            raise TypeError("No file_path, please provide file_path for each request.")
148        if not context_lines:
149            raise TypeError("No context_lines, please context lines so I can find where to insert the new lines.")
150        with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
151            lines = file.readlines()
152
153        try:
154            ends_with_n = lines[:-1][0].endswith("\n")
155        except IndexError:
156            ends_with_n = False
157
158        # this is going to make it hard to preserve whitespace.
159        # Convert context_lines to a string for easier matching
160        context_string = "".join([line + "\n" for line in context_lines]).rstrip("\n")
161
162        # Convert file lines to a string
163        file_string = "".join(lines)
164
165        starts_at = file_string.find(context_string)
166        if starts_at == -1:
167            with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file:
168                plain_text = file.read()
169            raise ValueError(
170                f"No matches found, no changes made, context_lines are not found in this document. "
171                f"For reference, here is the contents of the file:\n{plain_text}"
172            )
173        # Find the index where the context ends
174        context_end_index = starts_at + len(context_string)
175
176        # Split the file_string back into lines at the context end
177        before_context = file_string[:context_end_index]
178        after_context = file_string[context_end_index:]
179
180        # Insert the new text
181        new_file_string = before_context + "\n" + text_to_insert + "\n" + after_context.strip("\n")
182
183        if ends_with_n:
184            new_file_string += "\n"
185
186        return self._save_if_changed(file_path, lines, new_file_string)

Inserts text immediately after a specified multiline context in a file.

Opens the file and searches for a sequence of lines (context). Once the context is found, it inserts the specified text immediately after this context. If the context is not found, it raises a ValueError.

Args: file_path (str): The path of the file in which the text is to be inserted. context_lines (list of str): A list of strings representing the multiline context to search for in the file. text_to_insert (str): The text to insert into the file after the context.

Raises: ValueError: If the multiline context is not found in the file.

class TodoTool:
 17class TodoTool:
 18    """Keep track of tasks."""
 19
 20    def __init__(self, root_folder: str, config: Config) -> None:
 21        """
 22        Initialize the TodoTool with a root folder.
 23
 24        Args:
 25            root_folder (str): The root folder for valid files.
 26            config (Config): The developer input that bot shouldn't set.
 27        """
 28        self.root_folder: str = root_folder
 29        self.config = config
 30        self.roles = config.get_list("todo_roles")
 31        self.task_manager = TaskManager(self.root_folder, self.roles)
 32
 33    @log()
 34    def add_todo(
 35        self,
 36        title: str,
 37        description: str,
 38        category: str,
 39        source_code_ref: str,
 40        assignee: str | None = None,
 41        done_when: str = "",
 42    ) -> str:
 43        """
 44        Adds a new task to the task manager.
 45
 46        Args:
 47            title (str): The title of the task.
 48            description (str): A description of the task.
 49            category (str): The category of the task (e.g., 'bug', 'feature').
 50            source_code_ref (str): Reference to the source code related to the task.
 51            assignee (str | None, optional): The name of the assignee. Defaults to None.
 52            done_when (str, optional): Acceptance criteria — how to know the task is
 53                                       complete. Defaults to "".
 54
 55        Returns:
 56            str: A confirmation message indicating successful addition of the task.
 57        """
 58        self.task_manager.add_task(title, description, category, source_code_ref, assignee, done_when)
 59        summary = self.task_manager.get_stats()
 60        return f"Successful added task {title}\n{summary}"
 61
 62    @log()
 63    def remove_todo(self, title: str) -> str:
 64        """
 65        Marks a task as finished based on its title.
 66
 67        Args:
 68            title (str): The title of the task to be marked as finished.
 69
 70        Returns:
 71            str: A confirmation message indicating the task was successfully marked as finished.
 72        """
 73        self.task_manager.finish_task(title)
 74        summary = self.task_manager.get_stats()
 75        return f"Successful removed task {title}\n{summary}"
 76
 77    @log()
 78    def query_todos_by_regex(self, regex_pattern: str = r"[\s\S]+") -> str:
 79        r"""
 80        Queries tasks by a keyword in their title, using a regular expression pattern.
 81
 82        Args:
 83            regex_pattern (str, optional): The regular expression pattern to match in task titles.
 84                                           Defaults to "[\s\S]+", which matches any title.
 85
 86        Returns:
 87            str: The rendered Markdown string of tasks matching the given pattern.
 88        """
 89        return self.task_manager.query_by_title_keyword(regex_pattern)
 90
 91    @log()
 92    def query_todos_by_assignee(self, assignee_name: str) -> str:
 93        """
 94        Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'.
 95
 96        Args:
 97            assignee_name (str): The name of the assignee to query tasks for.
 98
 99        Returns:
100            str: The rendered Markdown string of tasks assigned to the specified assignee.
101        """
102        return self.task_manager.query_by_assignee(assignee_name)
103
104    @log()
105    def list_valid_assignees(
106        self,
107    ) -> list[str]:
108        """
109        Lists the valid assignees for tasks.
110
111        Returns:
112            list[str]: The rendered Markdown string of valid assignees.
113        """
114        return self.task_manager.valid_assignees

Keep track of tasks.

TodoTool(root_folder: str, config: Config)
20    def __init__(self, root_folder: str, config: Config) -> None:
21        """
22        Initialize the TodoTool with a root folder.
23
24        Args:
25            root_folder (str): The root folder for valid files.
26            config (Config): The developer input that bot shouldn't set.
27        """
28        self.root_folder: str = root_folder
29        self.config = config
30        self.roles = config.get_list("todo_roles")
31        self.task_manager = TaskManager(self.root_folder, self.roles)

Initialize the TodoTool with a root folder.

Args: root_folder (str): The root folder for valid files. config (Config): The developer input that bot shouldn't set.

root_folder: str
config
roles
task_manager
@log()
def add_todo( self, title: str, description: str, category: str, source_code_ref: str, assignee: str | None = None, done_when: str = '') -> str:
33    @log()
34    def add_todo(
35        self,
36        title: str,
37        description: str,
38        category: str,
39        source_code_ref: str,
40        assignee: str | None = None,
41        done_when: str = "",
42    ) -> str:
43        """
44        Adds a new task to the task manager.
45
46        Args:
47            title (str): The title of the task.
48            description (str): A description of the task.
49            category (str): The category of the task (e.g., 'bug', 'feature').
50            source_code_ref (str): Reference to the source code related to the task.
51            assignee (str | None, optional): The name of the assignee. Defaults to None.
52            done_when (str, optional): Acceptance criteria — how to know the task is
53                                       complete. Defaults to "".
54
55        Returns:
56            str: A confirmation message indicating successful addition of the task.
57        """
58        self.task_manager.add_task(title, description, category, source_code_ref, assignee, done_when)
59        summary = self.task_manager.get_stats()
60        return f"Successful added task {title}\n{summary}"

Adds a new task to the task manager.

Args: title (str): The title of the task. description (str): A description of the task. category (str): The category of the task (e.g., 'bug', 'feature'). source_code_ref (str): Reference to the source code related to the task. assignee (str | None, optional): The name of the assignee. Defaults to None. done_when (str, optional): Acceptance criteria — how to know the task is complete. Defaults to "".

Returns: str: A confirmation message indicating successful addition of the task.

@log()
def remove_todo(self, title: str) -> str:
62    @log()
63    def remove_todo(self, title: str) -> str:
64        """
65        Marks a task as finished based on its title.
66
67        Args:
68            title (str): The title of the task to be marked as finished.
69
70        Returns:
71            str: A confirmation message indicating the task was successfully marked as finished.
72        """
73        self.task_manager.finish_task(title)
74        summary = self.task_manager.get_stats()
75        return f"Successful removed task {title}\n{summary}"

Marks a task as finished based on its title.

Args: title (str): The title of the task to be marked as finished.

Returns: str: A confirmation message indicating the task was successfully marked as finished.

@log()
def query_todos_by_regex(self, regex_pattern: str = '[\\s\\S]+') -> str:
77    @log()
78    def query_todos_by_regex(self, regex_pattern: str = r"[\s\S]+") -> str:
79        r"""
80        Queries tasks by a keyword in their title, using a regular expression pattern.
81
82        Args:
83            regex_pattern (str, optional): The regular expression pattern to match in task titles.
84                                           Defaults to "[\s\S]+", which matches any title.
85
86        Returns:
87            str: The rendered Markdown string of tasks matching the given pattern.
88        """
89        return self.task_manager.query_by_title_keyword(regex_pattern)

Queries tasks by a keyword in their title, using a regular expression pattern.

Args: regex_pattern (str, optional): The regular expression pattern to match in task titles. Defaults to "[\s\S]+", which matches any title.

Returns: str: The rendered Markdown string of tasks matching the given pattern.

@log()
def query_todos_by_assignee(self, assignee_name: str) -> str:
 91    @log()
 92    def query_todos_by_assignee(self, assignee_name: str) -> str:
 93        """
 94        Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'.
 95
 96        Args:
 97            assignee_name (str): The name of the assignee to query tasks for.
 98
 99        Returns:
100            str: The rendered Markdown string of tasks assigned to the specified assignee.
101        """
102        return self.task_manager.query_by_assignee(assignee_name)

Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'.

Args: assignee_name (str): The name of the assignee to query tasks for.

Returns: str: The rendered Markdown string of tasks assigned to the specified assignee.

@log()
def list_valid_assignees(self) -> list[str]:
104    @log()
105    def list_valid_assignees(
106        self,
107    ) -> list[str]:
108        """
109        Lists the valid assignees for tasks.
110
111        Returns:
112            list[str]: The rendered Markdown string of valid assignees.
113        """
114        return self.task_manager.valid_assignees

Lists the valid assignees for tasks.

Returns: list[str]: The rendered Markdown string of valid assignees.

class AnswerCollectorTool:
 23class AnswerCollectorTool:
 24    def __init__(self, root_folder: str, config: Config) -> None:
 25        """
 26        Initialize the PytestTool class.
 27
 28        Args:
 29            root_folder (str): The root folder path for file operations. (Not used yet)
 30            config (Config): The developer input that bot shouldn't set.
 31        """
 32        self.root_folder = root_folder
 33        self.config = config
 34        self.comment: str | None = None
 35        self.bool_answer: bool | None = None
 36        self.json_answer: str | None = None
 37        self.xml_answer: str | None = None
 38        self.toml_answer: str | None = None
 39        self.tuple_answer: tuple | None = None
 40        self.set_answer: set | None = None
 41        self.text_answer: str | None = None
 42        self.list_answer: list[str] | None = None
 43        self.int_answer: int | None = None
 44        self.float_answer: float | None = None
 45        self.dict_answer: dict[str, Any] | None = None
 46        self.response_received = "Response received."
 47
 48    def _answered(self) -> None:
 49        """Check if this tool has been used.
 50
 51        Raises:
 52            TypeError: If the tool has been used. Recreate a new one after each usage.
 53        """
 54        if any(
 55            [
 56                self.comment,
 57                self.bool_answer is not None,
 58                self.json_answer,
 59                self.xml_answer,
 60                self.toml_answer,
 61                self.tuple_answer,
 62                self.set_answer,
 63                self.text_answer,
 64                self.list_answer,
 65                self.int_answer,
 66                self.float_answer,
 67                self.dict_answer,
 68            ]
 69        ):
 70            raise TypeError("This Answer tool has been used. Please create a new one for another answer.")
 71
 72    @log()
 73    def report_list(self, answer: list[str], comment: str = "") -> str:
 74        """Report answer in list format.
 75
 76        Args:
 77            answer (list[str]): The answer to be reported in list format.
 78            comment (str, optional): Any comments, supplemental info about the answer.
 79
 80        Returns:
 81            str: A string indicating that the response has been received.
 82        """
 83        self._answered()
 84        self.list_answer = answer
 85        self.comment = comment
 86        return self.response_received
 87
 88    @log()
 89    def report_int(self, answer: int, comment: str = "") -> str:
 90        """Report answer in integer format
 91        Args:
 92            answer (int): The answer to be reported in integer format.
 93            comment (str, optional): Any comments, supplemental info about the answer.
 94
 95
 96        Returns:
 97            str: A string indicating that the response has been received.
 98        """
 99        self._answered()
100        self.int_answer = answer
101        self.comment = comment
102        return self.response_received
103
104    @log()
105    def report_float(self, answer: float, comment: str = "") -> str:
106        """Report answer in string format.
107
108        Args:
109            answer (float): The answer to be reported in float format.
110            comment (str, optional): Any comments, supplemental info about the answer.
111
112        Returns:
113            str: A string indicating that the response has been received.
114        """
115        self._answered()
116        self.float_answer = answer
117        self.comment = comment
118        return self.response_received
119
120    @log()
121    def report_dict(self, answer: dict[str, Any], comment: str = "") -> str:
122        """Report answer in dict format.
123
124        Args:
125            answer (dict[str, Any]): The answer to be reported in dict format.
126            comment (str, optional): Any comments, supplemental info about the answer.
127
128        Returns:
129            str: A string indicating that the response has been received.
130        """
131        self._answered()
132        self.dict_answer = answer
133        self.comment = comment
134        return self.response_received
135
136    @log()
137    def report_text(self, answer: str, comment: str = "") -> str:
138        """Report answer in string format.
139
140        Args:
141            answer (str): The answer to be reported in string format.
142            comment (str, optional): Any comments, supplemental info about the answer.
143
144        Returns:
145            str: A string indicating that the response has been received.
146        """
147        self._answered()
148        self.text_answer = answer
149        self.comment = comment
150        return self.response_received
151
152    @log()
153    def report_bool(self, answer: bool, comment: str = "") -> str:
154        """Report answer in bool format.
155
156        Args:
157            answer (bool): The answer to be reported in bool format.
158            comment (str, optional): Any comments, supplemental info about the answer.
159
160        Returns:
161            str: A string indicating that the response has been received.
162        """
163        self._answered()
164        self.bool_answer = answer
165        self.comment = comment
166        return self.response_received
167
168    @log()
169    def report_tuple(self, answer: tuple, comment: str = "") -> str:
170        """Report answer in tuple format.
171
172        Args:
173            answer (tuple): The answer to be reported in tuple format.
174            comment (str, optional): Any comments, supplemental info about the answer.
175
176        Returns:
177            str: A string indicating that the response has been received.
178        """
179        self._answered()
180        self.tuple_answer = answer
181        self.comment = comment
182        return self.response_received
183
184    @log()
185    def report_set(self, answer: set, comment: str = "") -> str:
186        """Report answer in set format.
187
188        Args:
189            answer (set): The answer to be reported in set format.
190            comment (str, optional): Any comments, supplemental info about the answer.
191
192        Returns:
193            str: A string indicating that the response has been received.
194        """
195        self._answered()
196        self.set_answer = answer
197        self.comment = comment
198        return self.response_received
199
200    @log()
201    def report_json(self, answer: str, comment: str = "") -> str:
202        """Report answer in json format.
203
204        Args:
205            answer (str): The answer to be reported in json format.
206            comment (str, optional): Any comments, supplemental info about the answer.
207
208        Returns:
209            str: A string indicating that the response has been received.
210        """
211        self._answered()
212        self.json_answer = answer
213        self.comment = comment
214        return self.response_received
215
216    @log()
217    def report_xml(self, answer: str, comment: str = "") -> str:
218        """Report answer in xml format.
219
220        Args:
221            answer (str): The answer to be reported in xml format.
222            comment (str, optional): Any comments, supplemental info about the answer.
223
224        Returns:
225            str: A string indicating that the response has been received.
226        """
227        self._answered()
228        self.xml_answer = answer
229        self.comment = comment
230        return self.response_received
231
232    @log()
233    def report_toml(self, answer: str, comment: str = "") -> str:
234        """Report answer in toml format.
235
236        Args:
237            answer (str): The answer to be reported in toml format.
238            comment (str, optional): Any comments, supplemental info about the answer.
239
240        Returns:
241            str: A string indicating that the response has been received.
242        """
243        self._answered()
244        self.toml_answer = answer
245        self.comment = comment
246        return self.response_received
AnswerCollectorTool(root_folder: str, config: Config)
24    def __init__(self, root_folder: str, config: Config) -> None:
25        """
26        Initialize the PytestTool class.
27
28        Args:
29            root_folder (str): The root folder path for file operations. (Not used yet)
30            config (Config): The developer input that bot shouldn't set.
31        """
32        self.root_folder = root_folder
33        self.config = config
34        self.comment: str | None = None
35        self.bool_answer: bool | None = None
36        self.json_answer: str | None = None
37        self.xml_answer: str | None = None
38        self.toml_answer: str | None = None
39        self.tuple_answer: tuple | None = None
40        self.set_answer: set | None = None
41        self.text_answer: str | None = None
42        self.list_answer: list[str] | None = None
43        self.int_answer: int | None = None
44        self.float_answer: float | None = None
45        self.dict_answer: dict[str, Any] | None = None
46        self.response_received = "Response received."

Initialize the PytestTool class.

Args: root_folder (str): The root folder path for file operations. (Not used yet) config (Config): The developer input that bot shouldn't set.

root_folder
config
comment: str | None
bool_answer: bool | None
json_answer: str | None
xml_answer: str | None
toml_answer: str | None
tuple_answer: tuple | None
set_answer: set | None
text_answer: str | None
list_answer: list[str] | None
int_answer: int | None
float_answer: float | None
dict_answer: dict[str, typing.Any] | None
response_received
@log()
def report_list(self, answer: list[str], comment: str = '') -> str:
72    @log()
73    def report_list(self, answer: list[str], comment: str = "") -> str:
74        """Report answer in list format.
75
76        Args:
77            answer (list[str]): The answer to be reported in list format.
78            comment (str, optional): Any comments, supplemental info about the answer.
79
80        Returns:
81            str: A string indicating that the response has been received.
82        """
83        self._answered()
84        self.list_answer = answer
85        self.comment = comment
86        return self.response_received

Report answer in list format.

Args: answer (list[str]): The answer to be reported in list format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_int(self, answer: int, comment: str = '') -> str:
 88    @log()
 89    def report_int(self, answer: int, comment: str = "") -> str:
 90        """Report answer in integer format
 91        Args:
 92            answer (int): The answer to be reported in integer format.
 93            comment (str, optional): Any comments, supplemental info about the answer.
 94
 95
 96        Returns:
 97            str: A string indicating that the response has been received.
 98        """
 99        self._answered()
100        self.int_answer = answer
101        self.comment = comment
102        return self.response_received

Report answer in integer format Args: answer (int): The answer to be reported in integer format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_float(self, answer: float, comment: str = '') -> str:
104    @log()
105    def report_float(self, answer: float, comment: str = "") -> str:
106        """Report answer in string format.
107
108        Args:
109            answer (float): The answer to be reported in float format.
110            comment (str, optional): Any comments, supplemental info about the answer.
111
112        Returns:
113            str: A string indicating that the response has been received.
114        """
115        self._answered()
116        self.float_answer = answer
117        self.comment = comment
118        return self.response_received

Report answer in string format.

Args: answer (float): The answer to be reported in float format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_dict(self, answer: dict[str, typing.Any], comment: str = '') -> str:
120    @log()
121    def report_dict(self, answer: dict[str, Any], comment: str = "") -> str:
122        """Report answer in dict format.
123
124        Args:
125            answer (dict[str, Any]): The answer to be reported in dict format.
126            comment (str, optional): Any comments, supplemental info about the answer.
127
128        Returns:
129            str: A string indicating that the response has been received.
130        """
131        self._answered()
132        self.dict_answer = answer
133        self.comment = comment
134        return self.response_received

Report answer in dict format.

Args: answer (dict[str, Any]): The answer to be reported in dict format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_text(self, answer: str, comment: str = '') -> str:
136    @log()
137    def report_text(self, answer: str, comment: str = "") -> str:
138        """Report answer in string format.
139
140        Args:
141            answer (str): The answer to be reported in string format.
142            comment (str, optional): Any comments, supplemental info about the answer.
143
144        Returns:
145            str: A string indicating that the response has been received.
146        """
147        self._answered()
148        self.text_answer = answer
149        self.comment = comment
150        return self.response_received

Report answer in string format.

Args: answer (str): The answer to be reported in string format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_bool(self, answer: bool, comment: str = '') -> str:
152    @log()
153    def report_bool(self, answer: bool, comment: str = "") -> str:
154        """Report answer in bool format.
155
156        Args:
157            answer (bool): The answer to be reported in bool format.
158            comment (str, optional): Any comments, supplemental info about the answer.
159
160        Returns:
161            str: A string indicating that the response has been received.
162        """
163        self._answered()
164        self.bool_answer = answer
165        self.comment = comment
166        return self.response_received

Report answer in bool format.

Args: answer (bool): The answer to be reported in bool format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_tuple(self, answer: tuple, comment: str = '') -> str:
168    @log()
169    def report_tuple(self, answer: tuple, comment: str = "") -> str:
170        """Report answer in tuple format.
171
172        Args:
173            answer (tuple): The answer to be reported in tuple format.
174            comment (str, optional): Any comments, supplemental info about the answer.
175
176        Returns:
177            str: A string indicating that the response has been received.
178        """
179        self._answered()
180        self.tuple_answer = answer
181        self.comment = comment
182        return self.response_received

Report answer in tuple format.

Args: answer (tuple): The answer to be reported in tuple format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_set(self, answer: set, comment: str = '') -> str:
184    @log()
185    def report_set(self, answer: set, comment: str = "") -> str:
186        """Report answer in set format.
187
188        Args:
189            answer (set): The answer to be reported in set format.
190            comment (str, optional): Any comments, supplemental info about the answer.
191
192        Returns:
193            str: A string indicating that the response has been received.
194        """
195        self._answered()
196        self.set_answer = answer
197        self.comment = comment
198        return self.response_received

Report answer in set format.

Args: answer (set): The answer to be reported in set format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_json(self, answer: str, comment: str = '') -> str:
200    @log()
201    def report_json(self, answer: str, comment: str = "") -> str:
202        """Report answer in json format.
203
204        Args:
205            answer (str): The answer to be reported in json format.
206            comment (str, optional): Any comments, supplemental info about the answer.
207
208        Returns:
209            str: A string indicating that the response has been received.
210        """
211        self._answered()
212        self.json_answer = answer
213        self.comment = comment
214        return self.response_received

Report answer in json format.

Args: answer (str): The answer to be reported in json format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_xml(self, answer: str, comment: str = '') -> str:
216    @log()
217    def report_xml(self, answer: str, comment: str = "") -> str:
218        """Report answer in xml format.
219
220        Args:
221            answer (str): The answer to be reported in xml format.
222            comment (str, optional): Any comments, supplemental info about the answer.
223
224        Returns:
225            str: A string indicating that the response has been received.
226        """
227        self._answered()
228        self.xml_answer = answer
229        self.comment = comment
230        return self.response_received

Report answer in xml format.

Args: answer (str): The answer to be reported in xml format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

@log()
def report_toml(self, answer: str, comment: str = '') -> str:
232    @log()
233    def report_toml(self, answer: str, comment: str = "") -> str:
234        """Report answer in toml format.
235
236        Args:
237            answer (str): The answer to be reported in toml format.
238            comment (str, optional): Any comments, supplemental info about the answer.
239
240        Returns:
241            str: A string indicating that the response has been received.
242        """
243        self._answered()
244        self.toml_answer = answer
245        self.comment = comment
246        return self.response_received

Report answer in toml format.

Args: answer (str): The answer to be reported in toml format. comment (str, optional): Any comments, supplemental info about the answer.

Returns: str: A string indicating that the response has been received.

class PytestTool:
12class PytestTool:
13    """Optimized for AI version of pytest."""
14
15    def __init__(self, root_folder: str, config: Config) -> None:
16        """
17        Initialize the PytestTool class.
18
19        Args:
20            root_folder (str): The root folder path for file operations.
21            config (Config): The developer input that bot shouldn't set.
22        """
23        self.root_folder = root_folder
24
25        self.config = config
26        self.module = config.get_value("pytest_module")
27        self.tests_folder = config.get_value("pytest_folder")
28
29        self.min_coverage = float(config.get_value("pytest_min_coverage") or 0.0)
30
31    @log()
32    def pytest(
33        self,
34    ) -> str:
35        """
36        Runs pytest on tests in tests folder..
37
38        Returns:
39            str: Output from pytest.
40        """
41        # Host script must set env vars, temp folder location and pwd!
42        # with change_directory(self.root_folder):
43        # What is -rA
44        if not self.module or not self.tests_folder or self.min_coverage:
45            raise FatalConfigurationError("Please set in ai_config module, test_folder and min_coverage")
46        _passed_tests, _failed_tests, _coverage, command_result = count_pytest_results(
47            self.module, self.tests_folder, self.min_coverage
48        )
49        markdown_output = f"""## Pytest Output
50### Standard Output
51{command_result.stdout}
52### Standard Error
53{command_result.stderr}
54### Return Code
55`{command_result.return_code}`"""
56        return markdown_output

Optimized for AI version of pytest.

PytestTool(root_folder: str, config: Config)
15    def __init__(self, root_folder: str, config: Config) -> None:
16        """
17        Initialize the PytestTool class.
18
19        Args:
20            root_folder (str): The root folder path for file operations.
21            config (Config): The developer input that bot shouldn't set.
22        """
23        self.root_folder = root_folder
24
25        self.config = config
26        self.module = config.get_value("pytest_module")
27        self.tests_folder = config.get_value("pytest_folder")
28
29        self.min_coverage = float(config.get_value("pytest_min_coverage") or 0.0)

Initialize the PytestTool class.

Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.

root_folder
config
module
tests_folder
min_coverage
@log()
def pytest(self) -> str:
31    @log()
32    def pytest(
33        self,
34    ) -> str:
35        """
36        Runs pytest on tests in tests folder..
37
38        Returns:
39            str: Output from pytest.
40        """
41        # Host script must set env vars, temp folder location and pwd!
42        # with change_directory(self.root_folder):
43        # What is -rA
44        if not self.module or not self.tests_folder or self.min_coverage:
45            raise FatalConfigurationError("Please set in ai_config module, test_folder and min_coverage")
46        _passed_tests, _failed_tests, _coverage, command_result = count_pytest_results(
47            self.module, self.tests_folder, self.min_coverage
48        )
49        markdown_output = f"""## Pytest Output
50### Standard Output
51{command_result.stdout}
52### Standard Error
53{command_result.stderr}
54### Return Code
55`{command_result.return_code}`"""
56        return markdown_output

Runs pytest on tests in tests folder..

Returns: str: Output from pytest.

class ToolKit(ai_shell.toolkit_base.ToolKitBase):
  32class ToolKit(ToolKitBase):
  33    """AI Shell Toolkit"""
  34
  35    def __init__(
  36        self, root_folder: str, token_model: str, global_max_lines: int, permitted_tools: list[str], config: Config
  37    ) -> None:
  38        super().__init__(root_folder, token_model, global_max_lines, permitted_tools, config)
  39        self._lookup: dict[str, Callable[[dict[str, Any]], Any]] = {
  40            "report_bool": self.report_bool,
  41            "report_dict": self.report_dict,
  42            "report_float": self.report_float,
  43            "report_int": self.report_int,
  44            "report_json": self.report_json,
  45            "report_list": self.report_list,
  46            "report_set": self.report_set,
  47            "report_text": self.report_text,
  48            "report_toml": self.report_toml,
  49            "report_tuple": self.report_tuple,
  50            "report_xml": self.report_xml,
  51            "cat": self.cat,
  52            "cat_markdown": self.cat_markdown,
  53            "cut_characters": self.cut_characters,
  54            "cut_fields": self.cut_fields,
  55            "cut_fields_by_name": self.cut_fields_by_name,
  56            "find_files": self.find_files,
  57            "find_files_markdown": self.find_files_markdown,
  58            "get_current_branch": self.get_current_branch,
  59            "get_recent_commits": self.get_recent_commits,
  60            "git_diff": self.git_diff,
  61            "git_diff_commit": self.git_diff_commit,
  62            "git_log_file": self.git_log_file,
  63            "git_log_search": self.git_log_search,
  64            "git_show": self.git_show,
  65            "git_status": self.git_status,
  66            "is_ignored_by_gitignore": self.is_ignored_by_gitignore,
  67            "grep": self.grep,
  68            "grep_markdown": self.grep_markdown,
  69            "head": self.head,
  70            "head_markdown": self.head_markdown,
  71            "head_tail": self.head_tail,
  72            "tail": self.tail,
  73            "tail_markdown": self.tail_markdown,
  74            "insert_text_after_context": self.insert_text_after_context,
  75            "insert_text_after_multiline_context": self.insert_text_after_multiline_context,
  76            "insert_text_at_start_or_end": self.insert_text_at_start_or_end,
  77            "ls": self.ls,
  78            "ls_markdown": self.ls_markdown,
  79            "apply_git_patch": self.apply_git_patch,
  80            "format_code_as_markdown": self.format_code_as_markdown,
  81            "pytest": self.pytest,
  82            "replace_all": self.replace_all,
  83            "replace_line_by_line": self.replace_line_by_line,
  84            "replace_with_regex": self.replace_with_regex,
  85            "rewrite_file": self.rewrite_file,
  86            "write_new_file": self.write_new_file,
  87            "sed": self.sed,
  88            "add_todo": self.add_todo,
  89            "list_valid_assignees": self.list_valid_assignees,
  90            "query_todos_by_assignee": self.query_todos_by_assignee,
  91            "query_todos_by_regex": self.query_todos_by_regex,
  92            "remove_todo": self.remove_todo,
  93            "count_tokens": self.count_tokens,
  94        }
  95        # Stateful tool support. Useless assignment to make mypy happy
  96        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
  97
  98    def report_bool(self, arguments: dict[str, Any]) -> Any:
  99        """Generated Do Not Edit
 100
 101        Args:
 102            arguments (dict[str, Any]): The arguments for the tool.
 103
 104        Returns:
 105            Any: The result of the tool invocation.
 106        """
 107        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 108
 109        answer = cast(
 110            bool,
 111            arguments.get(
 112                "answer",
 113            ),
 114        )
 115        comment = cast(str, arguments.get("comment", ""))
 116        return self.tool_answer_collector.report_bool(answer=answer, comment=comment)
 117
 118    def report_dict(self, arguments: dict[str, Any]) -> Any:
 119        """Generated Do Not Edit
 120
 121        Args:
 122            arguments (dict[str, Any]): The arguments for the tool.
 123
 124        Returns:
 125            Any: The result of the tool invocation.
 126        """
 127        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 128
 129        answer = cast(
 130            Any,
 131            arguments.get(
 132                "answer",
 133            ),
 134        )
 135        comment = cast(str, arguments.get("comment", ""))
 136        return self.tool_answer_collector.report_dict(answer=answer, comment=comment)
 137
 138    def report_float(self, arguments: dict[str, Any]) -> Any:
 139        """Generated Do Not Edit
 140
 141        Args:
 142            arguments (dict[str, Any]): The arguments for the tool.
 143
 144        Returns:
 145            Any: The result of the tool invocation.
 146        """
 147        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 148
 149        answer = cast(
 150            float,
 151            arguments.get(
 152                "answer",
 153            ),
 154        )
 155        comment = cast(str, arguments.get("comment", ""))
 156        return self.tool_answer_collector.report_float(answer=answer, comment=comment)
 157
 158    def report_int(self, arguments: dict[str, Any]) -> Any:
 159        """Generated Do Not Edit
 160
 161        Args:
 162            arguments (dict[str, Any]): The arguments for the tool.
 163
 164        Returns:
 165            Any: The result of the tool invocation.
 166        """
 167        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 168
 169        answer = cast(
 170            int,
 171            arguments.get(
 172                "answer",
 173            ),
 174        )
 175        comment = cast(str, arguments.get("comment", ""))
 176        return self.tool_answer_collector.report_int(answer=answer, comment=comment)
 177
 178    def report_json(self, arguments: dict[str, Any]) -> Any:
 179        """Generated Do Not Edit
 180
 181        Args:
 182            arguments (dict[str, Any]): The arguments for the tool.
 183
 184        Returns:
 185            Any: The result of the tool invocation.
 186        """
 187        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 188
 189        answer = cast(
 190            str,
 191            arguments.get(
 192                "answer",
 193            ),
 194        )
 195        comment = cast(str, arguments.get("comment", ""))
 196        return self.tool_answer_collector.report_json(answer=answer, comment=comment)
 197
 198    def report_list(self, arguments: dict[str, Any]) -> Any:
 199        """Generated Do Not Edit
 200
 201        Args:
 202            arguments (dict[str, Any]): The arguments for the tool.
 203
 204        Returns:
 205            Any: The result of the tool invocation.
 206        """
 207        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 208
 209        answer = cast(
 210            str,
 211            arguments.get(
 212                "answer",
 213            ),
 214        )
 215        comment = cast(str, arguments.get("comment", ""))
 216        return self.tool_answer_collector.report_list(answer=answer, comment=comment)
 217
 218    def report_set(self, arguments: dict[str, Any]) -> Any:
 219        """Generated Do Not Edit
 220
 221        Args:
 222            arguments (dict[str, Any]): The arguments for the tool.
 223
 224        Returns:
 225            Any: The result of the tool invocation.
 226        """
 227        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 228
 229        answer = cast(
 230            list[Any],
 231            arguments.get(
 232                "answer",
 233            ),
 234        )
 235        comment = cast(str, arguments.get("comment", ""))
 236        return self.tool_answer_collector.report_set(answer=answer, comment=comment)
 237
 238    def report_text(self, arguments: dict[str, Any]) -> Any:
 239        """Generated Do Not Edit
 240
 241        Args:
 242            arguments (dict[str, Any]): The arguments for the tool.
 243
 244        Returns:
 245            Any: The result of the tool invocation.
 246        """
 247        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 248
 249        answer = cast(
 250            str,
 251            arguments.get(
 252                "answer",
 253            ),
 254        )
 255        comment = cast(str, arguments.get("comment", ""))
 256        return self.tool_answer_collector.report_text(answer=answer, comment=comment)
 257
 258    def report_toml(self, arguments: dict[str, Any]) -> Any:
 259        """Generated Do Not Edit
 260
 261        Args:
 262            arguments (dict[str, Any]): The arguments for the tool.
 263
 264        Returns:
 265            Any: The result of the tool invocation.
 266        """
 267        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 268
 269        answer = cast(
 270            str,
 271            arguments.get(
 272                "answer",
 273            ),
 274        )
 275        comment = cast(str, arguments.get("comment", ""))
 276        return self.tool_answer_collector.report_toml(answer=answer, comment=comment)
 277
 278    def report_tuple(self, arguments: dict[str, Any]) -> Any:
 279        """Generated Do Not Edit
 280
 281        Args:
 282            arguments (dict[str, Any]): The arguments for the tool.
 283
 284        Returns:
 285            Any: The result of the tool invocation.
 286        """
 287        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 288
 289        answer = cast(
 290            list[Any],
 291            arguments.get(
 292                "answer",
 293            ),
 294        )
 295        comment = cast(str, arguments.get("comment", ""))
 296        return self.tool_answer_collector.report_tuple(answer=answer, comment=comment)
 297
 298    def report_xml(self, arguments: dict[str, Any]) -> Any:
 299        """Generated Do Not Edit
 300
 301        Args:
 302            arguments (dict[str, Any]): The arguments for the tool.
 303
 304        Returns:
 305            Any: The result of the tool invocation.
 306        """
 307        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
 308
 309        answer = cast(
 310            str,
 311            arguments.get(
 312                "answer",
 313            ),
 314        )
 315        comment = cast(str, arguments.get("comment", ""))
 316        return self.tool_answer_collector.report_xml(answer=answer, comment=comment)
 317
 318    def cat(self, arguments: dict[str, Any]) -> Any:
 319        """Generated Do Not Edit
 320
 321        Args:
 322            arguments (dict[str, Any]): The arguments for the tool.
 323
 324        Returns:
 325            Any: The result of the tool invocation.
 326        """
 327        tool = CatTool(self.root_folder, self.config)
 328
 329        file_paths = cast(
 330            str,
 331            arguments.get(
 332                "file_paths",
 333            ),
 334        )
 335        number_lines = cast(bool, arguments.get("number_lines", True))
 336        squeeze_blank = cast(bool, arguments.get("squeeze_blank", False))
 337        return tool.cat(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)
 338
 339    def cat_markdown(self, arguments: dict[str, Any]) -> Any:
 340        """Generated Do Not Edit
 341
 342        Args:
 343            arguments (dict[str, Any]): The arguments for the tool.
 344
 345        Returns:
 346            Any: The result of the tool invocation.
 347        """
 348        tool = CatTool(self.root_folder, self.config)
 349
 350        file_paths = cast(
 351            str,
 352            arguments.get(
 353                "file_paths",
 354            ),
 355        )
 356        number_lines = cast(bool, arguments.get("number_lines", True))
 357        squeeze_blank = cast(bool, arguments.get("squeeze_blank", False))
 358        return tool.cat_markdown(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)
 359
 360    def cut_characters(self, arguments: dict[str, Any]) -> Any:
 361        """Generated Do Not Edit
 362
 363        Args:
 364            arguments (dict[str, Any]): The arguments for the tool.
 365
 366        Returns:
 367            Any: The result of the tool invocation.
 368        """
 369        tool = CutTool(self.root_folder, self.config)
 370
 371        character_ranges = cast(
 372            str,
 373            arguments.get(
 374                "character_ranges",
 375            ),
 376        )
 377        file_path = cast(
 378            str,
 379            arguments.get(
 380                "file_path",
 381            ),
 382        )
 383        return tool.cut_characters(character_ranges=character_ranges, file_path=file_path)
 384
 385    def cut_fields(self, arguments: dict[str, Any]) -> Any:
 386        """Generated Do Not Edit
 387
 388        Args:
 389            arguments (dict[str, Any]): The arguments for the tool.
 390
 391        Returns:
 392            Any: The result of the tool invocation.
 393        """
 394        tool = CutTool(self.root_folder, self.config)
 395
 396        delimiter = cast(str, arguments.get("delimiter", ","))
 397        field_ranges = cast(
 398            str,
 399            arguments.get(
 400                "field_ranges",
 401            ),
 402        )
 403        filename = cast(
 404            str,
 405            arguments.get(
 406                "filename",
 407            ),
 408        )
 409        return tool.cut_fields(delimiter=delimiter, field_ranges=field_ranges, filename=filename)
 410
 411    def cut_fields_by_name(self, arguments: dict[str, Any]) -> Any:
 412        """Generated Do Not Edit
 413
 414        Args:
 415            arguments (dict[str, Any]): The arguments for the tool.
 416
 417        Returns:
 418            Any: The result of the tool invocation.
 419        """
 420        tool = CutTool(self.root_folder, self.config)
 421
 422        delimiter = cast(str, arguments.get("delimiter", ","))
 423        field_names = cast(
 424            str,
 425            arguments.get(
 426                "field_names",
 427            ),
 428        )
 429        filename = cast(
 430            str,
 431            arguments.get(
 432                "filename",
 433            ),
 434        )
 435        return tool.cut_fields_by_name(delimiter=delimiter, field_names=field_names, filename=filename)
 436
 437    def find_files(self, arguments: dict[str, Any]) -> Any:
 438        """Generated Do Not Edit
 439
 440        Args:
 441            arguments (dict[str, Any]): The arguments for the tool.
 442
 443        Returns:
 444            Any: The result of the tool invocation.
 445        """
 446        tool = FindTool(self.root_folder, self.config)
 447
 448        file_type = cast(
 449            str | None,
 450            arguments.get(
 451                "file_type",
 452            ),
 453        )
 454        name = cast(
 455            str | None,
 456            arguments.get(
 457                "name",
 458            ),
 459        )
 460        regex = cast(
 461            str | None,
 462            arguments.get(
 463                "regex",
 464            ),
 465        )
 466        size = cast(
 467            str | None,
 468            arguments.get(
 469                "size",
 470            ),
 471        )
 472        return tool.find_files(file_type=file_type, name=name, regex=regex, size=size)
 473
 474    def find_files_markdown(self, arguments: dict[str, Any]) -> Any:
 475        """Generated Do Not Edit
 476
 477        Args:
 478            arguments (dict[str, Any]): The arguments for the tool.
 479
 480        Returns:
 481            Any: The result of the tool invocation.
 482        """
 483        tool = FindTool(self.root_folder, self.config)
 484
 485        file_type = cast(
 486            str | None,
 487            arguments.get(
 488                "file_type",
 489            ),
 490        )
 491        name = cast(
 492            str | None,
 493            arguments.get(
 494                "name",
 495            ),
 496        )
 497        regex = cast(
 498            str | None,
 499            arguments.get(
 500                "regex",
 501            ),
 502        )
 503        size = cast(
 504            str | None,
 505            arguments.get(
 506                "size",
 507            ),
 508        )
 509        return tool.find_files_markdown(file_type=file_type, name=name, regex=regex, size=size)
 510
 511    def get_current_branch(self, arguments: dict[str, Any]) -> Any:
 512        """Generated Do Not Edit
 513
 514        Args:
 515            arguments (dict[str, Any]): The arguments for the tool.
 516
 517        Returns:
 518            Any: The result of the tool invocation.
 519        """
 520        tool = GitTool(self.root_folder, self.config)
 521
 522        return tool.get_current_branch()
 523
 524    def get_recent_commits(self, arguments: dict[str, Any]) -> Any:
 525        """Generated Do Not Edit
 526
 527        Args:
 528            arguments (dict[str, Any]): The arguments for the tool.
 529
 530        Returns:
 531            Any: The result of the tool invocation.
 532        """
 533        tool = GitTool(self.root_folder, self.config)
 534
 535        n = cast(int, arguments.get("n", 10))
 536        short_hash = cast(bool, arguments.get("short_hash", False))
 537        return tool.get_recent_commits(n=n, short_hash=short_hash)
 538
 539    def git_diff(self, arguments: dict[str, Any]) -> Any:
 540        """Generated Do Not Edit
 541
 542        Args:
 543            arguments (dict[str, Any]): The arguments for the tool.
 544
 545        Returns:
 546            Any: The result of the tool invocation.
 547        """
 548        tool = GitTool(self.root_folder, self.config)
 549
 550        return tool.git_diff()
 551
 552    def git_diff_commit(self, arguments: dict[str, Any]) -> Any:
 553        """Generated Do Not Edit
 554
 555        Args:
 556            arguments (dict[str, Any]): The arguments for the tool.
 557
 558        Returns:
 559            Any: The result of the tool invocation.
 560        """
 561        tool = GitTool(self.root_folder, self.config)
 562
 563        commit1 = cast(
 564            str,
 565            arguments.get(
 566                "commit1",
 567            ),
 568        )
 569        commit2 = cast(
 570            str,
 571            arguments.get(
 572                "commit2",
 573            ),
 574        )
 575        return tool.git_diff_commit(commit1=commit1, commit2=commit2)
 576
 577    def git_log_file(self, arguments: dict[str, Any]) -> Any:
 578        """Generated Do Not Edit
 579
 580        Args:
 581            arguments (dict[str, Any]): The arguments for the tool.
 582
 583        Returns:
 584            Any: The result of the tool invocation.
 585        """
 586        tool = GitTool(self.root_folder, self.config)
 587
 588        filename = cast(
 589            str,
 590            arguments.get(
 591                "filename",
 592            ),
 593        )
 594        return tool.git_log_file(filename=filename)
 595
 596    def git_log_search(self, arguments: dict[str, Any]) -> Any:
 597        """Generated Do Not Edit
 598
 599        Args:
 600            arguments (dict[str, Any]): The arguments for the tool.
 601
 602        Returns:
 603            Any: The result of the tool invocation.
 604        """
 605        tool = GitTool(self.root_folder, self.config)
 606
 607        search_string = cast(
 608            str,
 609            arguments.get(
 610                "search_string",
 611            ),
 612        )
 613        return tool.git_log_search(search_string=search_string)
 614
 615    def git_show(self, arguments: dict[str, Any]) -> Any:
 616        """Generated Do Not Edit
 617
 618        Args:
 619            arguments (dict[str, Any]): The arguments for the tool.
 620
 621        Returns:
 622            Any: The result of the tool invocation.
 623        """
 624        tool = GitTool(self.root_folder, self.config)
 625
 626        return tool.git_show()
 627
 628    def git_status(self, arguments: dict[str, Any]) -> Any:
 629        """Generated Do Not Edit
 630
 631        Args:
 632            arguments (dict[str, Any]): The arguments for the tool.
 633
 634        Returns:
 635            Any: The result of the tool invocation.
 636        """
 637        tool = GitTool(self.root_folder, self.config)
 638
 639        return tool.git_status()
 640
 641    def is_ignored_by_gitignore(self, arguments: dict[str, Any]) -> Any:
 642        """Generated Do Not Edit
 643
 644        Args:
 645            arguments (dict[str, Any]): The arguments for the tool.
 646
 647        Returns:
 648            Any: The result of the tool invocation.
 649        """
 650        tool = GitTool(self.root_folder, self.config)
 651
 652        file_path = cast(
 653            str,
 654            arguments.get(
 655                "file_path",
 656            ),
 657        )
 658        gitignore_path = cast(str, arguments.get("gitignore_path", ".gitignore"))
 659        return tool.is_ignored_by_gitignore(file_path=file_path, gitignore_path=gitignore_path)
 660
 661    def grep(self, arguments: dict[str, Any]) -> Any:
 662        """Generated Do Not Edit
 663
 664        Args:
 665            arguments (dict[str, Any]): The arguments for the tool.
 666
 667        Returns:
 668            Any: The result of the tool invocation.
 669        """
 670        tool = GrepTool(self.root_folder, self.config)
 671
 672        glob_pattern = cast(
 673            str,
 674            arguments.get(
 675                "glob_pattern",
 676            ),
 677        )
 678        maximum_matches_per_file = cast(int, arguments.get("maximum_matches_per_file", -1))
 679        maximum_matches_total = cast(int, arguments.get("maximum_matches_total", -1))
 680        regex = cast(
 681            str,
 682            arguments.get(
 683                "regex",
 684            ),
 685        )
 686        skip_first_matches = cast(int, arguments.get("skip_first_matches", -1))
 687        return tool.grep(
 688            glob_pattern=glob_pattern,
 689            maximum_matches_per_file=maximum_matches_per_file,
 690            maximum_matches_total=maximum_matches_total,
 691            regex=regex,
 692            skip_first_matches=skip_first_matches,
 693        )
 694
 695    def grep_markdown(self, arguments: dict[str, Any]) -> Any:
 696        """Generated Do Not Edit
 697
 698        Args:
 699            arguments (dict[str, Any]): The arguments for the tool.
 700
 701        Returns:
 702            Any: The result of the tool invocation.
 703        """
 704        tool = GrepTool(self.root_folder, self.config)
 705
 706        glob_pattern = cast(
 707            str,
 708            arguments.get(
 709                "glob_pattern",
 710            ),
 711        )
 712        maximum_matches = cast(int, arguments.get("maximum_matches", -1))
 713        regex = cast(
 714            str,
 715            arguments.get(
 716                "regex",
 717            ),
 718        )
 719        skip_first_matches = cast(int, arguments.get("skip_first_matches", -1))
 720        return tool.grep_markdown(
 721            glob_pattern=glob_pattern,
 722            maximum_matches=maximum_matches,
 723            regex=regex,
 724            skip_first_matches=skip_first_matches,
 725        )
 726
 727    def head(self, arguments: dict[str, Any]) -> Any:
 728        """Generated Do Not Edit
 729
 730        Args:
 731            arguments (dict[str, Any]): The arguments for the tool.
 732
 733        Returns:
 734            Any: The result of the tool invocation.
 735        """
 736        tool = HeadTailTool(self.root_folder, self.config)
 737
 738        byte_count = cast(
 739            int | None,
 740            arguments.get(
 741                "byte_count",
 742            ),
 743        )
 744        file_path = cast(
 745            str,
 746            arguments.get(
 747                "file_path",
 748            ),
 749        )
 750        lines = cast(int, arguments.get("lines", 10))
 751        return tool.head(byte_count=byte_count, file_path=file_path, lines=lines)
 752
 753    def head_markdown(self, arguments: dict[str, Any]) -> Any:
 754        """Generated Do Not Edit
 755
 756        Args:
 757            arguments (dict[str, Any]): The arguments for the tool.
 758
 759        Returns:
 760            Any: The result of the tool invocation.
 761        """
 762        tool = HeadTailTool(self.root_folder, self.config)
 763
 764        file_path = cast(
 765            str,
 766            arguments.get(
 767                "file_path",
 768            ),
 769        )
 770        lines = cast(int, arguments.get("lines", 10))
 771        return tool.head_markdown(file_path=file_path, lines=lines)
 772
 773    def head_tail(self, arguments: dict[str, Any]) -> Any:
 774        """Generated Do Not Edit
 775
 776        Args:
 777            arguments (dict[str, Any]): The arguments for the tool.
 778
 779        Returns:
 780            Any: The result of the tool invocation.
 781        """
 782        tool = HeadTailTool(self.root_folder, self.config)
 783
 784        byte_count = cast(
 785            int | None,
 786            arguments.get(
 787                "byte_count",
 788            ),
 789        )
 790        file_path = cast(
 791            str,
 792            arguments.get(
 793                "file_path",
 794            ),
 795        )
 796        lines = cast(int, arguments.get("lines", 10))
 797        mode = cast(str, arguments.get("mode", "head"))
 798        return tool.head_tail(byte_count=byte_count, file_path=file_path, lines=lines, mode=mode)
 799
 800    def tail(self, arguments: dict[str, Any]) -> Any:
 801        """Generated Do Not Edit
 802
 803        Args:
 804            arguments (dict[str, Any]): The arguments for the tool.
 805
 806        Returns:
 807            Any: The result of the tool invocation.
 808        """
 809        tool = HeadTailTool(self.root_folder, self.config)
 810
 811        byte_count = cast(
 812            int | None,
 813            arguments.get(
 814                "byte_count",
 815            ),
 816        )
 817        file_path = cast(
 818            str,
 819            arguments.get(
 820                "file_path",
 821            ),
 822        )
 823        lines = cast(int, arguments.get("lines", 10))
 824        return tool.tail(byte_count=byte_count, file_path=file_path, lines=lines)
 825
 826    def tail_markdown(self, arguments: dict[str, Any]) -> Any:
 827        """Generated Do Not Edit
 828
 829        Args:
 830            arguments (dict[str, Any]): The arguments for the tool.
 831
 832        Returns:
 833            Any: The result of the tool invocation.
 834        """
 835        tool = HeadTailTool(self.root_folder, self.config)
 836
 837        file_path = cast(
 838            str,
 839            arguments.get(
 840                "file_path",
 841            ),
 842        )
 843        lines = cast(int, arguments.get("lines", 10))
 844        return tool.tail_markdown(file_path=file_path, lines=lines)
 845
 846    def insert_text_after_context(self, arguments: dict[str, Any]) -> Any:
 847        """Generated Do Not Edit
 848
 849        Args:
 850            arguments (dict[str, Any]): The arguments for the tool.
 851
 852        Returns:
 853            Any: The result of the tool invocation.
 854        """
 855        tool = InsertTool(self.root_folder, self.config)
 856
 857        context = cast(
 858            str,
 859            arguments.get(
 860                "context",
 861            ),
 862        )
 863        file_path = cast(
 864            str,
 865            arguments.get(
 866                "file_path",
 867            ),
 868        )
 869        text_to_insert = cast(
 870            str,
 871            arguments.get(
 872                "text_to_insert",
 873            ),
 874        )
 875        return tool.insert_text_after_context(context=context, file_path=file_path, text_to_insert=text_to_insert)
 876
 877    def insert_text_after_multiline_context(self, arguments: dict[str, Any]) -> Any:
 878        """Generated Do Not Edit
 879
 880        Args:
 881            arguments (dict[str, Any]): The arguments for the tool.
 882
 883        Returns:
 884            Any: The result of the tool invocation.
 885        """
 886        tool = InsertTool(self.root_folder, self.config)
 887
 888        context_lines = cast(
 889            str,
 890            arguments.get(
 891                "context_lines",
 892            ),
 893        )
 894        file_path = cast(
 895            str,
 896            arguments.get(
 897                "file_path",
 898            ),
 899        )
 900        text_to_insert = cast(
 901            str,
 902            arguments.get(
 903                "text_to_insert",
 904            ),
 905        )
 906        return tool.insert_text_after_multiline_context(
 907            context_lines=context_lines, file_path=file_path, text_to_insert=text_to_insert
 908        )
 909
 910    def insert_text_at_start_or_end(self, arguments: dict[str, Any]) -> Any:
 911        """Generated Do Not Edit
 912
 913        Args:
 914            arguments (dict[str, Any]): The arguments for the tool.
 915
 916        Returns:
 917            Any: The result of the tool invocation.
 918        """
 919        tool = InsertTool(self.root_folder, self.config)
 920
 921        file_path = cast(
 922            str,
 923            arguments.get(
 924                "file_path",
 925            ),
 926        )
 927        position = cast(str, arguments.get("position", "end"))
 928        text_to_insert = cast(
 929            str,
 930            arguments.get(
 931                "text_to_insert",
 932            ),
 933        )
 934        return tool.insert_text_at_start_or_end(file_path=file_path, position=position, text_to_insert=text_to_insert)
 935
 936    def ls(self, arguments: dict[str, Any]) -> Any:
 937        """Generated Do Not Edit
 938
 939        Args:
 940            arguments (dict[str, Any]): The arguments for the tool.
 941
 942        Returns:
 943            Any: The result of the tool invocation.
 944        """
 945        tool = LsTool(self.root_folder, self.config)
 946
 947        all_files = cast(bool, arguments.get("all_files", False))
 948        long = cast(bool, arguments.get("long", False))
 949        path = cast(
 950            str | None,
 951            arguments.get(
 952                "path",
 953            ),
 954        )
 955        return tool.ls(all_files=all_files, long=long, path=path)
 956
 957    def ls_markdown(self, arguments: dict[str, Any]) -> Any:
 958        """Generated Do Not Edit
 959
 960        Args:
 961            arguments (dict[str, Any]): The arguments for the tool.
 962
 963        Returns:
 964            Any: The result of the tool invocation.
 965        """
 966        tool = LsTool(self.root_folder, self.config)
 967
 968        all_files = cast(bool, arguments.get("all_files", False))
 969        long = cast(bool, arguments.get("long", False))
 970        path = cast(str | None, arguments.get("path", "."))
 971        return tool.ls_markdown(all_files=all_files, long=long, path=path)
 972
 973    def apply_git_patch(self, arguments: dict[str, Any]) -> Any:
 974        """Generated Do Not Edit
 975
 976        Args:
 977            arguments (dict[str, Any]): The arguments for the tool.
 978
 979        Returns:
 980            Any: The result of the tool invocation.
 981        """
 982        tool = PatchTool(self.root_folder, self.config)
 983
 984        patch_content = cast(
 985            str,
 986            arguments.get(
 987                "patch_content",
 988            ),
 989        )
 990        return tool.apply_git_patch(patch_content=patch_content)
 991
 992    def format_code_as_markdown(self, arguments: dict[str, Any]) -> Any:
 993        """Generated Do Not Edit
 994
 995        Args:
 996            arguments (dict[str, Any]): The arguments for the tool.
 997
 998        Returns:
 999            Any: The result of the tool invocation.
1000        """
1001        tool = PyCatTool(self.root_folder, self.config)
1002
1003        base_path = cast(
1004            str,
1005            arguments.get(
1006                "base_path",
1007            ),
1008        )
1009        header = cast(
1010            str,
1011            arguments.get(
1012                "header",
1013            ),
1014        )
1015        no_comments = cast(bool, arguments.get("no_comments", False))
1016        no_docs = cast(bool, arguments.get("no_docs", False))
1017        return tool.format_code_as_markdown(
1018            base_path=base_path, header=header, no_comments=no_comments, no_docs=no_docs
1019        )
1020
1021    def pytest(self, arguments: dict[str, Any]) -> Any:
1022        """Generated Do Not Edit
1023
1024        Args:
1025            arguments (dict[str, Any]): The arguments for the tool.
1026
1027        Returns:
1028            Any: The result of the tool invocation.
1029        """
1030        tool = PytestTool(self.root_folder, self.config)
1031
1032        return tool.pytest()
1033
1034    def replace_all(self, arguments: dict[str, Any]) -> Any:
1035        """Generated Do Not Edit
1036
1037        Args:
1038            arguments (dict[str, Any]): The arguments for the tool.
1039
1040        Returns:
1041            Any: The result of the tool invocation.
1042        """
1043        tool = ReplaceTool(self.root_folder, self.config)
1044
1045        file_path = cast(
1046            str,
1047            arguments.get(
1048                "file_path",
1049            ),
1050        )
1051        new_text = cast(
1052            str,
1053            arguments.get(
1054                "new_text",
1055            ),
1056        )
1057        old_text = cast(
1058            str,
1059            arguments.get(
1060                "old_text",
1061            ),
1062        )
1063        return tool.replace_all(file_path=file_path, new_text=new_text, old_text=old_text)
1064
1065    def replace_line_by_line(self, arguments: dict[str, Any]) -> Any:
1066        """Generated Do Not Edit
1067
1068        Args:
1069            arguments (dict[str, Any]): The arguments for the tool.
1070
1071        Returns:
1072            Any: The result of the tool invocation.
1073        """
1074        tool = ReplaceTool(self.root_folder, self.config)
1075
1076        file_path = cast(
1077            str,
1078            arguments.get(
1079                "file_path",
1080            ),
1081        )
1082        line_end = cast(int, arguments.get("line_end", -1))
1083        line_start = cast(int, arguments.get("line_start", 0))
1084        new_text = cast(
1085            str,
1086            arguments.get(
1087                "new_text",
1088            ),
1089        )
1090        old_text = cast(
1091            str,
1092            arguments.get(
1093                "old_text",
1094            ),
1095        )
1096        return tool.replace_line_by_line(
1097            file_path=file_path, line_end=line_end, line_start=line_start, new_text=new_text, old_text=old_text
1098        )
1099
1100    def replace_with_regex(self, arguments: dict[str, Any]) -> Any:
1101        """Generated Do Not Edit
1102
1103        Args:
1104            arguments (dict[str, Any]): The arguments for the tool.
1105
1106        Returns:
1107            Any: The result of the tool invocation.
1108        """
1109        tool = ReplaceTool(self.root_folder, self.config)
1110
1111        file_path = cast(
1112            str,
1113            arguments.get(
1114                "file_path",
1115            ),
1116        )
1117        regex_match_expression = cast(
1118            str,
1119            arguments.get(
1120                "regex_match_expression",
1121            ),
1122        )
1123        replacement = cast(
1124            str,
1125            arguments.get(
1126                "replacement",
1127            ),
1128        )
1129        return tool.replace_with_regex(
1130            file_path=file_path, regex_match_expression=regex_match_expression, replacement=replacement
1131        )
1132
1133    def rewrite_file(self, arguments: dict[str, Any]) -> Any:
1134        """Generated Do Not Edit
1135
1136        Args:
1137            arguments (dict[str, Any]): The arguments for the tool.
1138
1139        Returns:
1140            Any: The result of the tool invocation.
1141        """
1142        tool = RewriteTool(self.root_folder, self.config)
1143
1144        file_path = cast(
1145            str,
1146            arguments.get(
1147                "file_path",
1148            ),
1149        )
1150        text = cast(
1151            str,
1152            arguments.get(
1153                "text",
1154            ),
1155        )
1156        return tool.rewrite_file(file_path=file_path, text=text)
1157
1158    def write_new_file(self, arguments: dict[str, Any]) -> Any:
1159        """Generated Do Not Edit
1160
1161        Args:
1162            arguments (dict[str, Any]): The arguments for the tool.
1163
1164        Returns:
1165            Any: The result of the tool invocation.
1166        """
1167        tool = RewriteTool(self.root_folder, self.config)
1168
1169        file_path = cast(
1170            str,
1171            arguments.get(
1172                "file_path",
1173            ),
1174        )
1175        text = cast(
1176            str,
1177            arguments.get(
1178                "text",
1179            ),
1180        )
1181        return tool.write_new_file(file_path=file_path, text=text)
1182
1183    def sed(self, arguments: dict[str, Any]) -> Any:
1184        """Generated Do Not Edit
1185
1186        Args:
1187            arguments (dict[str, Any]): The arguments for the tool.
1188
1189        Returns:
1190            Any: The result of the tool invocation.
1191        """
1192        tool = SedTool(self.root_folder, self.config)
1193
1194        commands = cast(
1195            str,
1196            arguments.get(
1197                "commands",
1198            ),
1199        )
1200        file_path = cast(
1201            str,
1202            arguments.get(
1203                "file_path",
1204            ),
1205        )
1206        return tool.sed(commands=commands, file_path=file_path)
1207
1208    def add_todo(self, arguments: dict[str, Any]) -> Any:
1209        """Generated Do Not Edit
1210
1211        Args:
1212            arguments (dict[str, Any]): The arguments for the tool.
1213
1214        Returns:
1215            Any: The result of the tool invocation.
1216        """
1217        tool = TodoTool(self.root_folder, self.config)
1218
1219        assignee = cast(
1220            str | None,
1221            arguments.get(
1222                "assignee",
1223            ),
1224        )
1225        category = cast(
1226            str,
1227            arguments.get(
1228                "category",
1229            ),
1230        )
1231        description = cast(
1232            str,
1233            arguments.get(
1234                "description",
1235            ),
1236        )
1237        done_when = cast(str, arguments.get("done_when", ""))
1238        source_code_ref = cast(
1239            str,
1240            arguments.get(
1241                "source_code_ref",
1242            ),
1243        )
1244        title = cast(
1245            str,
1246            arguments.get(
1247                "title",
1248            ),
1249        )
1250        return tool.add_todo(
1251            assignee=assignee,
1252            category=category,
1253            description=description,
1254            done_when=done_when,
1255            source_code_ref=source_code_ref,
1256            title=title,
1257        )
1258
1259    def list_valid_assignees(self, arguments: dict[str, Any]) -> Any:
1260        """Generated Do Not Edit
1261
1262        Args:
1263            arguments (dict[str, Any]): The arguments for the tool.
1264
1265        Returns:
1266            Any: The result of the tool invocation.
1267        """
1268        tool = TodoTool(self.root_folder, self.config)
1269
1270        return tool.list_valid_assignees()
1271
1272    def query_todos_by_assignee(self, arguments: dict[str, Any]) -> Any:
1273        """Generated Do Not Edit
1274
1275        Args:
1276            arguments (dict[str, Any]): The arguments for the tool.
1277
1278        Returns:
1279            Any: The result of the tool invocation.
1280        """
1281        tool = TodoTool(self.root_folder, self.config)
1282
1283        assignee_name = cast(
1284            str,
1285            arguments.get(
1286                "assignee_name",
1287            ),
1288        )
1289        return tool.query_todos_by_assignee(assignee_name=assignee_name)
1290
1291    def query_todos_by_regex(self, arguments: dict[str, Any]) -> Any:
1292        """Generated Do Not Edit
1293
1294        Args:
1295            arguments (dict[str, Any]): The arguments for the tool.
1296
1297        Returns:
1298            Any: The result of the tool invocation.
1299        """
1300        tool = TodoTool(self.root_folder, self.config)
1301
1302        regex_pattern = cast(str, arguments.get("regex_pattern", "[\\s\\S]+"))
1303        return tool.query_todos_by_regex(regex_pattern=regex_pattern)
1304
1305    def remove_todo(self, arguments: dict[str, Any]) -> Any:
1306        """Generated Do Not Edit
1307
1308        Args:
1309            arguments (dict[str, Any]): The arguments for the tool.
1310
1311        Returns:
1312            Any: The result of the tool invocation.
1313        """
1314        tool = TodoTool(self.root_folder, self.config)
1315
1316        title = cast(
1317            str,
1318            arguments.get(
1319                "title",
1320            ),
1321        )
1322        return tool.remove_todo(title=title)
1323
1324    def count_tokens(self, arguments: dict[str, Any]) -> Any:
1325        """Generated Do Not Edit
1326
1327        Args:
1328            arguments (dict[str, Any]): The arguments for the tool.
1329
1330        Returns:
1331            Any: The result of the tool invocation.
1332        """
1333        tool = TokenCounterTool(self.root_folder, self.config)
1334
1335        text = cast(
1336            str,
1337            arguments.get(
1338                "text",
1339            ),
1340        )
1341        return tool.count_tokens(text=text)

AI Shell Toolkit

ToolKit( root_folder: str, token_model: str, global_max_lines: int, permitted_tools: list[str], config: Config)
35    def __init__(
36        self, root_folder: str, token_model: str, global_max_lines: int, permitted_tools: list[str], config: Config
37    ) -> None:
38        super().__init__(root_folder, token_model, global_max_lines, permitted_tools, config)
39        self._lookup: dict[str, Callable[[dict[str, Any]], Any]] = {
40            "report_bool": self.report_bool,
41            "report_dict": self.report_dict,
42            "report_float": self.report_float,
43            "report_int": self.report_int,
44            "report_json": self.report_json,
45            "report_list": self.report_list,
46            "report_set": self.report_set,
47            "report_text": self.report_text,
48            "report_toml": self.report_toml,
49            "report_tuple": self.report_tuple,
50            "report_xml": self.report_xml,
51            "cat": self.cat,
52            "cat_markdown": self.cat_markdown,
53            "cut_characters": self.cut_characters,
54            "cut_fields": self.cut_fields,
55            "cut_fields_by_name": self.cut_fields_by_name,
56            "find_files": self.find_files,
57            "find_files_markdown": self.find_files_markdown,
58            "get_current_branch": self.get_current_branch,
59            "get_recent_commits": self.get_recent_commits,
60            "git_diff": self.git_diff,
61            "git_diff_commit": self.git_diff_commit,
62            "git_log_file": self.git_log_file,
63            "git_log_search": self.git_log_search,
64            "git_show": self.git_show,
65            "git_status": self.git_status,
66            "is_ignored_by_gitignore": self.is_ignored_by_gitignore,
67            "grep": self.grep,
68            "grep_markdown": self.grep_markdown,
69            "head": self.head,
70            "head_markdown": self.head_markdown,
71            "head_tail": self.head_tail,
72            "tail": self.tail,
73            "tail_markdown": self.tail_markdown,
74            "insert_text_after_context": self.insert_text_after_context,
75            "insert_text_after_multiline_context": self.insert_text_after_multiline_context,
76            "insert_text_at_start_or_end": self.insert_text_at_start_or_end,
77            "ls": self.ls,
78            "ls_markdown": self.ls_markdown,
79            "apply_git_patch": self.apply_git_patch,
80            "format_code_as_markdown": self.format_code_as_markdown,
81            "pytest": self.pytest,
82            "replace_all": self.replace_all,
83            "replace_line_by_line": self.replace_line_by_line,
84            "replace_with_regex": self.replace_with_regex,
85            "rewrite_file": self.rewrite_file,
86            "write_new_file": self.write_new_file,
87            "sed": self.sed,
88            "add_todo": self.add_todo,
89            "list_valid_assignees": self.list_valid_assignees,
90            "query_todos_by_assignee": self.query_todos_by_assignee,
91            "query_todos_by_regex": self.query_todos_by_regex,
92            "remove_todo": self.remove_todo,
93            "count_tokens": self.count_tokens,
94        }
95        # Stateful tool support. Useless assignment to make mypy happy
96        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)

Args: root_folder (str): The root folder path for file operations. token_model (str): The token model to use for the toolkit. global_max_lines (int): The global max lines to use for the toolkit. permitted_tools (list[str]): The tools the caller is allowed to invoke. config (Config): Developer config the model shouldn't set.

tool_answer_collector
def report_bool(self, arguments: dict[str, typing.Any]) -> Any:
 98    def report_bool(self, arguments: dict[str, Any]) -> Any:
 99        """Generated Do Not Edit
100
101        Args:
102            arguments (dict[str, Any]): The arguments for the tool.
103
104        Returns:
105            Any: The result of the tool invocation.
106        """
107        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
108
109        answer = cast(
110            bool,
111            arguments.get(
112                "answer",
113            ),
114        )
115        comment = cast(str, arguments.get("comment", ""))
116        return self.tool_answer_collector.report_bool(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_dict(self, arguments: dict[str, typing.Any]) -> Any:
118    def report_dict(self, arguments: dict[str, Any]) -> Any:
119        """Generated Do Not Edit
120
121        Args:
122            arguments (dict[str, Any]): The arguments for the tool.
123
124        Returns:
125            Any: The result of the tool invocation.
126        """
127        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
128
129        answer = cast(
130            Any,
131            arguments.get(
132                "answer",
133            ),
134        )
135        comment = cast(str, arguments.get("comment", ""))
136        return self.tool_answer_collector.report_dict(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_float(self, arguments: dict[str, typing.Any]) -> Any:
138    def report_float(self, arguments: dict[str, Any]) -> Any:
139        """Generated Do Not Edit
140
141        Args:
142            arguments (dict[str, Any]): The arguments for the tool.
143
144        Returns:
145            Any: The result of the tool invocation.
146        """
147        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
148
149        answer = cast(
150            float,
151            arguments.get(
152                "answer",
153            ),
154        )
155        comment = cast(str, arguments.get("comment", ""))
156        return self.tool_answer_collector.report_float(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_int(self, arguments: dict[str, typing.Any]) -> Any:
158    def report_int(self, arguments: dict[str, Any]) -> Any:
159        """Generated Do Not Edit
160
161        Args:
162            arguments (dict[str, Any]): The arguments for the tool.
163
164        Returns:
165            Any: The result of the tool invocation.
166        """
167        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
168
169        answer = cast(
170            int,
171            arguments.get(
172                "answer",
173            ),
174        )
175        comment = cast(str, arguments.get("comment", ""))
176        return self.tool_answer_collector.report_int(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_json(self, arguments: dict[str, typing.Any]) -> Any:
178    def report_json(self, arguments: dict[str, Any]) -> Any:
179        """Generated Do Not Edit
180
181        Args:
182            arguments (dict[str, Any]): The arguments for the tool.
183
184        Returns:
185            Any: The result of the tool invocation.
186        """
187        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
188
189        answer = cast(
190            str,
191            arguments.get(
192                "answer",
193            ),
194        )
195        comment = cast(str, arguments.get("comment", ""))
196        return self.tool_answer_collector.report_json(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_list(self, arguments: dict[str, typing.Any]) -> Any:
198    def report_list(self, arguments: dict[str, Any]) -> Any:
199        """Generated Do Not Edit
200
201        Args:
202            arguments (dict[str, Any]): The arguments for the tool.
203
204        Returns:
205            Any: The result of the tool invocation.
206        """
207        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
208
209        answer = cast(
210            str,
211            arguments.get(
212                "answer",
213            ),
214        )
215        comment = cast(str, arguments.get("comment", ""))
216        return self.tool_answer_collector.report_list(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_set(self, arguments: dict[str, typing.Any]) -> Any:
218    def report_set(self, arguments: dict[str, Any]) -> Any:
219        """Generated Do Not Edit
220
221        Args:
222            arguments (dict[str, Any]): The arguments for the tool.
223
224        Returns:
225            Any: The result of the tool invocation.
226        """
227        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
228
229        answer = cast(
230            list[Any],
231            arguments.get(
232                "answer",
233            ),
234        )
235        comment = cast(str, arguments.get("comment", ""))
236        return self.tool_answer_collector.report_set(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_text(self, arguments: dict[str, typing.Any]) -> Any:
238    def report_text(self, arguments: dict[str, Any]) -> Any:
239        """Generated Do Not Edit
240
241        Args:
242            arguments (dict[str, Any]): The arguments for the tool.
243
244        Returns:
245            Any: The result of the tool invocation.
246        """
247        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
248
249        answer = cast(
250            str,
251            arguments.get(
252                "answer",
253            ),
254        )
255        comment = cast(str, arguments.get("comment", ""))
256        return self.tool_answer_collector.report_text(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_toml(self, arguments: dict[str, typing.Any]) -> Any:
258    def report_toml(self, arguments: dict[str, Any]) -> Any:
259        """Generated Do Not Edit
260
261        Args:
262            arguments (dict[str, Any]): The arguments for the tool.
263
264        Returns:
265            Any: The result of the tool invocation.
266        """
267        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
268
269        answer = cast(
270            str,
271            arguments.get(
272                "answer",
273            ),
274        )
275        comment = cast(str, arguments.get("comment", ""))
276        return self.tool_answer_collector.report_toml(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_tuple(self, arguments: dict[str, typing.Any]) -> Any:
278    def report_tuple(self, arguments: dict[str, Any]) -> Any:
279        """Generated Do Not Edit
280
281        Args:
282            arguments (dict[str, Any]): The arguments for the tool.
283
284        Returns:
285            Any: The result of the tool invocation.
286        """
287        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
288
289        answer = cast(
290            list[Any],
291            arguments.get(
292                "answer",
293            ),
294        )
295        comment = cast(str, arguments.get("comment", ""))
296        return self.tool_answer_collector.report_tuple(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def report_xml(self, arguments: dict[str, typing.Any]) -> Any:
298    def report_xml(self, arguments: dict[str, Any]) -> Any:
299        """Generated Do Not Edit
300
301        Args:
302            arguments (dict[str, Any]): The arguments for the tool.
303
304        Returns:
305            Any: The result of the tool invocation.
306        """
307        self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
308
309        answer = cast(
310            str,
311            arguments.get(
312                "answer",
313            ),
314        )
315        comment = cast(str, arguments.get("comment", ""))
316        return self.tool_answer_collector.report_xml(answer=answer, comment=comment)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def cat(self, arguments: dict[str, typing.Any]) -> Any:
318    def cat(self, arguments: dict[str, Any]) -> Any:
319        """Generated Do Not Edit
320
321        Args:
322            arguments (dict[str, Any]): The arguments for the tool.
323
324        Returns:
325            Any: The result of the tool invocation.
326        """
327        tool = CatTool(self.root_folder, self.config)
328
329        file_paths = cast(
330            str,
331            arguments.get(
332                "file_paths",
333            ),
334        )
335        number_lines = cast(bool, arguments.get("number_lines", True))
336        squeeze_blank = cast(bool, arguments.get("squeeze_blank", False))
337        return tool.cat(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def cat_markdown(self, arguments: dict[str, typing.Any]) -> Any:
339    def cat_markdown(self, arguments: dict[str, Any]) -> Any:
340        """Generated Do Not Edit
341
342        Args:
343            arguments (dict[str, Any]): The arguments for the tool.
344
345        Returns:
346            Any: The result of the tool invocation.
347        """
348        tool = CatTool(self.root_folder, self.config)
349
350        file_paths = cast(
351            str,
352            arguments.get(
353                "file_paths",
354            ),
355        )
356        number_lines = cast(bool, arguments.get("number_lines", True))
357        squeeze_blank = cast(bool, arguments.get("squeeze_blank", False))
358        return tool.cat_markdown(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def cut_characters(self, arguments: dict[str, typing.Any]) -> Any:
360    def cut_characters(self, arguments: dict[str, Any]) -> Any:
361        """Generated Do Not Edit
362
363        Args:
364            arguments (dict[str, Any]): The arguments for the tool.
365
366        Returns:
367            Any: The result of the tool invocation.
368        """
369        tool = CutTool(self.root_folder, self.config)
370
371        character_ranges = cast(
372            str,
373            arguments.get(
374                "character_ranges",
375            ),
376        )
377        file_path = cast(
378            str,
379            arguments.get(
380                "file_path",
381            ),
382        )
383        return tool.cut_characters(character_ranges=character_ranges, file_path=file_path)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def cut_fields(self, arguments: dict[str, typing.Any]) -> Any:
385    def cut_fields(self, arguments: dict[str, Any]) -> Any:
386        """Generated Do Not Edit
387
388        Args:
389            arguments (dict[str, Any]): The arguments for the tool.
390
391        Returns:
392            Any: The result of the tool invocation.
393        """
394        tool = CutTool(self.root_folder, self.config)
395
396        delimiter = cast(str, arguments.get("delimiter", ","))
397        field_ranges = cast(
398            str,
399            arguments.get(
400                "field_ranges",
401            ),
402        )
403        filename = cast(
404            str,
405            arguments.get(
406                "filename",
407            ),
408        )
409        return tool.cut_fields(delimiter=delimiter, field_ranges=field_ranges, filename=filename)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def cut_fields_by_name(self, arguments: dict[str, typing.Any]) -> Any:
411    def cut_fields_by_name(self, arguments: dict[str, Any]) -> Any:
412        """Generated Do Not Edit
413
414        Args:
415            arguments (dict[str, Any]): The arguments for the tool.
416
417        Returns:
418            Any: The result of the tool invocation.
419        """
420        tool = CutTool(self.root_folder, self.config)
421
422        delimiter = cast(str, arguments.get("delimiter", ","))
423        field_names = cast(
424            str,
425            arguments.get(
426                "field_names",
427            ),
428        )
429        filename = cast(
430            str,
431            arguments.get(
432                "filename",
433            ),
434        )
435        return tool.cut_fields_by_name(delimiter=delimiter, field_names=field_names, filename=filename)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def find_files(self, arguments: dict[str, typing.Any]) -> Any:
437    def find_files(self, arguments: dict[str, Any]) -> Any:
438        """Generated Do Not Edit
439
440        Args:
441            arguments (dict[str, Any]): The arguments for the tool.
442
443        Returns:
444            Any: The result of the tool invocation.
445        """
446        tool = FindTool(self.root_folder, self.config)
447
448        file_type = cast(
449            str | None,
450            arguments.get(
451                "file_type",
452            ),
453        )
454        name = cast(
455            str | None,
456            arguments.get(
457                "name",
458            ),
459        )
460        regex = cast(
461            str | None,
462            arguments.get(
463                "regex",
464            ),
465        )
466        size = cast(
467            str | None,
468            arguments.get(
469                "size",
470            ),
471        )
472        return tool.find_files(file_type=file_type, name=name, regex=regex, size=size)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def find_files_markdown(self, arguments: dict[str, typing.Any]) -> Any:
474    def find_files_markdown(self, arguments: dict[str, Any]) -> Any:
475        """Generated Do Not Edit
476
477        Args:
478            arguments (dict[str, Any]): The arguments for the tool.
479
480        Returns:
481            Any: The result of the tool invocation.
482        """
483        tool = FindTool(self.root_folder, self.config)
484
485        file_type = cast(
486            str | None,
487            arguments.get(
488                "file_type",
489            ),
490        )
491        name = cast(
492            str | None,
493            arguments.get(
494                "name",
495            ),
496        )
497        regex = cast(
498            str | None,
499            arguments.get(
500                "regex",
501            ),
502        )
503        size = cast(
504            str | None,
505            arguments.get(
506                "size",
507            ),
508        )
509        return tool.find_files_markdown(file_type=file_type, name=name, regex=regex, size=size)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def get_current_branch(self, arguments: dict[str, typing.Any]) -> Any:
511    def get_current_branch(self, arguments: dict[str, Any]) -> Any:
512        """Generated Do Not Edit
513
514        Args:
515            arguments (dict[str, Any]): The arguments for the tool.
516
517        Returns:
518            Any: The result of the tool invocation.
519        """
520        tool = GitTool(self.root_folder, self.config)
521
522        return tool.get_current_branch()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def get_recent_commits(self, arguments: dict[str, typing.Any]) -> Any:
524    def get_recent_commits(self, arguments: dict[str, Any]) -> Any:
525        """Generated Do Not Edit
526
527        Args:
528            arguments (dict[str, Any]): The arguments for the tool.
529
530        Returns:
531            Any: The result of the tool invocation.
532        """
533        tool = GitTool(self.root_folder, self.config)
534
535        n = cast(int, arguments.get("n", 10))
536        short_hash = cast(bool, arguments.get("short_hash", False))
537        return tool.get_recent_commits(n=n, short_hash=short_hash)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def git_diff(self, arguments: dict[str, typing.Any]) -> Any:
539    def git_diff(self, arguments: dict[str, Any]) -> Any:
540        """Generated Do Not Edit
541
542        Args:
543            arguments (dict[str, Any]): The arguments for the tool.
544
545        Returns:
546            Any: The result of the tool invocation.
547        """
548        tool = GitTool(self.root_folder, self.config)
549
550        return tool.git_diff()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def git_diff_commit(self, arguments: dict[str, typing.Any]) -> Any:
552    def git_diff_commit(self, arguments: dict[str, Any]) -> Any:
553        """Generated Do Not Edit
554
555        Args:
556            arguments (dict[str, Any]): The arguments for the tool.
557
558        Returns:
559            Any: The result of the tool invocation.
560        """
561        tool = GitTool(self.root_folder, self.config)
562
563        commit1 = cast(
564            str,
565            arguments.get(
566                "commit1",
567            ),
568        )
569        commit2 = cast(
570            str,
571            arguments.get(
572                "commit2",
573            ),
574        )
575        return tool.git_diff_commit(commit1=commit1, commit2=commit2)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def git_log_file(self, arguments: dict[str, typing.Any]) -> Any:
577    def git_log_file(self, arguments: dict[str, Any]) -> Any:
578        """Generated Do Not Edit
579
580        Args:
581            arguments (dict[str, Any]): The arguments for the tool.
582
583        Returns:
584            Any: The result of the tool invocation.
585        """
586        tool = GitTool(self.root_folder, self.config)
587
588        filename = cast(
589            str,
590            arguments.get(
591                "filename",
592            ),
593        )
594        return tool.git_log_file(filename=filename)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def git_show(self, arguments: dict[str, typing.Any]) -> Any:
615    def git_show(self, arguments: dict[str, Any]) -> Any:
616        """Generated Do Not Edit
617
618        Args:
619            arguments (dict[str, Any]): The arguments for the tool.
620
621        Returns:
622            Any: The result of the tool invocation.
623        """
624        tool = GitTool(self.root_folder, self.config)
625
626        return tool.git_show()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def git_status(self, arguments: dict[str, typing.Any]) -> Any:
628    def git_status(self, arguments: dict[str, Any]) -> Any:
629        """Generated Do Not Edit
630
631        Args:
632            arguments (dict[str, Any]): The arguments for the tool.
633
634        Returns:
635            Any: The result of the tool invocation.
636        """
637        tool = GitTool(self.root_folder, self.config)
638
639        return tool.git_status()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def is_ignored_by_gitignore(self, arguments: dict[str, typing.Any]) -> Any:
641    def is_ignored_by_gitignore(self, arguments: dict[str, Any]) -> Any:
642        """Generated Do Not Edit
643
644        Args:
645            arguments (dict[str, Any]): The arguments for the tool.
646
647        Returns:
648            Any: The result of the tool invocation.
649        """
650        tool = GitTool(self.root_folder, self.config)
651
652        file_path = cast(
653            str,
654            arguments.get(
655                "file_path",
656            ),
657        )
658        gitignore_path = cast(str, arguments.get("gitignore_path", ".gitignore"))
659        return tool.is_ignored_by_gitignore(file_path=file_path, gitignore_path=gitignore_path)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def grep(self, arguments: dict[str, typing.Any]) -> Any:
661    def grep(self, arguments: dict[str, Any]) -> Any:
662        """Generated Do Not Edit
663
664        Args:
665            arguments (dict[str, Any]): The arguments for the tool.
666
667        Returns:
668            Any: The result of the tool invocation.
669        """
670        tool = GrepTool(self.root_folder, self.config)
671
672        glob_pattern = cast(
673            str,
674            arguments.get(
675                "glob_pattern",
676            ),
677        )
678        maximum_matches_per_file = cast(int, arguments.get("maximum_matches_per_file", -1))
679        maximum_matches_total = cast(int, arguments.get("maximum_matches_total", -1))
680        regex = cast(
681            str,
682            arguments.get(
683                "regex",
684            ),
685        )
686        skip_first_matches = cast(int, arguments.get("skip_first_matches", -1))
687        return tool.grep(
688            glob_pattern=glob_pattern,
689            maximum_matches_per_file=maximum_matches_per_file,
690            maximum_matches_total=maximum_matches_total,
691            regex=regex,
692            skip_first_matches=skip_first_matches,
693        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def grep_markdown(self, arguments: dict[str, typing.Any]) -> Any:
695    def grep_markdown(self, arguments: dict[str, Any]) -> Any:
696        """Generated Do Not Edit
697
698        Args:
699            arguments (dict[str, Any]): The arguments for the tool.
700
701        Returns:
702            Any: The result of the tool invocation.
703        """
704        tool = GrepTool(self.root_folder, self.config)
705
706        glob_pattern = cast(
707            str,
708            arguments.get(
709                "glob_pattern",
710            ),
711        )
712        maximum_matches = cast(int, arguments.get("maximum_matches", -1))
713        regex = cast(
714            str,
715            arguments.get(
716                "regex",
717            ),
718        )
719        skip_first_matches = cast(int, arguments.get("skip_first_matches", -1))
720        return tool.grep_markdown(
721            glob_pattern=glob_pattern,
722            maximum_matches=maximum_matches,
723            regex=regex,
724            skip_first_matches=skip_first_matches,
725        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def head(self, arguments: dict[str, typing.Any]) -> Any:
727    def head(self, arguments: dict[str, Any]) -> Any:
728        """Generated Do Not Edit
729
730        Args:
731            arguments (dict[str, Any]): The arguments for the tool.
732
733        Returns:
734            Any: The result of the tool invocation.
735        """
736        tool = HeadTailTool(self.root_folder, self.config)
737
738        byte_count = cast(
739            int | None,
740            arguments.get(
741                "byte_count",
742            ),
743        )
744        file_path = cast(
745            str,
746            arguments.get(
747                "file_path",
748            ),
749        )
750        lines = cast(int, arguments.get("lines", 10))
751        return tool.head(byte_count=byte_count, file_path=file_path, lines=lines)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def head_markdown(self, arguments: dict[str, typing.Any]) -> Any:
753    def head_markdown(self, arguments: dict[str, Any]) -> Any:
754        """Generated Do Not Edit
755
756        Args:
757            arguments (dict[str, Any]): The arguments for the tool.
758
759        Returns:
760            Any: The result of the tool invocation.
761        """
762        tool = HeadTailTool(self.root_folder, self.config)
763
764        file_path = cast(
765            str,
766            arguments.get(
767                "file_path",
768            ),
769        )
770        lines = cast(int, arguments.get("lines", 10))
771        return tool.head_markdown(file_path=file_path, lines=lines)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def head_tail(self, arguments: dict[str, typing.Any]) -> Any:
773    def head_tail(self, arguments: dict[str, Any]) -> Any:
774        """Generated Do Not Edit
775
776        Args:
777            arguments (dict[str, Any]): The arguments for the tool.
778
779        Returns:
780            Any: The result of the tool invocation.
781        """
782        tool = HeadTailTool(self.root_folder, self.config)
783
784        byte_count = cast(
785            int | None,
786            arguments.get(
787                "byte_count",
788            ),
789        )
790        file_path = cast(
791            str,
792            arguments.get(
793                "file_path",
794            ),
795        )
796        lines = cast(int, arguments.get("lines", 10))
797        mode = cast(str, arguments.get("mode", "head"))
798        return tool.head_tail(byte_count=byte_count, file_path=file_path, lines=lines, mode=mode)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def tail(self, arguments: dict[str, typing.Any]) -> Any:
800    def tail(self, arguments: dict[str, Any]) -> Any:
801        """Generated Do Not Edit
802
803        Args:
804            arguments (dict[str, Any]): The arguments for the tool.
805
806        Returns:
807            Any: The result of the tool invocation.
808        """
809        tool = HeadTailTool(self.root_folder, self.config)
810
811        byte_count = cast(
812            int | None,
813            arguments.get(
814                "byte_count",
815            ),
816        )
817        file_path = cast(
818            str,
819            arguments.get(
820                "file_path",
821            ),
822        )
823        lines = cast(int, arguments.get("lines", 10))
824        return tool.tail(byte_count=byte_count, file_path=file_path, lines=lines)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def tail_markdown(self, arguments: dict[str, typing.Any]) -> Any:
826    def tail_markdown(self, arguments: dict[str, Any]) -> Any:
827        """Generated Do Not Edit
828
829        Args:
830            arguments (dict[str, Any]): The arguments for the tool.
831
832        Returns:
833            Any: The result of the tool invocation.
834        """
835        tool = HeadTailTool(self.root_folder, self.config)
836
837        file_path = cast(
838            str,
839            arguments.get(
840                "file_path",
841            ),
842        )
843        lines = cast(int, arguments.get("lines", 10))
844        return tool.tail_markdown(file_path=file_path, lines=lines)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def insert_text_after_context(self, arguments: dict[str, typing.Any]) -> Any:
846    def insert_text_after_context(self, arguments: dict[str, Any]) -> Any:
847        """Generated Do Not Edit
848
849        Args:
850            arguments (dict[str, Any]): The arguments for the tool.
851
852        Returns:
853            Any: The result of the tool invocation.
854        """
855        tool = InsertTool(self.root_folder, self.config)
856
857        context = cast(
858            str,
859            arguments.get(
860                "context",
861            ),
862        )
863        file_path = cast(
864            str,
865            arguments.get(
866                "file_path",
867            ),
868        )
869        text_to_insert = cast(
870            str,
871            arguments.get(
872                "text_to_insert",
873            ),
874        )
875        return tool.insert_text_after_context(context=context, file_path=file_path, text_to_insert=text_to_insert)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def insert_text_after_multiline_context(self, arguments: dict[str, typing.Any]) -> Any:
877    def insert_text_after_multiline_context(self, arguments: dict[str, Any]) -> Any:
878        """Generated Do Not Edit
879
880        Args:
881            arguments (dict[str, Any]): The arguments for the tool.
882
883        Returns:
884            Any: The result of the tool invocation.
885        """
886        tool = InsertTool(self.root_folder, self.config)
887
888        context_lines = cast(
889            str,
890            arguments.get(
891                "context_lines",
892            ),
893        )
894        file_path = cast(
895            str,
896            arguments.get(
897                "file_path",
898            ),
899        )
900        text_to_insert = cast(
901            str,
902            arguments.get(
903                "text_to_insert",
904            ),
905        )
906        return tool.insert_text_after_multiline_context(
907            context_lines=context_lines, file_path=file_path, text_to_insert=text_to_insert
908        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def insert_text_at_start_or_end(self, arguments: dict[str, typing.Any]) -> Any:
910    def insert_text_at_start_or_end(self, arguments: dict[str, Any]) -> Any:
911        """Generated Do Not Edit
912
913        Args:
914            arguments (dict[str, Any]): The arguments for the tool.
915
916        Returns:
917            Any: The result of the tool invocation.
918        """
919        tool = InsertTool(self.root_folder, self.config)
920
921        file_path = cast(
922            str,
923            arguments.get(
924                "file_path",
925            ),
926        )
927        position = cast(str, arguments.get("position", "end"))
928        text_to_insert = cast(
929            str,
930            arguments.get(
931                "text_to_insert",
932            ),
933        )
934        return tool.insert_text_at_start_or_end(file_path=file_path, position=position, text_to_insert=text_to_insert)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def ls(self, arguments: dict[str, typing.Any]) -> Any:
936    def ls(self, arguments: dict[str, Any]) -> Any:
937        """Generated Do Not Edit
938
939        Args:
940            arguments (dict[str, Any]): The arguments for the tool.
941
942        Returns:
943            Any: The result of the tool invocation.
944        """
945        tool = LsTool(self.root_folder, self.config)
946
947        all_files = cast(bool, arguments.get("all_files", False))
948        long = cast(bool, arguments.get("long", False))
949        path = cast(
950            str | None,
951            arguments.get(
952                "path",
953            ),
954        )
955        return tool.ls(all_files=all_files, long=long, path=path)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def ls_markdown(self, arguments: dict[str, typing.Any]) -> Any:
957    def ls_markdown(self, arguments: dict[str, Any]) -> Any:
958        """Generated Do Not Edit
959
960        Args:
961            arguments (dict[str, Any]): The arguments for the tool.
962
963        Returns:
964            Any: The result of the tool invocation.
965        """
966        tool = LsTool(self.root_folder, self.config)
967
968        all_files = cast(bool, arguments.get("all_files", False))
969        long = cast(bool, arguments.get("long", False))
970        path = cast(str | None, arguments.get("path", "."))
971        return tool.ls_markdown(all_files=all_files, long=long, path=path)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def apply_git_patch(self, arguments: dict[str, typing.Any]) -> Any:
973    def apply_git_patch(self, arguments: dict[str, Any]) -> Any:
974        """Generated Do Not Edit
975
976        Args:
977            arguments (dict[str, Any]): The arguments for the tool.
978
979        Returns:
980            Any: The result of the tool invocation.
981        """
982        tool = PatchTool(self.root_folder, self.config)
983
984        patch_content = cast(
985            str,
986            arguments.get(
987                "patch_content",
988            ),
989        )
990        return tool.apply_git_patch(patch_content=patch_content)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def format_code_as_markdown(self, arguments: dict[str, typing.Any]) -> Any:
 992    def format_code_as_markdown(self, arguments: dict[str, Any]) -> Any:
 993        """Generated Do Not Edit
 994
 995        Args:
 996            arguments (dict[str, Any]): The arguments for the tool.
 997
 998        Returns:
 999            Any: The result of the tool invocation.
1000        """
1001        tool = PyCatTool(self.root_folder, self.config)
1002
1003        base_path = cast(
1004            str,
1005            arguments.get(
1006                "base_path",
1007            ),
1008        )
1009        header = cast(
1010            str,
1011            arguments.get(
1012                "header",
1013            ),
1014        )
1015        no_comments = cast(bool, arguments.get("no_comments", False))
1016        no_docs = cast(bool, arguments.get("no_docs", False))
1017        return tool.format_code_as_markdown(
1018            base_path=base_path, header=header, no_comments=no_comments, no_docs=no_docs
1019        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def pytest(self, arguments: dict[str, typing.Any]) -> Any:
1021    def pytest(self, arguments: dict[str, Any]) -> Any:
1022        """Generated Do Not Edit
1023
1024        Args:
1025            arguments (dict[str, Any]): The arguments for the tool.
1026
1027        Returns:
1028            Any: The result of the tool invocation.
1029        """
1030        tool = PytestTool(self.root_folder, self.config)
1031
1032        return tool.pytest()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def replace_all(self, arguments: dict[str, typing.Any]) -> Any:
1034    def replace_all(self, arguments: dict[str, Any]) -> Any:
1035        """Generated Do Not Edit
1036
1037        Args:
1038            arguments (dict[str, Any]): The arguments for the tool.
1039
1040        Returns:
1041            Any: The result of the tool invocation.
1042        """
1043        tool = ReplaceTool(self.root_folder, self.config)
1044
1045        file_path = cast(
1046            str,
1047            arguments.get(
1048                "file_path",
1049            ),
1050        )
1051        new_text = cast(
1052            str,
1053            arguments.get(
1054                "new_text",
1055            ),
1056        )
1057        old_text = cast(
1058            str,
1059            arguments.get(
1060                "old_text",
1061            ),
1062        )
1063        return tool.replace_all(file_path=file_path, new_text=new_text, old_text=old_text)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def replace_line_by_line(self, arguments: dict[str, typing.Any]) -> Any:
1065    def replace_line_by_line(self, arguments: dict[str, Any]) -> Any:
1066        """Generated Do Not Edit
1067
1068        Args:
1069            arguments (dict[str, Any]): The arguments for the tool.
1070
1071        Returns:
1072            Any: The result of the tool invocation.
1073        """
1074        tool = ReplaceTool(self.root_folder, self.config)
1075
1076        file_path = cast(
1077            str,
1078            arguments.get(
1079                "file_path",
1080            ),
1081        )
1082        line_end = cast(int, arguments.get("line_end", -1))
1083        line_start = cast(int, arguments.get("line_start", 0))
1084        new_text = cast(
1085            str,
1086            arguments.get(
1087                "new_text",
1088            ),
1089        )
1090        old_text = cast(
1091            str,
1092            arguments.get(
1093                "old_text",
1094            ),
1095        )
1096        return tool.replace_line_by_line(
1097            file_path=file_path, line_end=line_end, line_start=line_start, new_text=new_text, old_text=old_text
1098        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def replace_with_regex(self, arguments: dict[str, typing.Any]) -> Any:
1100    def replace_with_regex(self, arguments: dict[str, Any]) -> Any:
1101        """Generated Do Not Edit
1102
1103        Args:
1104            arguments (dict[str, Any]): The arguments for the tool.
1105
1106        Returns:
1107            Any: The result of the tool invocation.
1108        """
1109        tool = ReplaceTool(self.root_folder, self.config)
1110
1111        file_path = cast(
1112            str,
1113            arguments.get(
1114                "file_path",
1115            ),
1116        )
1117        regex_match_expression = cast(
1118            str,
1119            arguments.get(
1120                "regex_match_expression",
1121            ),
1122        )
1123        replacement = cast(
1124            str,
1125            arguments.get(
1126                "replacement",
1127            ),
1128        )
1129        return tool.replace_with_regex(
1130            file_path=file_path, regex_match_expression=regex_match_expression, replacement=replacement
1131        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def rewrite_file(self, arguments: dict[str, typing.Any]) -> Any:
1133    def rewrite_file(self, arguments: dict[str, Any]) -> Any:
1134        """Generated Do Not Edit
1135
1136        Args:
1137            arguments (dict[str, Any]): The arguments for the tool.
1138
1139        Returns:
1140            Any: The result of the tool invocation.
1141        """
1142        tool = RewriteTool(self.root_folder, self.config)
1143
1144        file_path = cast(
1145            str,
1146            arguments.get(
1147                "file_path",
1148            ),
1149        )
1150        text = cast(
1151            str,
1152            arguments.get(
1153                "text",
1154            ),
1155        )
1156        return tool.rewrite_file(file_path=file_path, text=text)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def write_new_file(self, arguments: dict[str, typing.Any]) -> Any:
1158    def write_new_file(self, arguments: dict[str, Any]) -> Any:
1159        """Generated Do Not Edit
1160
1161        Args:
1162            arguments (dict[str, Any]): The arguments for the tool.
1163
1164        Returns:
1165            Any: The result of the tool invocation.
1166        """
1167        tool = RewriteTool(self.root_folder, self.config)
1168
1169        file_path = cast(
1170            str,
1171            arguments.get(
1172                "file_path",
1173            ),
1174        )
1175        text = cast(
1176            str,
1177            arguments.get(
1178                "text",
1179            ),
1180        )
1181        return tool.write_new_file(file_path=file_path, text=text)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def sed(self, arguments: dict[str, typing.Any]) -> Any:
1183    def sed(self, arguments: dict[str, Any]) -> Any:
1184        """Generated Do Not Edit
1185
1186        Args:
1187            arguments (dict[str, Any]): The arguments for the tool.
1188
1189        Returns:
1190            Any: The result of the tool invocation.
1191        """
1192        tool = SedTool(self.root_folder, self.config)
1193
1194        commands = cast(
1195            str,
1196            arguments.get(
1197                "commands",
1198            ),
1199        )
1200        file_path = cast(
1201            str,
1202            arguments.get(
1203                "file_path",
1204            ),
1205        )
1206        return tool.sed(commands=commands, file_path=file_path)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def add_todo(self, arguments: dict[str, typing.Any]) -> Any:
1208    def add_todo(self, arguments: dict[str, Any]) -> Any:
1209        """Generated Do Not Edit
1210
1211        Args:
1212            arguments (dict[str, Any]): The arguments for the tool.
1213
1214        Returns:
1215            Any: The result of the tool invocation.
1216        """
1217        tool = TodoTool(self.root_folder, self.config)
1218
1219        assignee = cast(
1220            str | None,
1221            arguments.get(
1222                "assignee",
1223            ),
1224        )
1225        category = cast(
1226            str,
1227            arguments.get(
1228                "category",
1229            ),
1230        )
1231        description = cast(
1232            str,
1233            arguments.get(
1234                "description",
1235            ),
1236        )
1237        done_when = cast(str, arguments.get("done_when", ""))
1238        source_code_ref = cast(
1239            str,
1240            arguments.get(
1241                "source_code_ref",
1242            ),
1243        )
1244        title = cast(
1245            str,
1246            arguments.get(
1247                "title",
1248            ),
1249        )
1250        return tool.add_todo(
1251            assignee=assignee,
1252            category=category,
1253            description=description,
1254            done_when=done_when,
1255            source_code_ref=source_code_ref,
1256            title=title,
1257        )

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def list_valid_assignees(self, arguments: dict[str, typing.Any]) -> Any:
1259    def list_valid_assignees(self, arguments: dict[str, Any]) -> Any:
1260        """Generated Do Not Edit
1261
1262        Args:
1263            arguments (dict[str, Any]): The arguments for the tool.
1264
1265        Returns:
1266            Any: The result of the tool invocation.
1267        """
1268        tool = TodoTool(self.root_folder, self.config)
1269
1270        return tool.list_valid_assignees()

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def query_todos_by_assignee(self, arguments: dict[str, typing.Any]) -> Any:
1272    def query_todos_by_assignee(self, arguments: dict[str, Any]) -> Any:
1273        """Generated Do Not Edit
1274
1275        Args:
1276            arguments (dict[str, Any]): The arguments for the tool.
1277
1278        Returns:
1279            Any: The result of the tool invocation.
1280        """
1281        tool = TodoTool(self.root_folder, self.config)
1282
1283        assignee_name = cast(
1284            str,
1285            arguments.get(
1286                "assignee_name",
1287            ),
1288        )
1289        return tool.query_todos_by_assignee(assignee_name=assignee_name)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def query_todos_by_regex(self, arguments: dict[str, typing.Any]) -> Any:
1291    def query_todos_by_regex(self, arguments: dict[str, Any]) -> Any:
1292        """Generated Do Not Edit
1293
1294        Args:
1295            arguments (dict[str, Any]): The arguments for the tool.
1296
1297        Returns:
1298            Any: The result of the tool invocation.
1299        """
1300        tool = TodoTool(self.root_folder, self.config)
1301
1302        regex_pattern = cast(str, arguments.get("regex_pattern", "[\\s\\S]+"))
1303        return tool.query_todos_by_regex(regex_pattern=regex_pattern)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def remove_todo(self, arguments: dict[str, typing.Any]) -> Any:
1305    def remove_todo(self, arguments: dict[str, Any]) -> Any:
1306        """Generated Do Not Edit
1307
1308        Args:
1309            arguments (dict[str, Any]): The arguments for the tool.
1310
1311        Returns:
1312            Any: The result of the tool invocation.
1313        """
1314        tool = TodoTool(self.root_folder, self.config)
1315
1316        title = cast(
1317            str,
1318            arguments.get(
1319                "title",
1320            ),
1321        )
1322        return tool.remove_todo(title=title)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

def count_tokens(self, arguments: dict[str, typing.Any]) -> Any:
1324    def count_tokens(self, arguments: dict[str, Any]) -> Any:
1325        """Generated Do Not Edit
1326
1327        Args:
1328            arguments (dict[str, Any]): The arguments for the tool.
1329
1330        Returns:
1331            Any: The result of the tool invocation.
1332        """
1333        tool = TokenCounterTool(self.root_folder, self.config)
1334
1335        text = cast(
1336            str,
1337            arguments.get(
1338                "text",
1339            ),
1340        )
1341        return tool.count_tokens(text=text)

Generated Do Not Edit

Args: arguments (dict[str, Any]): The arguments for the tool.

Returns: Any: The result of the tool invocation.

ALL_TOOLS = []
def initialize_all_tools(skips: list[str] | None = None, keeps: list[str] | None = None) -> None:
36def initialize_all_tools(skips: list[str] | None = None, keeps: list[str] | None = None) -> None:
37    """Initialize all tools
38
39    Args:
40        skips (Optional[list[str]], optional): Tools to skip. Defaults to None.
41        keeps (Optional[list[str]], optional): Tools to keep. Defaults to None.
42    """
43    if keeps is not None:
44        keep = keeps
45    elif skips is None:
46        keep = just_tool_names()
47    else:
48        keep = [name for name in just_tool_names() if name not in skips]
49
50    for _ns, tools in SCHEMAS.items():
51        for name, schema in tools.items():
52            function_style: dict[str, Union[str, Collection[str]]] = {"name": name}
53            parameters = {"type": "object", "properties": schema["properties"], "required": schema["required"]}
54            function_style["parameters"] = parameters
55            function_style["description"] = schema["description"]
56            if name in keep:
57                ALL_TOOLS.append(function_style)
58    active_tools_string = ", ".join(tool["name"] for tool in ALL_TOOLS)
59    logger.info(f"Active tools {active_tools_string}")

Initialize all tools

Args: skips (Optional[list[str]], optional): Tools to skip. Defaults to None. keeps (Optional[list[str]], optional): Tools to keep. Defaults to None.

class Config:
 11class Config:
 12    """A class for managing the ai_shell.toml file.
 13
 14    This is for globally available settings that the model shouldn't (or can't) set,
 15    e.g. read-only mode, the plugin folder, and tool defaults.
 16    """
 17
 18    def __init__(self, config_path: str = "") -> None:
 19        """Initialize the Config class."""
 20        if config_path and config_path.endswith(".toml"):
 21            self.config_file = config_path
 22        elif config_path:
 23            self.config_file = os.path.join(config_path, "ai_shell.toml")
 24        else:
 25            self.config_file = os.getenv("CONFIG_PATH", "ai_shell.toml")
 26        # freeze the location of the config file
 27        self.config_file = os.path.abspath(self.config_file)
 28        self._list_data: dict[str, list[str]] = {}
 29        self._values_data: dict[str, str] = {}
 30        self._flags_data: dict[str, bool] = {
 31            "enable_autocat": True,
 32        }
 33        self.load_config()
 34
 35    def load_config(self) -> None:
 36        """Load the config from the config file."""
 37        if os.path.isfile(self.config_file):
 38            data = toml.load(self.config_file)
 39            self._flags_data = data.get("flags", self._flags_data)
 40            self._values_data = data.get("values", {})
 41            self._list_data = data.get("lists", {})
 42        else:
 43            self.save_config()
 44
 45    def save_config(self) -> None:
 46        """Save the config to the config file."""
 47        if not os.path.isabs(self.config_file):
 48            raise ValueError("Config file path must be absolute.")
 49        with open(self.config_file, "w", encoding="utf-8") as f:
 50            toml.dump(
 51                {
 52                    "flags": self._flags_data,
 53                    "values": self._values_data,
 54                    "lists": self._list_data,
 55                },
 56                f,
 57            )
 58
 59    def set_flag(self, flag_name: str, value: bool) -> None:
 60        """Set the value of the given flag.
 61
 62        Args:
 63            flag_name (str): The name of the flag.
 64            value (bool): The value of the flag.
 65        """
 66        self._flags_data[flag_name] = value
 67        self.save_config()
 68
 69    def get_flag(self, flag_name: str, default_value: bool | None = None) -> bool | None:
 70        """Return the value of the given flag.
 71
 72        Args:
 73            flag_name (str): The name of the flag.
 74            default_value (Optional[bool]): The default to return if the flag is unset.
 75
 76        Returns:
 77            Optional[bool]: The value of the flag, or the default.
 78        """
 79        return self._flags_data.get(flag_name, default_value)
 80
 81    def get_value(self, name: str, default: str | None = None) -> str | None:
 82        """Return the value of the given named value.
 83
 84        Args:
 85            name (str): The name of the config value.
 86            default (Optional[str]): The default to return if unset.
 87
 88        Returns:
 89            Optional[str]: The value, or the default.
 90        """
 91        return self._values_data.get(name, default)
 92
 93    def get_required_value(self, name: str) -> str:
 94        """Return a required named value.
 95
 96        Args:
 97            name (str): The name of the config value.
 98
 99        Returns:
100            str: The value.
101
102        Raises:
103            FatalConfigurationError: If the value does not exist.
104        """
105        value = self._values_data.get(name, None)
106        if value is None:
107            raise FatalConfigurationError(f"Need {name} in config file")
108        return value
109
110    def set_list(self, list_name: str, value: list[str]) -> None:
111        """Set a named list.
112
113        Args:
114            list_name (str): The name of the list.
115            value (list[str]): The list contents.
116        """
117        self._list_data[list_name] = value
118        self.save_config()
119
120    def get_list(self, list_name: str) -> list[str]:
121        """Return a named list.
122
123        Args:
124            list_name (str): The name of the list.
125
126        Returns:
127            list[str]: The list, or empty.
128        """
129        return self._list_data.get(list_name, [])

A class for managing the ai_shell.toml file.

This is for globally available settings that the model shouldn't (or can't) set, e.g. read-only mode, the plugin folder, and tool defaults.

Config(config_path: str = '')
18    def __init__(self, config_path: str = "") -> None:
19        """Initialize the Config class."""
20        if config_path and config_path.endswith(".toml"):
21            self.config_file = config_path
22        elif config_path:
23            self.config_file = os.path.join(config_path, "ai_shell.toml")
24        else:
25            self.config_file = os.getenv("CONFIG_PATH", "ai_shell.toml")
26        # freeze the location of the config file
27        self.config_file = os.path.abspath(self.config_file)
28        self._list_data: dict[str, list[str]] = {}
29        self._values_data: dict[str, str] = {}
30        self._flags_data: dict[str, bool] = {
31            "enable_autocat": True,
32        }
33        self.load_config()

Initialize the Config class.

config_file
def load_config(self) -> None:
35    def load_config(self) -> None:
36        """Load the config from the config file."""
37        if os.path.isfile(self.config_file):
38            data = toml.load(self.config_file)
39            self._flags_data = data.get("flags", self._flags_data)
40            self._values_data = data.get("values", {})
41            self._list_data = data.get("lists", {})
42        else:
43            self.save_config()

Load the config from the config file.

def save_config(self) -> None:
45    def save_config(self) -> None:
46        """Save the config to the config file."""
47        if not os.path.isabs(self.config_file):
48            raise ValueError("Config file path must be absolute.")
49        with open(self.config_file, "w", encoding="utf-8") as f:
50            toml.dump(
51                {
52                    "flags": self._flags_data,
53                    "values": self._values_data,
54                    "lists": self._list_data,
55                },
56                f,
57            )

Save the config to the config file.

def set_flag(self, flag_name: str, value: bool) -> None:
59    def set_flag(self, flag_name: str, value: bool) -> None:
60        """Set the value of the given flag.
61
62        Args:
63            flag_name (str): The name of the flag.
64            value (bool): The value of the flag.
65        """
66        self._flags_data[flag_name] = value
67        self.save_config()

Set the value of the given flag.

Args: flag_name (str): The name of the flag. value (bool): The value of the flag.

def get_flag(self, flag_name: str, default_value: bool | None = None) -> bool | None:
69    def get_flag(self, flag_name: str, default_value: bool | None = None) -> bool | None:
70        """Return the value of the given flag.
71
72        Args:
73            flag_name (str): The name of the flag.
74            default_value (Optional[bool]): The default to return if the flag is unset.
75
76        Returns:
77            Optional[bool]: The value of the flag, or the default.
78        """
79        return self._flags_data.get(flag_name, default_value)

Return the value of the given flag.

Args: flag_name (str): The name of the flag. default_value (Optional[bool]): The default to return if the flag is unset.

Returns: Optional[bool]: The value of the flag, or the default.

def get_value(self, name: str, default: str | None = None) -> str | None:
81    def get_value(self, name: str, default: str | None = None) -> str | None:
82        """Return the value of the given named value.
83
84        Args:
85            name (str): The name of the config value.
86            default (Optional[str]): The default to return if unset.
87
88        Returns:
89            Optional[str]: The value, or the default.
90        """
91        return self._values_data.get(name, default)

Return the value of the given named value.

Args: name (str): The name of the config value. default (Optional[str]): The default to return if unset.

Returns: Optional[str]: The value, or the default.

def get_required_value(self, name: str) -> str:
 93    def get_required_value(self, name: str) -> str:
 94        """Return a required named value.
 95
 96        Args:
 97            name (str): The name of the config value.
 98
 99        Returns:
100            str: The value.
101
102        Raises:
103            FatalConfigurationError: If the value does not exist.
104        """
105        value = self._values_data.get(name, None)
106        if value is None:
107            raise FatalConfigurationError(f"Need {name} in config file")
108        return value

Return a required named value.

Args: name (str): The name of the config value.

Returns: str: The value.

Raises: FatalConfigurationError: If the value does not exist.

def set_list(self, list_name: str, value: list[str]) -> None:
110    def set_list(self, list_name: str, value: list[str]) -> None:
111        """Set a named list.
112
113        Args:
114            list_name (str): The name of the list.
115            value (list[str]): The list contents.
116        """
117        self._list_data[list_name] = value
118        self.save_config()

Set a named list.

Args: list_name (str): The name of the list. value (list[str]): The list contents.

def get_list(self, list_name: str) -> list[str]:
120    def get_list(self, list_name: str) -> list[str]:
121        """Return a named list.
122
123        Args:
124            list_name (str): The name of the list.
125
126        Returns:
127            list[str]: The list, or empty.
128        """
129        return self._list_data.get(list_name, [])

Return a named list.

Args: list_name (str): The name of the list.

Returns: list[str]: The list, or empty.

def configure_logging() -> dict[str, typing.Any]:
 9def configure_logging() -> dict[str, Any]:
10    """Basic style"""
11    logging_config: dict[str, Any] = {
12        "version": 1,
13        "disable_existing_loggers": True,
14        "formatters": {
15            "standard": {"format": "[%(levelname)s] %(name)s: %(message)s"},
16        },
17        "handlers": {
18            "default": {
19                "level": "DEBUG",
20                "formatter": "standard",
21                "class": "logging.StreamHandler",
22                "stream": "ext://sys.stdout",  # Default is stderr
23            },
24            # "bug_trail": {
25            #     "level": "DEBUG",
26            #     # "formatter": "standard",
27            #     "class": "bug_trail_core.BugTrailHandler",
28            #     "db_path": bug_trail_config.database_path,
29            #     "minimum_level": logging.DEBUG,
30            # },
31            # "json": {
32            #     # "()": "json_file_handler_factory",
33            #     "level": "DEBUG",
34            #     "class": "ai_shell.utils.json_log_handler.JSONFileHandler",
35            #     "directory": "api_logs",
36            #     "module_name": "openai",
37            # },
38        },
39        "loggers": {
40            # root logger can capture too much
41            "": {  # root logger
42                "handlers": [
43                    "default",
44                    # "bug_trail"
45                ],
46                "level": "DEBUG",
47                "propagate": False,
48            },
49        },
50    }
51
52    debug_level_modules: list[str] = ["__main__", "ai_shell", "minimal_example"]
53
54    info_level_modules: list[str] = []
55    warn_level_modules: list[str] = []
56
57    # json handler
58    for name in ["openai"]:
59        logging_config["loggers"][name] = {
60            "handlers": [],  # ["json"],
61            "level": "DEBUG",
62            "propagate": False,
63        }
64
65    for name in debug_level_modules:
66        logging_config["loggers"][name] = {
67            "handlers": [
68                "default",
69                # "bug_trail"
70            ],
71            "level": "DEBUG",
72            "propagate": False,
73        }
74
75    for name in info_level_modules:
76        logging_config["loggers"][name] = {
77            "handlers": [
78                "default",
79                # "bug_trail"
80            ],
81            "level": "INFO",
82            "propagate": False,
83        }
84
85    for name in warn_level_modules:
86        logging_config["loggers"][name] = {
87            "handlers": [
88                "default",
89                # "bug_trail"
90            ],
91            "level": "WARNING",
92            "propagate": False,
93        }
94    return logging_config

Basic style

def invoke_pylint( module_name: str, minimum_score: float) -> ai_shell.externals.subprocess_utils.CommandResult:
11def invoke_pylint(module_name: str, minimum_score: float) -> CommandResult:
12    """
13    Runs pylint on the module.
14
15    Args:
16        module_name (str): The name of the module to run pylint on.
17        minimum_score (float): The minimum score to pass.
18
19    Returns:
20        CommandResult: The result of the command.
21    """
22    command_name = "pylint"
23    arg_string = f"'{module_name}' --fail-under {minimum_score}"
24
25    # generic response.
26    return safe_subprocess(command_name, arg_string)

Runs pylint on the module.

Args: module_name (str): The name of the module to run pylint on. minimum_score (float): The minimum score to pass.

Returns: CommandResult: The result of the command.

def invoke_black(file_path: str) -> ai_shell.externals.subprocess_utils.CommandResult:
 9def invoke_black(file_path: str) -> CommandResult:
10    """
11    Runs black on the file or folder. Code 128 means the file is hosed.
12
13    Args:
14        file_path (str): The name of the module to run black on.
15
16    Returns:
17        CommandResult: The result of the command.
18    """
19    command_name = "black"
20    arg_string = f"'{file_path}' --check"
21
22    return safe_subprocess(command_name, arg_string)

Runs black on the file or folder. Code 128 means the file is hosed.

Args: file_path (str): The name of the module to run black on.

Returns: CommandResult: The result of the command.

def count_lines_of_code(file_path: str) -> pygount.analysis.SourceAnalysis:
10def count_lines_of_code(file_path: str) -> "SourceAnalysis":
11    """
12    Check the lines of code in a file. File must exist.
13    Args:
14        file_path (str): The path to the file.
15
16    Returns:
17        SourceAnalysis: The analysis of the file, including line counts.
18    """
19    try:
20        from pygount import SourceAnalysis  # pylint: disable=import-outside-toplevel
21    except ImportError as exc:
22        raise RuntimeError(
23            "count_lines_of_code requires pygount. Install ai_shell[checkers] or the pygount package."
24        ) from exc
25
26    return SourceAnalysis.from_file(file_path, "pygount", encoding="utf-8")

Check the lines of code in a file. File must exist. Args: file_path (str): The path to the file.

Returns: SourceAnalysis: The analysis of the file, including line counts.

@contextmanager
def change_directory(new_path: str) -> Iterator[None]:
 9@contextmanager
10def change_directory(new_path: str) -> Iterator[None]:
11    """Change the current working directory to a new path.
12
13    Args:
14        new_path (str): The new path to change to.
15    """
16    original_directory = os.getcwd()
17    try:
18        os.chdir(new_path)
19        yield None
20    finally:
21        os.chdir(original_directory)

Change the current working directory to a new path.

Args: new_path (str): The new path to change to.