architectai-mcp / services /filesystem_service.py
JawadBenali's picture
Initial deployment - ArchitectAI MCP
2ba8e82
import os
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import List, Optional, Literal
@dataclass
class FileSystemNode:
name: str
type: Literal["file", "directory"]
path: str
size: Optional[int] = None
children: Optional[List['FileSystemNode']] = None
class FileSystemVisitor:
"""
A deterministic visitor for the file system.
Acts exactly like ast.NodeVisitor but for folders.
"""
# Hardcoded junk filter (The "Context Window Saver")
IGNORED_DIRS = {
"__pycache__", "node_modules", ".git", ".vscode", ".idea",
"dist", "build", "coverage", ".venv", "venv", "env"
}
IGNORED_FILES = {
".DS_Store", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"
}
def visit(self, root_path: str, max_depth: int = 4) -> dict:
"""
Public entry point. Returns a Dictionary representing the tree.
"""
path = Path(root_path).resolve()
if not path.exists():
raise ValueError(f"Path not found: {root_path}")
# Start the recursion
node = self._visit_node(path, current_depth=0, max_depth=max_depth)
return asdict(node) if node else {}
def _visit_node(self, path: Path, current_depth: int, max_depth: int) -> Optional[FileSystemNode]:
"""
Recursive logic.
If directory: returns Node with children.
If file: returns Node (leaf).
"""
# 1. Check Filters (The "Guard Clauses")
if path.name in self.IGNORED_DIRS or path.name in self.IGNORED_FILES:
return None
# 2. Handle File (Leaf Node)
if path.is_file():
return FileSystemNode(
name=path.name,
type="file",
path=str(path),
size=path.stat().st_size
)
# 3. Handle Directory (Container Node)
if path.is_dir():
# Stop recursion if too deep
if current_depth >= max_depth:
return FileSystemNode(
name=path.name,
type="directory",
path=str(path),
children=[] # Truncated
)
children_nodes = []
try:
# Deterministic Sort: Directories first, then files (A-Z)
# This ensures the LLM always sees the same structure.
entries = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
for entry in entries:
child = self._visit_node(entry, current_depth + 1, max_depth)
if child:
children_nodes.append(child)
except PermissionError:
pass # Skip locked folders
return FileSystemNode(
name=path.name,
type="directory",
path=str(path),
children=children_nodes
)
return None
# ---------------------------------------------------------
# The Formatter (Equivalent to your PlantUML Converter)
# ---------------------------------------------------------
class TreeFormatter:
def format(self, node_dict: dict) -> str:
"""Converts the JSON tree into a visual string."""
lines = []
self._render(node_dict, lines, "", is_last=True)
return "\n".join(lines)
def _render(self, node: dict, lines: list, prefix: str, is_last: bool):
# Visual logic (└── vs β”œβ”€β”€)
connector = "└── " if is_last else "β”œβ”€β”€ "
lines.append(f"{prefix}{connector}{node['name']}")
prefix += " " if is_last else "β”‚ "
children = node.get("children", []) or []
count = len(children)
for i, child in enumerate(children):
self._render(child, lines, prefix, i == count - 1)