Download plugins/code_stats.py from elyanlabs/trashclaw: direct link, hf CLI and curl.
- Browser
- Download file 5.8 kB
-
https://huggingface.co/elyanlabs/trashclaw/resolve/main/plugins/code_stats.py
- Command line
-
hf download hf://elyanlabs/trashclaw/plugins/code_stats.py
-
curl -L -o code_stats.py https://huggingface.co/elyanlabs/trashclaw/resolve/main/plugins/code_stats.py
5.8 kB
| # SPDX-License-Identifier: MIT | |
| """ | |
| Code Stats Plugin for TrashClaw | |
| Analyze a codebase directory and return statistics: | |
| lines of code, file counts by language, largest files, | |
| TODO/FIXME counts, and complexity indicators. | |
| Useful for understanding a project before diving in. | |
| Zero external dependencies — pure stdlib. | |
| Plugin contract: | |
| TOOL_DEF = dict with name, description, parameters (OpenAI function schema) | |
| run(**kwargs) -> str (called with the parsed arguments) | |
| """ | |
| import os | |
| import re | |
| from collections import defaultdict | |
| TOOL_DEF = { | |
| "name": "code_stats", | |
| "description": "Analyze a codebase directory: lines of code by language, file counts, largest files, TODO/FIXME counts. Useful for understanding a project's scope before working on it.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "path": { | |
| "type": "string", | |
| "description": "Directory to analyze (default: current directory)" | |
| }, | |
| "top_n": { | |
| "type": "integer", | |
| "description": "Number of largest files to show (default: 10)" | |
| } | |
| }, | |
| "required": [] | |
| } | |
| } | |
| # Extension -> language mapping | |
| LANG_MAP = { | |
| ".py": "Python", ".js": "JavaScript", ".ts": "TypeScript", | |
| ".tsx": "TypeScript (JSX)", ".jsx": "JavaScript (JSX)", | |
| ".rs": "Rust", ".go": "Go", ".rb": "Ruby", ".java": "Java", | |
| ".c": "C", ".h": "C Header", ".cpp": "C++", ".hpp": "C++ Header", | |
| ".cs": "C#", ".php": "PHP", ".swift": "Swift", ".kt": "Kotlin", | |
| ".lua": "Lua", ".sh": "Shell", ".bash": "Shell", ".zsh": "Shell", | |
| ".sql": "SQL", ".html": "HTML", ".css": "CSS", ".scss": "SCSS", | |
| ".md": "Markdown", ".json": "JSON", ".yaml": "YAML", ".yml": "YAML", | |
| ".toml": "TOML", ".xml": "XML", ".r": "R", ".jl": "Julia", | |
| ".pl": "Perl", ".ex": "Elixir", ".exs": "Elixir", | |
| ".zig": "Zig", ".nim": "Nim", ".v": "V", ".dart": "Dart", | |
| } | |
| SKIP_DIRS = { | |
| ".git", "node_modules", "__pycache__", ".venv", "venv", | |
| "target", "build", "dist", ".next", ".cache", "vendor", | |
| ".tox", ".mypy_cache", ".pytest_cache", "coverage", | |
| } | |
| SKIP_EXTENSIONS = {".min.js", ".min.css", ".map", ".lock"} | |
| def _is_binary(filepath, sample_size=8192): | |
| """Quick check if a file is binary.""" | |
| try: | |
| with open(filepath, "rb") as f: | |
| chunk = f.read(sample_size) | |
| if b"\x00" in chunk: | |
| return True | |
| except Exception: | |
| return True | |
| return False | |
| def run(path: str = ".", top_n: int = 10, **kwargs) -> str: | |
| """Analyze codebase and return formatted statistics.""" | |
| path = os.path.expanduser(path) | |
| if not os.path.isabs(path): | |
| path = os.path.abspath(path) | |
| if not os.path.isdir(path): | |
| return f"Error: Not a directory: {path}" | |
| lang_lines = defaultdict(int) | |
| lang_files = defaultdict(int) | |
| file_sizes = [] # (lines, path) | |
| total_files = 0 | |
| total_lines = 0 | |
| todo_count = 0 | |
| fixme_count = 0 | |
| hack_count = 0 | |
| todo_pattern = re.compile(r"\bTODO\b", re.IGNORECASE) | |
| fixme_pattern = re.compile(r"\bFIXME\b", re.IGNORECASE) | |
| hack_pattern = re.compile(r"\bHACK\b", re.IGNORECASE) | |
| for root, dirs, files in os.walk(path): | |
| # Skip hidden and vendor dirs | |
| dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")] | |
| for fname in files: | |
| filepath = os.path.join(root, fname) | |
| _, ext = os.path.splitext(fname) | |
| if ext in SKIP_EXTENSIONS or any( | |
| fname.endswith(skip_ext) for skip_ext in SKIP_EXTENSIONS | |
| ): | |
| continue | |
| lang = LANG_MAP.get(ext) | |
| if not lang: | |
| continue | |
| if _is_binary(filepath): | |
| continue | |
| try: | |
| with open(filepath, "r", errors="replace") as f: | |
| lines = f.readlines() | |
| except Exception: | |
| continue | |
| line_count = len(lines) | |
| total_files += 1 | |
| total_lines += line_count | |
| lang_lines[lang] += line_count | |
| lang_files[lang] += 1 | |
| rel_path = os.path.relpath(filepath, path) | |
| file_sizes.append((line_count, rel_path)) | |
| # Count markers | |
| for line in lines: | |
| if todo_pattern.search(line): | |
| todo_count += 1 | |
| if fixme_pattern.search(line): | |
| fixme_count += 1 | |
| if hack_pattern.search(line): | |
| hack_count += 1 | |
| if total_files == 0: | |
| return f"No recognized source files found in {path}" | |
| # Build output | |
| out = [] | |
| out.append(f"=== Code Stats: {os.path.basename(path)} ===\n") | |
| out.append(f"Total: {total_files} files, {total_lines:,} lines\n") | |
| # Language breakdown (sorted by lines) | |
| out.append("Language Breakdown:") | |
| sorted_langs = sorted(lang_lines.items(), key=lambda x: -x[1]) | |
| for lang, lines in sorted_langs: | |
| pct = (lines / total_lines) * 100 if total_lines else 0 | |
| bar = "█" * int(pct / 3) | |
| out.append(f" {lang:<20} {lines:>8,} lines ({lang_files[lang]} files) {bar} {pct:.1f}%") | |
| # Largest files | |
| out.append(f"\nTop {top_n} Largest Files:") | |
| file_sizes.sort(key=lambda x: -x[0]) | |
| for lines, fpath in file_sizes[:top_n]: | |
| out.append(f" {lines:>6,} lines {fpath}") | |
| # Markers | |
| markers_total = todo_count + fixme_count + hack_count | |
| if markers_total > 0: | |
| out.append(f"\nCode Markers:") | |
| if todo_count: | |
| out.append(f" TODO: {todo_count}") | |
| if fixme_count: | |
| out.append(f" FIXME: {fixme_count}") | |
| if hack_count: | |
| out.append(f" HACK: {hack_count}") | |
| return "\n".join(out) | |