mcp-git-ingest
by adhikasp
- src
- llm_context
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional, cast
from packaging import version
from llm_context.utils import ProjectLayout, Toml, safe_read_file
CURRENT_CONFIG_VERSION = version.parse("2.4")
MEDIA_EXTENSIONS: set[str] = {
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".mp4",
".mkv",
".avi",
".mov",
".wmv",
".mp3",
".wav",
".flac",
".ttf",
".otf",
".woff",
".woff2",
".eot",
".ico",
".pdf",
".zip",
".rar",
".7z",
".tar",
".exe",
".dll",
".so",
".dylib",
".map",
}
GIT_IGNORE_DEFAULT: list[str] = [
".git",
".gitignore",
".llm-context/",
"*.tmp",
"*.lock",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"go.sum",
"elm-stuff",
"LICENSE",
"CHANGELOG.md",
"README.md",
".env",
".dockerignore",
"Dockerfile",
"docker-compose.yml",
"*.log",
"*.svg",
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"*.ico",
"*.woff",
"*.woff2",
"*.eot",
"*.ttf",
"*.map",
]
IGNORE_NOTHING = [".git"]
INCLUDE_ALL = ["**/*"]
@dataclass(frozen=True)
class Profile:
gitignores: dict[str, list[str]]
only_includes: dict[str, list[str]]
settings: dict[str, Any]
prompt: str
@staticmethod
def create_default() -> "Profile":
return Profile.create(
{"full_files": GIT_IGNORE_DEFAULT, "outline_files": GIT_IGNORE_DEFAULT},
{"full_files": INCLUDE_ALL, "outline_files": INCLUDE_ALL},
{"no_media": False, "with_user_notes": False},
None,
)
@staticmethod
def create_code() -> "Profile":
return Profile.create_default()
@staticmethod
def from_config(config: dict[str, Any]) -> "Profile":
return Profile.create(
config["gitignores"],
config["only-include"],
config["settings"],
config.get("prompt", {}),
)
@staticmethod
def create(gitignores, only_include, settings, prompt) -> "Profile":
return Profile(gitignores, only_include, settings, prompt)
def get_ignore_patterns(self, context_type: str) -> list[str]:
return self.gitignores[f"{context_type}_files"]
def get_settings(self) -> dict[str, Any]:
return self.settings
def get_only_includes(self, context_type: str) -> list[str]:
return self.only_includes[f"{context_type}_files"]
def get_prompt(self, project_layout: ProjectLayout) -> Optional[str]:
if self.prompt:
prompt_path = project_layout.project_config_path / self.prompt
return safe_read_file(str(prompt_path))
return None
def get_project_notes(self, project_layout: ProjectLayout) -> Optional[str]:
return safe_read_file(str(project_layout.project_notes_path))
def get_user_notes(self, project_layout: ProjectLayout) -> Optional[str]:
return safe_read_file(str(project_layout.user_notes_path))
def to_dict(self) -> dict[str, Any]:
non_optional = {
"gitignores": self.gitignores,
"only-include": self.only_includes,
"settings": self.settings,
}
return {**non_optional, "prompt": self.prompt} if self.prompt else non_optional
@dataclass(frozen=True)
class ToolConstants:
__warning__: str
config_version: str
default_profile: dict[str, Any]
@staticmethod
def load(path: Path) -> "ToolConstants":
try:
return ToolConstants(**Toml.load(path))
except Exception:
return ToolConstants.create_null()
@staticmethod
def create_new() -> "ToolConstants":
return ToolConstants.create_default(str(CURRENT_CONFIG_VERSION))
@staticmethod
def create_null() -> "ToolConstants":
return ToolConstants.create_default("0")
@staticmethod
def create_default(version: str) -> "ToolConstants":
return ToolConstants.create(version, Profile.create_default().to_dict())
@staticmethod
def create(config_version: str, default_profile: dict[str, Any]) -> "ToolConstants":
return ToolConstants(
"This file is managed by llm-context. DO NOT EDIT.",
config_version,
default_profile,
)
@property
def needs_update(self) -> bool:
return version.parse(self.config_version) < CURRENT_CONFIG_VERSION
def to_dict(self) -> dict[str, Any]:
return {
"__warning__": self.__warning__,
"config_version": self.config_version,
"default_profile": self.default_profile,
}
@dataclass(frozen=True)
class ProfileResolver:
config: dict[str, Any]
system_state: ToolConstants
@staticmethod
def create(config: dict[str, Any], system_state: ToolConstants) -> "ProfileResolver":
return ProfileResolver(config, system_state)
def has_profile(self, profile_name: str) -> bool:
if profile_name == "default":
return True
return profile_name in self.config["profiles"]
def get_profile(self, profile_name: str) -> Profile:
if profile_name == "default":
return Profile.from_config(self.system_state.default_profile)
resolved_config = self.resolve_profile(profile_name)
return Profile.from_config(resolved_config)
def resolve_profile(self, profile_name: str) -> dict[str, Any]:
if profile_name == "default":
return self.system_state.default_profile
try:
profile_config = self.config["profiles"][profile_name]
except KeyError:
raise ValueError(f"Profile '{profile_name}' not found in config")
if "base" not in profile_config:
return cast(dict[str, Any], profile_config)
base_name = profile_config["base"]
try:
base_profile = self.resolve_profile(base_name)
except KeyError:
raise ValueError(
f"Base profile '{base_name}' referenced by '{profile_name}' not found in config"
)
merged = base_profile.copy()
for key, value in profile_config.items():
if key == "base":
continue
if isinstance(value, dict) and key in merged and isinstance(merged[key], dict):
merged[key] = {**merged[key], **value}
else:
merged[key] = value
return merged