lmcat API documentation

docs for lmcat v0.0.1
View Source on GitHub

# 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

2. 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

View Source
View on GitHub

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

def main() -> None:
View Source
View on GitHub

    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