Skip to content

Instantly share code, notes, and snippets.

@Uninen
Created November 18, 2024 23:04
Show Gist options
  • Select an option

  • Save Uninen/2515d89755f681f374355e6ee687f9c3 to your computer and use it in GitHub Desktop.

Select an option

Save Uninen/2515d89755f681f374355e6ee687f9c3 to your computer and use it in GitHub Desktop.
"""
Inspired by https://github.com/jefftriplett/files-to-claude-xml
"""
import logging
from contextlib import contextmanager
from pathlib import Path
from typing import Optional, Set
from xml.sax import saxutils
import glob2
import typer
import yaml
from pydantic import BaseModel, Field, ValidationError
from rich import print
from rich.logging import RichHandler
from rich.progress import track
logging.basicConfig(level=logging.INFO, format="%(message)s", handlers=[RichHandler(rich_tracebacks=True)])
log = logging.getLogger("claude-xml")
class ProjectConfig(BaseModel):
include: list[str] = Field(
...,
description="Glob patterns for files to include",
)
exclude: list[str] = Field(default=[], description="Glob patterns for files to exclude")
project_details_path: Optional[Path] = Field(default=None, description="Optional path to project details file")
class XMLCompilationError(Exception):
pass
@contextmanager
def error_handler(operation: str):
try:
yield None
except ValidationError as e:
log.error("Configuration validation failed")
for error in e.errors():
log.error(f" - {error['loc']}: {error['msg']}")
raise typer.Exit(1)
except XMLCompilationError as e:
log.error(f"XML compilation failed: {e}")
raise typer.Exit(1)
except Exception as e:
log.error(f"Error during {operation}: {str(e)}")
raise typer.Exit(1)
def resolve_paths(patterns: list[str], base_dir: Path) -> Set[Path]:
paths: Set[Path] = set()
for pattern in patterns:
if Path(pattern).is_absolute():
full_pattern = pattern
else:
full_pattern = str(base_dir / pattern)
paths.update(Path(p).resolve() for p in glob2.glob(full_pattern, recursive=True))
return paths
def get_project_files(config: ProjectConfig, base_dir: Path) -> Set[Path]:
included = resolve_paths(config.include, base_dir)
excluded = resolve_paths(config.exclude, base_dir)
return {p for p in (included - excluded) if p.is_file()}
def load_config(config_path: Path) -> ProjectConfig:
with open(config_path) as f:
raw_config = yaml.safe_load(f)
return ProjectConfig.model_validate(raw_config)
def compile_xml(files: Set[Path], show_progress: bool = True) -> str:
xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>', "<documents>"]
errors = []
file_iter = track(sorted(files)) if show_progress else sorted(files)
for index, file in enumerate(file_iter, start=1):
log.debug(f"Processing file: {file}")
try:
content = file.read_text(encoding="utf-8")
# escape XML special characters
escaped_content = saxutils.escape(content)
relative_path = file.relative_to(Path.cwd())
escaped_path = saxutils.escape(relative_path.as_posix())
xml_parts.extend(
[
f'<document index="{index}">',
f"<source>{escaped_path}</source>",
"<document_content>",
escaped_content,
"</document_content>",
"</document>",
]
)
except Exception as e:
error_msg = f"Error processing {file}: {str(e)}"
log.error(error_msg)
errors.append(error_msg)
# continue processing other files
xml_parts.append("</documents>")
if errors:
raise XMLCompilationError(f"Failed to process {len(errors)} files. Check logs for details.")
return "\n".join(xml_parts)
app = typer.Typer(help=__doc__)
@app.command()
def main(
config_path: Path = typer.Argument(default=".claude_project/config.yml", help="Path to configuration file"),
output_path: Path = typer.Argument(default=".claude_project/project.xml", help="Output XML file path"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress progress display"),
) -> None:
log.setLevel(logging.DEBUG if verbose else logging.INFO)
if not config_path.exists():
print(f"[red]Error:[/red] Configuration file {config_path} not found")
raise typer.Exit(1)
base_dir = Path.cwd()
with error_handler("XML generation"):
config = load_config(config_path)
log.debug(f"Loaded configuration from {config_path}")
project_files = get_project_files(config, base_dir)
if not project_files:
log.warning("No files matched the patterns")
raise typer.Exit(0)
log.info(f"Processing {len(project_files)} files...")
xml_content = compile_xml(project_files, show_progress=not quiet)
output_path.write_text(xml_content, encoding="utf-8")
log.info(f"Successfully wrote XML to {output_path}")
if __name__ == "__main__":
app()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment