Skip to content

Commit 3e871ac

Browse files
initial upload
0 parents  commit 3e871ac

9 files changed

Lines changed: 364 additions & 0 deletions

File tree

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
annotator.egg-info/
9+
build/
10+
11+
# Virtual environments
12+
.venv

LICENSE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Gourab
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
18+
THE SOFTWARE.

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Annotator
2+
3+
**Annotator** is a simple CLI utility that prepends relative file paths as comments to your project files as the first line.
4+
It’s designed to make AI debugging easier by automatically marking file paths for better context and easier copy-pasting.
5+
6+
---
7+
8+
## ✨ Features
9+
10+
- 🚀 **Automatically annotates** files with their relative paths.
11+
- 🛠️ **Supports many programming languages and file types** out of the box.
12+
- ⚙️ **Fully customizable** via a `.annotator.json` configuration file.
13+
- 🚫 **Exclude** specific directories, files, or extensions.
14+
-**Lightweight and fast** — no external dependencies.
15+
16+
---
17+
18+
## 📝 Example `.annotator.json`
19+
20+
```json
21+
{
22+
"comment_styles": {
23+
".md": "<!--",
24+
"Dockerfile": "#"
25+
},
26+
"exclude_extensions": ["*.test.js", "*.spec.ts"],
27+
"exclude_dirs": ["test", "dist"],
28+
"exclude_files": [".env"]
29+
}
30+
```
31+
32+
<details>
33+
<summary><strong>Configuration Notes</strong></summary>
34+
35+
- **comment_styles**: Maps file extensions (from the first `.` to the end) or exact filenames to their comment syntax.
36+
_Example:_ `.js``//`, `.md``<!--`. You can add any extension to annotate non-default types.
37+
38+
- **exclude_extensions**: Skips all files with these full extensions (from the first `.`).
39+
_Note:_ Even if an extension is mapped in `comment_styles`, files matching an excluded extension will not be annotated.
40+
41+
- **exclude_dirs**: Skips these directories entirely.
42+
_Note:_ Any file inside an excluded directory is never annotated, even if its extension is mapped in `comment_styles`.
43+
44+
- **exclude_files**: Skips specific full filenames (with extensions) entirely.
45+
_Note:_ These take precedence over both `comment_styles` and `exclude_extensions`.
46+
47+
- **Extension parsing**: Extensions are always taken from the first `.` to the end of the filename.
48+
_Example:_ For a file named `file.a.b.c.js`, the full extension is `.a.b.c.js`.
49+
50+
</details>
51+
52+
---
53+
54+
## Installation
55+
56+
### 1. Install pipx
57+
58+
Follow the official guide: [pipx Installation Guide](https://pipxproject.github.io/pipx/installation/)
59+
60+
### 2. Install annotator-cli via [pipx](https://pipx.pypa.io/) (recommended):
61+
62+
```sh
63+
pipx install annotator-cli
64+
```
65+
66+
---
67+
68+
## 🚀 Usage
69+
70+
### 1. Run the CLI
71+
72+
```sh
73+
annotator /path/to/project
74+
```
75+
76+
Annotator will process and annotate all eligible files based on your configuration.
77+
78+
### 2. Uninstall (if needed)
79+
80+
```sh
81+
pipx uninstall annotator-cli
82+
```
83+
84+
---
85+
86+
## ⚠️ Disclaimer
87+
88+
> This software is provided **as-is**, without warranty of any kind.
89+
> Use at your own risk — the author is not responsible for data loss, crashes, or security issues.
90+
91+
---

annotator/__init__.py

Whitespace-only changes.

annotator/cli.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import argparse
2+
import json
3+
import os
4+
import sys
5+
from .core import annotate_project
6+
from .defaults import DEFAULT_COMMENT_STYLES, DEFAULT_EXCLUDE_DIRS
7+
8+
CONFIG_FILE = ".annotator.json"
9+
10+
11+
def load_config(root):
12+
config_path = os.path.join(root, CONFIG_FILE)
13+
if not os.path.exists(config_path):
14+
return {}
15+
try:
16+
with open(config_path, "r", encoding="utf-8") as f:
17+
return json.load(f)
18+
except json.JSONDecodeError as e:
19+
print(f"[ERROR] Invalid JSON in {CONFIG_FILE}: {e}", file=sys.stderr)
20+
return {}
21+
except Exception as e:
22+
print(f"[ERROR] Could not read {CONFIG_FILE}: {e}", file=sys.stderr)
23+
return {}
24+
25+
26+
def main():
27+
parser = argparse.ArgumentParser(description="Annotate files with relative paths.")
28+
parser.add_argument("path", nargs="?", default=".", help="Project root (default: current dir)")
29+
args = parser.parse_args()
30+
31+
root = os.path.abspath(args.path)
32+
if not os.path.exists(root):
33+
print(f"[ERROR] Path does not exist: {root}", file=sys.stderr)
34+
sys.exit(1)
35+
if not os.path.isdir(root):
36+
print(f"[ERROR] Path is not a directory: {root}", file=sys.stderr)
37+
sys.exit(1)
38+
39+
config = load_config(root)
40+
41+
valid_keys = {"comment_styles", "exclude_extensions", "exclude_dirs", "exclude_files"}
42+
for key in config.keys():
43+
if key not in valid_keys:
44+
print(f"[WARN] Unknown key in {CONFIG_FILE}: '{key}'", file=sys.stderr)
45+
46+
comment_styles = DEFAULT_COMMENT_STYLES.copy()
47+
if "comment_styles" in config and isinstance(config["comment_styles"], dict):
48+
comment_styles.update(config["comment_styles"])
49+
50+
exclude_exts = set(config.get("exclude_extensions", []))
51+
exclude_dirs = set(config.get("exclude_dirs", []))
52+
exclude_files = set(config.get("exclude_files", []))
53+
54+
print(f"[INFO] Annotating project at {root}")
55+
annotate_project(
56+
root,
57+
comment_styles,
58+
exclude_exts,
59+
exclude_dirs,
60+
exclude_files
61+
)
62+
print("[INFO] Done.")

annotator/core.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
from .defaults import DEFAULT_COMMENT_STYLES, DEFAULT_EXCLUDE_DIRS
3+
4+
5+
def get_full_extension(filename: str) -> str:
6+
"""Return everything after the first dot, e.g., 'unit.test.js' -> '.test.js'."""
7+
if "." not in filename:
8+
return ""
9+
return filename[filename.index("."):] # keeps the dot
10+
11+
12+
def is_excluded_dir(filepath: str, root: str, exclude_dirs: set) -> bool:
13+
"""Check if file lives in an excluded directory (relative to root)."""
14+
rel_path = os.path.relpath(filepath, root)
15+
parts = rel_path.split(os.sep)
16+
return any(part in exclude_dirs for part in parts)
17+
18+
19+
def is_excluded_file(filename: str, exclude_files: set) -> bool:
20+
"""Check if filename matches exactly one of the excluded files."""
21+
return filename in exclude_files
22+
23+
24+
def is_excluded_ext(filename: str, exclude_exts: set) -> bool:
25+
"""Check if full extension matches excluded extensions."""
26+
ext = get_full_extension(filename)
27+
return ext in exclude_exts
28+
29+
30+
def annotate_file(root, filepath, comment_styles):
31+
rel_path = os.path.relpath(filepath, root)
32+
ext = get_full_extension(os.path.basename(filepath))
33+
34+
prefix = comment_styles.get(ext)
35+
if not prefix:
36+
return
37+
38+
annotation = f"{prefix} {rel_path}\n"
39+
if prefix in ["<!--", "/*"]:
40+
annotation = f"{prefix} {rel_path} {'-->' if prefix == '<!--' else '*/'}\n"
41+
42+
try:
43+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
44+
lines = f.readlines()
45+
except Exception as e:
46+
print(f"[WARN] Skipping {filepath}: {e}")
47+
return
48+
49+
if lines and rel_path in lines[0]:
50+
return
51+
52+
new_content = [annotation] + lines
53+
try:
54+
with open(filepath, "w", encoding="utf-8") as f:
55+
f.writelines(new_content)
56+
print(f"[OK] Annotated {rel_path}")
57+
except Exception as e:
58+
print(f"[ERROR] Failed to write {filepath}: {e}")
59+
60+
61+
def annotate_project(root=".", comment_styles=None,
62+
exclude_exts=None, exclude_dirs=None, exclude_files=None):
63+
if comment_styles is None:
64+
comment_styles = DEFAULT_COMMENT_STYLES
65+
if exclude_exts is None:
66+
exclude_exts = set()
67+
if exclude_dirs is None:
68+
exclude_dirs = set()
69+
if exclude_files is None:
70+
exclude_files = set()
71+
72+
effective_exclude_dirs = set(DEFAULT_EXCLUDE_DIRS) | set(exclude_dirs)
73+
74+
for subdir, dirs, files in os.walk(root):
75+
# skip excluded dirs at traversal level
76+
dirs[:] = [d for d in dirs if d not in effective_exclude_dirs]
77+
78+
for file in files:
79+
filepath = os.path.join(subdir, file)
80+
81+
# precedence rules
82+
if is_excluded_dir(filepath, root, effective_exclude_dirs):
83+
continue
84+
if is_excluded_file(file, exclude_files):
85+
continue
86+
if is_excluded_ext(file, exclude_exts):
87+
continue
88+
89+
annotate_file(root, filepath, comment_styles)

annotator/defaults.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# annotator/defaults.py
2+
3+
DEFAULT_COMMENT_STYLES = {
4+
".py": "#",
5+
".js": "//",
6+
".ts": "//",
7+
".jsx": "//",
8+
".tsx": "//",
9+
".java": "//",
10+
".c": "//",
11+
".cpp": "//",
12+
".h": "//",
13+
".hpp": "//",
14+
".php": "//",
15+
".swift": "//",
16+
".kt": "//",
17+
".kts": "//",
18+
".html": "<!--",
19+
".xml": "<!--",
20+
".css": "/*",
21+
".scss": "/*",
22+
".less": "/*",
23+
".sh": "#",
24+
".rb": "#",
25+
".go": "//",
26+
".rs": "//",
27+
".pl": "#",
28+
".pm": "#",
29+
".lua": "--",
30+
".sql": "--",
31+
".scala": "//",
32+
".dart": "//",
33+
".m": "%",
34+
".r": "#"
35+
}
36+
37+
# Common dependency/build/virtual env directories we should skip
38+
DEFAULT_EXCLUDE_DIRS = {
39+
"node_modules",
40+
".venv",
41+
"venv",
42+
"__pycache__",
43+
"dist",
44+
"build",
45+
".mypy_cache",
46+
".pytest_cache",
47+
".tox",
48+
".git",
49+
".hg",
50+
".svn",
51+
"target", # Java/Maven/Gradle, Rust
52+
"out", # C++/Java builds
53+
"bin", # Compiled bins
54+
"obj", # Compiled objs
55+
".idea", # JetBrains IDEs
56+
".vscode", # VSCode settings
57+
".DS_Store", # macOS Finder metadata
58+
".cache", # General cache
59+
".gradle", # Gradle cache
60+
".settings", # Eclipse settings
61+
".history", # Some editors
62+
".coverage", # Coverage.py
63+
".env", # Environment files
64+
".eggs", # Python eggs
65+
".bundle", # Ruby bundle
66+
"log", # Log files
67+
"logs", # Log files
68+
".sass-cache" # Sass cache
69+
}

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[project]
2+
name = "annotator-cli"
3+
version = "0.1.0"
4+
description = "Annotates source files with their relative paths for easier AI/debug context"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = []
8+
9+
# CLI entry point: after install, users can run `annotator`
10+
[project.scripts]
11+
annotator = "annotator.cli:main"
12+
13+
[build-system]
14+
requires = ["setuptools>=61.0"]
15+
build-backend = "setuptools.build_meta"

uv.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)