"""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))