Files
fegger b0229b6332 Add todoObsidian tool for syncing TODOs to Obsidian
Scans codebases for TODO/FIXME/HACK/XXX comments and generates Markdown
notes in an Obsidian vault. Includes Zed editor integration that runs
on file save, plus a shell wrapper for CLI usage. Supports customizable
vault paths, project names, and output directories.
2026-05-26 14:38:26 +02:00

117 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()