mcp-git-ingest
by adhikasp
- src
- llm_context
from dataclasses import dataclass
from datetime import datetime as dt
from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, getLogger
from pathlib import Path
from typing import Any, Optional, Union
import tomlkit
@dataclass(frozen=True)
class Toml:
@staticmethod
def load(file_path: Path) -> dict[str, Any]:
with open(file_path, "r") as f:
return tomlkit.load(f)
@staticmethod
def save(file_path: Path, data: dict[str, Any]):
with open(file_path, "w") as f:
f.write(tomlkit.dumps(data))
@dataclass(frozen=True)
class ProjectLayout:
root_path: Path
@property
def project_config_path(self) -> Path:
return self.root_path / ".llm-context"
@property
def project_notes_path(self) -> Path:
return self.project_config_path / "lc-project-notes.md"
@property
def user_notes_path(self) -> Path:
return Path.home() / ".llm-context" / "lc-user-notes.md"
@property
def config_path(self) -> Path:
return self.project_config_path / "config.toml"
@property
def state_path(self) -> Path:
return self.project_config_path / "lc-state.toml"
@property
def state_store_path(self) -> Path:
return self.project_config_path / "curr_ctx.toml"
@property
def templates_path(self) -> Path:
return self.project_config_path / "templates"
def get_template_path(self, template_name: str) -> Path:
return self.templates_path / template_name
def _format_size(size_bytes):
for unit in ["B", "KB", "MB", "GB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def format_age(timestamp: float) -> str:
delta = dt.now().timestamp() - timestamp
if delta < 3600:
return f"{int(delta/60)}m ago"
if delta < 86400:
return f"{int(delta/3600)}h ago"
return f"{int(delta/86400)}d ago"
def size_feedback(content: str) -> None:
if content is None:
log(WARNING, "No content to copy")
else:
bytes_copied = len(content.encode("utf-8"))
log(INFO, f"Copied {_format_size(bytes_copied)} to clipboard")
def safe_read_file(path: str) -> Optional[str]:
file_path = Path(path)
if not file_path.exists():
log(ERROR, f"File not found: {file_path}")
return None
if not file_path.is_file():
log(ERROR, f"Not a file: {file_path}")
return None
try:
return file_path.read_text()
except PermissionError:
log(ERROR, f"Permission denied: {file_path}")
except Exception as e:
log(ERROR, f"Error reading file {file_path}: {str(e)}")
return None
@dataclass(frozen=True)
class PathConverter:
root: Path
@staticmethod
def create(root: Path) -> "PathConverter":
return PathConverter(root)
def validate(self, paths: Union[str, list[str]]) -> bool:
if isinstance(paths, str):
return paths.startswith(f"/{self.root.name}/")
return all(path.startswith(f"/{self.root.name}/") for path in paths)
def to_absolute(self, relative_paths: list[str]) -> list[str]:
return [self._convert_single_path(path) for path in relative_paths]
def to_relative(self, absolute_paths: list[str]) -> list[str]:
return [self._make_relative(path) for path in absolute_paths]
def _convert_single_path(self, path: str) -> str:
return str(self.root / Path(path[len(self.root.name) + 2 :]))
def _make_relative(self, path: str) -> str:
return f"/{self.root.name}/{Path(path).relative_to(self.root)}"
def log(level: int, msg: str) -> None:
from llm_context.exec_env import ExecutionEnvironment
logger = (
ExecutionEnvironment.current().logger
if ExecutionEnvironment.has_current()
else getLogger("llm-context-fallback")
)
if level == ERROR:
logger.error(msg)
elif level == WARNING:
logger.warning(msg)
elif level == INFO:
logger.info(msg)
elif level == DEBUG:
logger.debug(msg)
elif level == CRITICAL:
logger.critical(msg)