Skip to content

Search files

search_files

SearchFilesTool — grep-like search across project files.

Registered as search_files via the axm.tools entry point.

SearchFilesTool

Grep-like search across project files.

Searches for literal strings or regex patterns across files within a sandboxed project root. Supports glob-based file filtering. Registered as search_files via axm.tools entry point.

Source code in packages/axm-edit/src/axm_edit/tools/search_files.py
Python
class SearchFilesTool:
    """Grep-like search across project files.

    Searches for literal strings or regex patterns across files within a
    sandboxed project root. Supports glob-based file filtering.
    Registered as ``search_files`` via axm.tools entry point.
    """

    agent_hint: str = (
        "Text or regex search across files."
        " Returns matching lines with file:line context."
        " Use when ast_search can't help (comments, strings, config)."
    )

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "search_files"

    def execute(
        self,
        *,
        path: str = ".",
        pattern: str | None = None,
        is_regex: bool = False,
        include: list[str] | None = None,
        **kwargs: object,
    ) -> ToolResult:
        """Search for a pattern across project files.

        Args:
            path: Project root directory (default ".").
            pattern: Search string or regex (required).
            is_regex: Treat pattern as regex (default False).
            include: Glob patterns to filter files (e.g. ["*.py"]).

        Returns:
            ToolResult with matches list (file, line, content),
            count, and truncated flag.
        """
        root_str = path

        # ── Validate inputs ──────────────────────────────────────────
        if pattern is None or pattern == "":
            return ToolResult(
                success=False,
                error="Missing required argument: pattern",
            )

        root = Path(root_str).resolve()
        if not root.is_dir():
            return ToolResult(
                success=False,
                error=f"Root is not a directory: {root_str}",
            )

        # Compile regex if needed
        matcher: re.Pattern[str] | str
        if is_regex:
            try:
                matcher = re.compile(pattern)
            except re.error as exc:
                return ToolResult(
                    success=False,
                    error=f"Invalid regex pattern: {exc}",
                )
        else:
            matcher = pattern

        # ── Walk and search ──────────────────────────────────────────
        results, truncated = _walk_and_search(root, matcher, is_regex, include)

        count = len(results)
        return ToolResult(
            success=True,
            data={
                "matches": results,
                "count": count,
                "truncated": truncated,
            },
            text=render_text(
                matches=results,
                count=count,
                truncated=truncated,
            ),
        )
name property

Tool name used for MCP registration.

execute(*, path='.', pattern=None, is_regex=False, include=None, **kwargs)

Search for a pattern across project files.

Parameters:

Name Type Description Default
path str

Project root directory (default ".").

'.'
pattern str | None

Search string or regex (required).

None
is_regex bool

Treat pattern as regex (default False).

False
include list[str] | None

Glob patterns to filter files (e.g. ["*.py"]).

None

Returns:

Type Description
ToolResult

ToolResult with matches list (file, line, content),

ToolResult

count, and truncated flag.

Source code in packages/axm-edit/src/axm_edit/tools/search_files.py
Python
def execute(
    self,
    *,
    path: str = ".",
    pattern: str | None = None,
    is_regex: bool = False,
    include: list[str] | None = None,
    **kwargs: object,
) -> ToolResult:
    """Search for a pattern across project files.

    Args:
        path: Project root directory (default ".").
        pattern: Search string or regex (required).
        is_regex: Treat pattern as regex (default False).
        include: Glob patterns to filter files (e.g. ["*.py"]).

    Returns:
        ToolResult with matches list (file, line, content),
        count, and truncated flag.
    """
    root_str = path

    # ── Validate inputs ──────────────────────────────────────────
    if pattern is None or pattern == "":
        return ToolResult(
            success=False,
            error="Missing required argument: pattern",
        )

    root = Path(root_str).resolve()
    if not root.is_dir():
        return ToolResult(
            success=False,
            error=f"Root is not a directory: {root_str}",
        )

    # Compile regex if needed
    matcher: re.Pattern[str] | str
    if is_regex:
        try:
            matcher = re.compile(pattern)
        except re.error as exc:
            return ToolResult(
                success=False,
                error=f"Invalid regex pattern: {exc}",
            )
    else:
        matcher = pattern

    # ── Walk and search ──────────────────────────────────────────
    results, truncated = _walk_and_search(root, matcher, is_regex, include)

    count = len(results)
    return ToolResult(
        success=True,
        data={
            "matches": results,
            "count": count,
            "truncated": truncated,
        },
        text=render_text(
            matches=results,
            count=count,
            truncated=truncated,
        ),
    )

render_text(*, matches, count, truncated)

Render a compact, ripgrep-style LLM-facing view of the matches.

Matches are grouped under their file path; each line is rendered as {line}: {content}. The header carries the total match count, the number of distinct files, and an explicit TRUNCATED flag when the result cap was reached. Every match (file, line, content) and the truncation signal are preserved verbatim, so no information is lost relative to data.

Source code in packages/axm-edit/src/axm_edit/tools/search_files.py
Python
def render_text(
    *,
    matches: list[dict[str, object]],
    count: int,
    truncated: bool,
) -> str:
    """Render a compact, ripgrep-style LLM-facing view of the matches.

    Matches are grouped under their file path; each line is rendered as
    ``  {line}: {content}``. The header carries the total match count, the
    number of distinct files, and an explicit ``TRUNCATED`` flag when the
    result cap was reached. Every match (file, line, content) and the
    truncation signal are preserved verbatim, so no information is lost
    relative to ``data``.
    """
    if not matches:
        return "search_files | 0 matches"

    n_files = len({str(m["file"]) for m in matches})
    plural_m = "es" if count != 1 else ""
    plural_f = "s" if n_files != 1 else ""
    header = f"search_files | {count} match{plural_m} · {n_files} file{plural_f}"
    if truncated:
        header += f" · TRUNCATED at {count}"

    lines = [header]
    current: str | None = None
    for match in matches:
        file_rel = str(match["file"])
        if file_rel != current:
            current = file_rel
            lines.append(file_rel)
        lines.append(f"  {match['line']}: {match['content']}")
    return "\n".join(lines)