Newer
Older
navi-1 / navi / tools / delete_tool.py
"""Built-in tool that removes a user tool by moving it to tools/.trash/."""

import json
from pathlib import Path

from navi.config import settings

from ._internal.base import Tool, ToolResult

_TRASH_DIR_NAME = ".trash"


def _trash_dir(tools_dir: Path) -> Path:
    d = tools_dir / _TRASH_DIR_NAME
    d.mkdir(exist_ok=True)
    return d


def _read_enabled(tools_dir: Path) -> list[str]:
    f = tools_dir / "enabled.json"
    try:
        return json.loads(f.read_text()) if f.exists() else []
    except Exception:
        return []


def _write_enabled(tools_dir: Path, names: list[str]) -> None:
    (tools_dir / "enabled.json").write_text(json.dumps(names, indent=2))


class DeleteToolTool(Tool):
    name = "delete_tool"
    description = (
        "Remove, restore, or list trashed user tools. "
        "remove — moves tools/<name>.py to tools/.trash/ and unregisters it. "
        "restore — moves it back and re-registers it. "
        "list — shows what is currently in the trash."
    )
    parameters = {
        "type": "object",
        "properties": {
            "action": {
                "type": "string",
                "enum": ["remove", "restore", "list"],
                "description": "remove: trash the tool. restore: recover from trash. list: show trash contents.",
            },
            "name": {
                "type": "string",
                "description": "Tool name (filename without .py). Required for remove and restore.",
            },
        },
        "required": ["action"],
    }

    def __init__(self, registry=None) -> None:
        self._registry = registry

    async def execute(self, params: dict) -> ToolResult:
        action = params.get("action", "")
        if action == "remove":
            return await self._remove(params)
        if action == "restore":
            return await self._restore(params)
        if action == "list":
            return self._list()
        return ToolResult(success=False, output=f"Unknown action '{action}'.", error="invalid action")

    async def _remove(self, params: dict) -> ToolResult:
        name = (params.get("name") or "").strip().removesuffix(".py")
        if not name:
            return ToolResult(success=False, output="name is required for remove.", error="missing name")

        tools_dir = Path(settings.tools_dir)
        src = tools_dir / f"{name}.py"
        if not src.exists():
            return ToolResult(success=False, output=f"tools/{name}.py not found.", error="not found")

        dest = _trash_dir(tools_dir) / f"{name}.py"
        if dest.exists():
            # avoid silent overwrite — rename with suffix
            i = 1
            while dest.exists():
                dest = _trash_dir(tools_dir) / f"{name}.{i}.py"
                i += 1

        src.rename(dest)

        # Remove from enabled.json
        enabled = _read_enabled(tools_dir)
        if name in enabled:
            enabled.remove(name)
            _write_enabled(tools_dir, enabled)

        # Reload registry to unregister
        notes = []
        if self._registry is not None:
            self._registry.reload_user_tools(settings.tools_dir)
            notes.append("unregistered from registry")
        else:
            notes.append("registry unavailable — restart to fully unregister")

        return ToolResult(
            success=True,
            output=f"Moved tools/{name}.py → tools/.trash/{dest.name}. "
                   f"Removed from enabled.json. {', '.join(notes).capitalize()}.",
        )

    async def _restore(self, params: dict) -> ToolResult:
        name = (params.get("name") or "").strip().removesuffix(".py")
        if not name:
            return ToolResult(success=False, output="name is required for restore.", error="missing name")

        tools_dir = Path(settings.tools_dir)
        trash = _trash_dir(tools_dir)
        src = trash / f"{name}.py"
        if not src.exists():
            trashed = [p.name for p in trash.glob("*.py")]
            hint = f" Trash contains: {', '.join(trashed)}" if trashed else " Trash is empty."
            return ToolResult(success=False, output=f"tools/.trash/{name}.py not found.{hint}", error="not found")

        dest = tools_dir / f"{name}.py"
        if dest.exists():
            return ToolResult(
                success=False,
                output=f"tools/{name}.py already exists. Remove or rename it first.",
                error="conflict",
            )

        src.rename(dest)

        # Add back to enabled.json
        enabled = _read_enabled(tools_dir)
        if name not in enabled:
            enabled.append(name)
            _write_enabled(tools_dir, enabled)

        # Reload registry
        notes = []
        if self._registry is not None:
            self._registry.reload_user_tools(settings.tools_dir)
            notes.append("re-registered in registry")
        else:
            notes.append("registry unavailable — restart to register")

        return ToolResult(
            success=True,
            output=f"Restored tools/.trash/{name}.py → tools/{name}.py. "
                   f"Added to enabled.json. {', '.join(notes).capitalize()}.",
        )

    def _list(self) -> ToolResult:
        tools_dir = Path(settings.tools_dir)
        trash = _trash_dir(tools_dir)
        files = sorted(trash.glob("*.py"))
        if not files:
            return ToolResult(success=True, output="Trash is empty.")
        lines = [f.name for f in files]
        return ToolResult(success=True, output=f"{len(lines)} file(s) in trash:\n" + "\n".join(lines))