120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
import time
|
|
import datetime
|
|
import tomllib
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
|
|
class ToolError(Exception):
|
|
"""Custom exception for tool-related errors to be caught by the MCP server."""
|
|
pass
|
|
|
|
def load_toml(path: str) -> Dict[str, Any]:
|
|
"""Loads a TOML file into a dictionary."""
|
|
try:
|
|
with open(path, "rb") as f:
|
|
return tomllib.load(f)
|
|
except Exception as e:
|
|
raise ToolError(f"Failed to load config file {path}: {str(e)}")
|
|
|
|
def format_relative_time(timestamp: float) -> str:
|
|
"""Converts a timestamp to a human-readable relative format."""
|
|
now = time.time()
|
|
diff = now - timestamp
|
|
seconds = int(diff)
|
|
|
|
if seconds < 0:
|
|
return "In the future"
|
|
if seconds < 60:
|
|
return "Just now"
|
|
|
|
minutes = seconds // 60
|
|
if minutes < 60:
|
|
return f"{minutes}m ago"
|
|
|
|
hours = minutes // 60
|
|
if hours < 24:
|
|
return f"{hours}h ago"
|
|
|
|
days = hours // 24
|
|
if days == 1:
|
|
return "Yesterday"
|
|
if days < 30:
|
|
return f"{days}d ago"
|
|
|
|
months = days // 30
|
|
if months < 12:
|
|
return f"{months}mo ago"
|
|
|
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
|
return dt.strftime('%Y-%m-%d')
|
|
|
|
|
|
def get_file_info_list(path: Path, extensions: Optional[tuple] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
Gathers basic information about files in a directory.
|
|
Optional extensions filter (e.g. ('.png', '.jpg')).
|
|
"""
|
|
items = []
|
|
try:
|
|
for entry in path.iterdir():
|
|
if extensions and not entry.name.lower().endswith(extensions):
|
|
continue
|
|
|
|
stats = entry.stat()
|
|
items.append({
|
|
"name": entry.name,
|
|
"path": entry,
|
|
"mtime": stats.st_mtime,
|
|
"size": stats.st_size,
|
|
"is_dir": entry.is_dir()
|
|
})
|
|
except Exception as e:
|
|
raise ToolError(f"Error accessing directory {path}: {e}")
|
|
|
|
return items
|
|
|
|
def sort_file_list(items: List[Dict[str, Any]], sort_by: str) -> List[Dict[str, Any]]:
|
|
"""Sorts items based on the provided key. Directories always come first."""
|
|
if sort_by == "mtime":
|
|
# Newest first
|
|
items.sort(key=lambda x: (not x["is_dir"], -x["mtime"]))
|
|
elif sort_by == "size":
|
|
# Largest first
|
|
items.sort(key=lambda x: (not x["is_dir"], -x["size"]))
|
|
else:
|
|
# Alphabetical
|
|
items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
|
|
return items
|
|
|
|
def get_paginated_list(items: List[Any], page: int, page_size: int) -> List[Any]:
|
|
"""Returns a slice of the list based on page and page_size."""
|
|
start_idx = (page - 1) * page_size
|
|
end_idx = start_idx + page_size
|
|
return items[start_idx:end_idx]
|
|
|
|
def parse_indices(indices_str: str) -> List[int]:
|
|
"""
|
|
Parses a string like '11, 31-33, 44' into a list of 1-based indices.
|
|
Preserves the order specified in the string.
|
|
"""
|
|
indices = []
|
|
parts = [p.strip() for p in indices_str.split(',')]
|
|
for part in parts:
|
|
if not part:
|
|
continue
|
|
if '-' in part:
|
|
try:
|
|
start, end = map(int, part.split('-'))
|
|
step = 1 if start <= end else -1
|
|
for i in range(start, end + step, step):
|
|
indices.append(i)
|
|
except ValueError:
|
|
raise ToolError(f"Invalid range format: {part}")
|
|
else:
|
|
try:
|
|
indices.append(int(part))
|
|
except ValueError:
|
|
raise ToolError(f"Invalid index format: {part}")
|
|
return indices
|