docs for lmcat v0.0.1
View Source on GitHub

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 .gitignore patterns (can be disabled)
  • Supports custom ignore patterns via .lmignore
  • Configurable via pyproject.toml, lmcat.toml, or lmcat.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 .gitignore files (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):

  1. pyproject.toml (under [tool.lmcat])
  2. lmcat.toml
  3. lmcat.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:

  1. .gitignore - Standard Git ignore patterns (used by default)
  2. .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

  1. Clone the repository:
git clone https://github.com/mivanit/lmcat
cd lmcat
  1. 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 dependencies
  • make format: Format code using ruff and pycln
  • make test: Run tests
  • make typing: Run type checks
  • make check: Run all checks (format, test, typing)
  • make clean: Clean temporary files
  • make docs: Generate documentation
  • make build: Build the package
  • make 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

1"""
2.. include:: ../README.md
3"""
4
5from lmcat.lmcat import main
6
7__all__ = ["main"]

def main() -> None:
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