lmcat
lmcat
A Python tool for concatenating files and directory structures into a single document, perfect for sharing code with language models. It respects .gitignore and .lmignore patterns and provides configurable output formatting.
Features
- Creates a tree view of your directory structure
- Includes file contents with clear delimiters
- Respects
.gitignorepatterns (can be disabled) - Supports custom ignore patterns via
.lmignore - Configurable via
pyproject.toml,lmcat.toml, orlmcat.json - Python 3.11+ native, with fallback support for older versions
Installation
Install from PyPI:
pip install lmcat
Usage
Basic usage - concatenate current directory:
python -m lmcat
The output will include a directory tree and the contents of each non-ignored file.
Command Line Options
-g,--no-include-gitignore: Ignore.gitignorefiles (they are included by default)-t,--tree-only: Only print the directory tree, not file contents-o,--output: Specify an output file (defaults to stdout)-h,--help: Show help message
Configuration
lmcat can be configured using any of these files (in order of precedence):
pyproject.toml(under[tool.lmcat])lmcat.tomllmcat.json
Configuration options:
[tool.lmcat]
tree_divider = "│ " # Used for vertical lines in the tree
indent = " " # Used for indentation
file_divider = "├── " # Used for file/directory entries
content_divider = "``````" # Used to delimit file contents
include_gitignore = true # Whether to respect .gitignore files
tree_only = false # Whether to only show the tree
Ignore Patterns
lmcat supports two types of ignore files:
.gitignore- Standard Git ignore patterns (used by default).lmignore- Custom ignore patterns specific to lmcat
.lmignore follows the same pattern syntax as .gitignore. Patterns in .lmignore take precedence over .gitignore.
Example .lmignore:
# Ignore all .log files
*.log
# Ignore the build directory and its contents
build/
# Un-ignore a specific file (overrides previous patterns)
!important.log
Development
Setup
- Clone the repository:
git clone https://github.com/mivanit/lmcat
cd lmcat
- Set up the development environment:
make setup
This will:
- Create a virtual environment
- Install development dependencies
- Set up pre-commit hooks
Development Commands
The project uses make for common development tasks:
make dep: Install/update dependenciesmake format: Format code using ruff and pyclnmake test: Run testsmake typing: Run type checksmake check: Run all checks (format, test, typing)make clean: Clean temporary filesmake docs: Generate documentationmake build: Build the packagemake publish: Publish to PyPI (maintainers only)
Run make help to see all available commands.
Running Tests
make test
For verbose output:
VERBOSE=1 make test
For test coverage:
make cov
Roadmap
- better tests, I feel like gitignore/lmignore interaction is broken
- llm summarization and caching of those summaries in
.lmsummary/ - reasonable defaults for file extensions to ignore
- web interface
195def main() -> None: 196 """Main entry point for the script""" 197 parser = argparse.ArgumentParser( 198 description="lmcat - list tree and content, combining .gitignore + .lmignore", 199 add_help=False, 200 ) 201 parser.add_argument( 202 "-g", 203 "--no-include-gitignore", 204 action="store_false", 205 dest="include_gitignore", 206 default=True, 207 help="Do not parse .gitignore files, only .lmignore (default: parse them).", 208 ) 209 parser.add_argument( 210 "-t", 211 "--tree-only", 212 action="store_true", 213 default=False, 214 help="Only print the tree, not the file contents.", 215 ) 216 parser.add_argument( 217 "-o", 218 "--output", 219 action="store", 220 default=None, 221 help="Output file to write the tree and contents to.", 222 ) 223 parser.add_argument( 224 "-h", "--help", action="help", help="Show this help message and exit." 225 ) 226 227 args, unknown = parser.parse_known_args() 228 229 root_dir = Path(".").resolve() 230 config = LMCatConfig.read(root_dir) 231 232 # CLI overrides 233 config.include_gitignore = args.include_gitignore 234 config.tree_only = args.tree_only 235 236 tree_output, collected_files = walk_and_collect(root_dir, config) 237 238 output: list[str] = [] 239 output.append("# File Tree") 240 output.append("\n```") 241 output.extend(tree_output) 242 output.append("```\n") 243 244 cwd = Path.cwd() 245 246 # Add file contents if not suppressed 247 if not config.tree_only: 248 output.append("# File Contents") 249 250 for fpath in collected_files: 251 relpath_posix = fpath.relative_to(cwd).as_posix() 252 pathspec_start = f'{{ path: "{relpath_posix}" }}' 253 pathspec_end = f'{{ end_of_file: "{relpath_posix}" }}' 254 output.append("") 255 output.append(config.content_divider + pathspec_start) 256 with fpath.open("r", encoding="utf-8", errors="ignore") as fobj: 257 output.append(fobj.read()) 258 output.append(config.content_divider + pathspec_end) 259 260 # Write output 261 if args.output: 262 Path(args.output).parent.mkdir(parents=True, exist_ok=True) 263 with open(args.output, "w", encoding="utf-8") as f: 264 f.write("\n".join(output)) 265 else: 266 if sys.platform == "win32": 267 sys.stdout = io.TextIOWrapper( 268 sys.stdout.buffer, encoding="utf-8", errors="replace" 269 ) 270 sys.stderr = io.TextIOWrapper( 271 sys.stderr.buffer, encoding="utf-8", errors="replace" 272 ) 273 274 print("\n".join(output))
Main entry point for the script