#!/usr/bin/env python3 """Obsidian TODO Scanner Scan a project directory for TODO / FIXME / HACK / XXX comments and emit a Markdown note into an Obsidian vault. Usage: python main.py /path/to/project python main.py /path/to/project --vault ~/Obsidian/MyVault python main.py /path/to/project --name "My Project" """ import argparse import sys from datetime import datetime from pathlib import Path from typing import List from utils.config import ensure_unique_path, get_vault_path, sanitize_filename from utils.scanner import TodoItem, count_tags, group_by_file, scan_directory def generate_markdown(project_name: str, todos: List[TodoItem]) -> str: lines = [] lines.append(f"# TODOs – {project_name}") lines.append("") lines.append(f"_Scanned on {datetime.now().strftime('%Y-%m-%d %H:%M')}_") lines.append("") if not todos: lines.append("No TODO comments found. 🎉") return "\n".join(lines) tag_counts = count_tags(todos) lines.append("## Summary") for tag, count in sorted(tag_counts.items()): lines.append(f"- **{tag}**: {count}") lines.append("") groups = group_by_file(todos) lines.append("## By File") lines.append("") for file_path, items in sorted(groups.items(), key=lambda x: str(x[0])): lines.append(f"### `{file_path}`") for item in items: # Obsidian checkbox + link to line in backticks lines.append(f"- [ ] **{item.tag}** (line {item.line}) – {item.text}") lines.append("") return "\n".join(lines) def main(): parser = argparse.ArgumentParser( description="Scan a project for TODOs and write them to an Obsidian vault." ) parser.add_argument("project", help="Path to the project directory to scan.") parser.add_argument( "--vault", dest="vault", default=None, help="Path to the Obsidian vault. Overrides OBSIDIAN_VAULT env var.", ) parser.add_argument( "--name", dest="name", default=None, help="Project name for the note title. Defaults to the directory name.", ) parser.add_argument( "--output-dir", dest="output_dir", default="Todos", help="Subdirectory inside the vault where the note is created (default: Todos).", ) args = parser.parse_args() project_path = Path(args.project).expanduser().resolve() if not project_path.exists(): print(f"Error: project path does not exist: {project_path}", file=sys.stderr) sys.exit(1) if not project_path.is_dir(): print(f"Error: not a directory: {project_path}", file=sys.stderr) sys.exit(1) try: vault_path = get_vault_path(args.vault) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) project_name = args.name or project_path.name print(f"🔍 Scanning {project_path} ...") todos = scan_directory(project_path) print(f" Found {len(todos)} item(s).") md_content = generate_markdown(project_name, todos) note_dir = vault_path / args.output_dir note_dir.mkdir(parents=True, exist_ok=True) safe_name = sanitize_filename(f"TODOs {project_name}") note_path = note_dir / f"{safe_name}.md" note_path = ensure_unique_path(note_path) try: note_path.write_text(md_content, encoding="utf-8") print(f"✅ Note written to: {note_path}") except OSError as e: print(f"Error writing note: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()