Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ repos:
language: python
files: ''
pass_filenames: false
args: [".", "-c", '.*\.(py|md|yaml|toml)$', "-o", "CONTEXT.md"]
always_run: true
args: ["-c", '.*\.(py|md|yaml|toml)$', "-o", "CONTEXT.md"]
3 changes: 1 addition & 2 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
language: python
files: ''
pass_filenames: false
args: [".", "-o", "CONTEXT.md"]
always_run: true
args: ["-o", "CONTEXT.md"]
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ if __name__ == "__main__":
main()
```

Running `project-context .` in the project root would generate the following output to stdout:
Running `project-context` from the project root would generate the following output to stdout:

````markdown
# hello-world-pkg
Expand Down Expand Up @@ -102,13 +102,13 @@ By default, all files tracked by Git are included in the directory tree.
Generate context for the current directory and write it to stdout:

```bash
project-context .
project-context
```

Save output to a file:

```bash
project-context . -o CONTEXT.md
project-context -o CONTEXT.md
```

### Customizing the Output
Expand All @@ -119,6 +119,7 @@ By default, all files that are tracked by Git are included in the directory tree

| Flag | Description | Example |
|------|-------------|---------|
| `--root, -r` | Root directory to use. Defaults to the current working directory. | `-r my-repo` |
| `--exclude, -e` | Regex patterns to exclude paths from the tree | `-e 'test.*'` |
| `--include, -i` | Only include paths matching these regex patterns | `-i '.*\.py$' -i '.*\.y[a]?ml$` |
| `--always-include, -a` | Always include these paths regardless of exclusion rules | `-a 'README.*'` |
Expand Down Expand Up @@ -157,41 +158,41 @@ For any file in the root directory, the inclusion/exclusion rules are applied in

### Advanced Usage Examples

**Write the context to a custom dot-file:**
**Write the context of a subdirectory to a custom dot-file:**

```bash
project-context . -o .context.md
project-context -r src/my_project -o src/my_project/.context.md
```

**Include Python files, YAML files, and markdown files in the content section:**

```bash

project-context . -c '.*\.(py|md|yaml)$'
project-context -c '.*\.(py|md|yaml)$'
```

**Exclude multiple patterns from the project context:**

```bash
project-context . -e '^\..*' -e '.*\.yaml$'
project-context -e '^\..*' -e '.*\.yaml$'
```

**Exclude all YAML files, except for your `.pre-commit-config.yaml`:**

```bash
project-context . -e '.*\.yaml$' -a '\.pre-commit-config\.yaml'
project-context -e '.*\.yaml$' -a '\.pre-commit-config\.yaml'
```

**Only include typescript files and README files:**

```bash
project-context . -i '.*\.ts$' -a 'README.*'
project-context -i '.*\.ts$' -a 'README.*'
```

**Generate the output using a custom template and write to file:**

```bash
project-context . -t custom_template.md.j2 -o CONTEXT.md
project-context -t custom_template.md.j2 -o CONTEXT.md
```

### Pre-commit Hook Integration
Expand All @@ -205,11 +206,12 @@ repos:
hooks:
- id: project-context
name: Generate LLM context from project contents
args: ['.', '-o', 'CONTEXT.md'] # defaults, feel free to customize filters/output here
files: '' # change as needed if you only want to update when specific files change
args: ['-o', 'CONTEXT.md'] # default args, update as needed
```

**Important**:
2. Consider adding `CONTEXT.md` to your `.gitignore` file if you don't want to track the generated context file in your repository, since it effectively duplicates your project contents.
Consider adding `CONTEXT.md` to your `.gitignore` file if you don't want to track the generated context file in your repository, since it effectively duplicates your project contents.

The pre-commit hook will automatically regenerate the context file whenever you make a commit, ensuring your project context is always up-to-date for sharing with LLMs.

Expand Down Expand Up @@ -263,5 +265,5 @@ Here are some guidelines and constraints on how the project should be maintained
Then you can generate the context using your custom template like this:

```bash
project-context . -t custom_template.md.j2 -o CONTEXT.md
project-context -t custom_template.md.j2 -o CONTEXT.md
```
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,24 @@ bump = true
dev = [
"pre-commit>=4.2.0",
"pytest>=8.4.0",
"pytest-cov>=6.2.1",
]

[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]

[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
addopts = "-v --cov=project_context"
25 changes: 16 additions & 9 deletions src/project_context/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def main(
root: str | Path,
root: str | Path | None = None,
exclude: list[str] | None = None,
include: list[str] | None = None,
always_include: list[str] | None = None,
Expand All @@ -31,6 +31,8 @@ def main(
rendering the output. If not provided, a default template is used.
"""

if root is None:
root = Path.cwd()
root = Path(root).resolve()
if template:
template_path = Path(template)
Expand Down Expand Up @@ -64,7 +66,12 @@ def main(


@click.command("project-context")
@click.argument("root", type=click.Path(exists=True, file_okay=False, path_type=Path)) # type: ignore
@click.option(
"--root",
"-r",
type=click.Path(exists=True, file_okay=False, path_type=Path), # type: ignore
help="Root directory of the project. If not provided, the current directory is used.",
)
@click.option(
"--exclude",
"-e",
Expand Down Expand Up @@ -105,21 +112,21 @@ def main(
),
)
def cli(
root: Path,
exclude: tuple[str],
root: Path | None = None,
exclude: tuple[str] | None = None,
include: tuple[str] | None = None,
always_include: tuple[str] | None = None,
contents: tuple[str] | None = None,
output: Path | None = None,
template: Path | None = None,
):
"""Prints a tree structure of the files in the given ROOT directory."""
"""project-context generates LLM-friendly markdown files from your project contents."""
main(
root,
list(exclude) if exclude else None,
list(include) if include else None,
list(always_include) if always_include else None,
list(contents) if contents else None,
exclude=list(exclude) if exclude else None,
include=list(include) if include else None,
always_include=list(always_include) if always_include else None,
contents=list(contents) if contents else None,
output=output,
template=template,
)
Expand Down
4 changes: 3 additions & 1 deletion src/project_context/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import Counter
from pathlib import Path
from typing import Callable, Generator
from unittest.mock import patch

from .utils import is_file_tracked, is_path_gitignored

Expand Down Expand Up @@ -173,7 +174,8 @@ def __repr__(self) -> str:
)

def __str__(self) -> str:
return "\n".join(str(path) for path in self.tree)
tree_str = "\n".join(str(path) for path in self.tree)
return f"```bash\n{tree_str}\n```"

def to_markdown(
self,
Expand Down
7 changes: 3 additions & 4 deletions src/project_context/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pathlib import Path


def get_root_paths(path: Path) -> tuple[Path | None, Path | None]:
def get_root_paths(path: str | Path) -> tuple[Path | None, Path | None]:
"""Returns the root path of the Git repository and the relative path from the root.

If the path is not in a Git repository, returns (None, None).
Expand All @@ -15,6 +15,7 @@ def get_root_paths(path: Path) -> tuple[Path | None, Path | None]:
If the path is not in a Git repository, returns (None, None).
"""
try:
path = Path(path)
if not path.exists():
return None, None

Expand All @@ -39,9 +40,7 @@ def get_root_paths(path: Path) -> tuple[Path | None, Path | None]:
stderr=subprocess.DEVNULL, # Suppress error output
text=True,
).strip()
return Path(repo_root).resolve(), path.resolve().relative_to(
Path(repo_root).resolve()
)
return Path(repo_root), path.relative_to(Path(repo_root))

except (subprocess.CalledProcessError, ValueError):
return None, None
Expand Down
Loading