ml-knowledge-platform/knowledge_platform/tools/builtin/ls.py
2026-02-16 04:50:51 -08:00

116 lines
3.7 KiB
Python

"""ListDirectoryTool — list directory contents with metadata.
Provides a lightweight ``ls``-style listing showing file type, size,
and modification time. Faster and more structured than Bash(ls) or
Glob for simple directory orientation.
"""
from __future__ import annotations
import time
from pathlib import Path
from typing import Any, ClassVar
from ..base import Tool, ToolParameter, ToolResult
def _format_size(size: int) -> str:
"""Format byte count into human-readable size."""
for unit in ("B", "KB", "MB", "GB"):
if size < 1024:
return f"{size:>{6}.1f} {unit}" if unit != "B" else f"{size:>{6}} {unit} "
break
size /= 1024 # type: ignore[assignment]
return f"{size:>{6}.1f} TB"
class ListDirectoryTool(Tool):
"""List contents of a directory with type, size, and modification time.
Returns entries sorted with directories first, then files, each
group sorted alphabetically.
"""
name: ClassVar[str] = "ls"
description: ClassVar[str] = (
"List directory contents showing file type, size, and modification time. "
"Directories are listed first, then files."
)
parameters: ClassVar[list[ToolParameter]] = [
ToolParameter(
name="path",
type="string",
description="Absolute path to the directory to list",
),
ToolParameter(
name="show_hidden",
type="boolean",
description="Include hidden files/directories (names starting with .)",
required=False,
default=False,
),
]
async def execute(self, **kwargs: Any) -> ToolResult:
dir_path = Path(kwargs["path"])
show_hidden: bool = kwargs.get("show_hidden", False)
if not dir_path.is_absolute():
return ToolResult.fail(f"Path must be absolute: {dir_path}")
if not dir_path.exists():
return ToolResult.fail(f"Directory not found: {dir_path}")
if not dir_path.is_dir():
return ToolResult.fail(f"Not a directory: {dir_path}")
try:
entries = list(dir_path.iterdir())
except PermissionError:
return ToolResult.fail(f"Permission denied: {dir_path}")
if not show_hidden:
entries = [e for e in entries if not e.name.startswith(".")]
dirs: list[Path] = sorted(
[e for e in entries if e.is_dir()], key=lambda p: p.name
)
files: list[Path] = sorted(
[e for e in entries if e.is_file()], key=lambda p: p.name
)
other: list[Path] = sorted(
[e for e in entries if not e.is_dir() and not e.is_file()],
key=lambda p: p.name,
)
lines: list[str] = []
for d in dirs:
try:
st = d.stat()
mtime = time.strftime("%Y-%m-%d %H:%M", time.localtime(st.st_mtime))
except OSError:
mtime = " "
lines.append(f"d {mtime} - {d.name}/")
for f in files:
try:
st = f.stat()
mtime = time.strftime("%Y-%m-%d %H:%M", time.localtime(st.st_mtime))
size = _format_size(st.st_size)
except OSError:
mtime = " "
size = " ? B "
lines.append(f"f {mtime} {size} {f.name}")
for o in other:
lines.append(f"? {'':16} {'':9} {o.name}")
output = "\n".join(lines) if lines else "(empty directory)"
return ToolResult.success(
output,
total_entries=len(dirs) + len(files) + len(other),
directories=len(dirs),
files=len(files),
)