#!/usr/bin/env python3 """ dev_session_doc.py – Dokumentiert die zuletzt abgeschlossene Timetrack-Session in DEVLOG.md. Wird automatisch vom Claude Code Stop-Hook aufgerufen. """ import json import os import subprocess from datetime import datetime, timezone BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_FILE = os.path.expanduser("~/.claude/timetrack.json") DEVLOG = os.path.join(BASE_DIR, "DEVLOG.md") BACKEND_DIR = os.path.join(BASE_DIR, "backend") # ── Helpers ─────────────────────────────────────────────────────────────────── def parse_iso(s: str) -> datetime: if s.endswith("Z"): s = s[:-1] + "+00:00" return datetime.fromisoformat(s) def format_duration(seconds: float) -> str: seconds = int(seconds) h = seconds // 3600 m = (seconds % 3600) // 60 if h > 0: return f"{h}h {m:02d}m" return f"{m}m" def local_display(dt: datetime) -> str: return dt.astimezone().strftime("%H:%M") def local_date(dt: datetime) -> str: return dt.astimezone().strftime("%Y-%m-%d") def run(cmd: list[str]) -> str: """Run a command and return stdout; return '' on any error.""" try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=15, ) return result.stdout.strip() except Exception: return "" # ── Core logic ──────────────────────────────────────────────────────────────── def get_last_completed_session(data: dict) -> dict | None: """Return the most recently *completed* session (end != None).""" completed = [s for s in data.get("sessions", []) if s.get("end") is not None] if not completed: return None return max(completed, key=lambda s: s["end"]) def get_commits(start_iso: str, end_iso: str) -> list[str]: """Return list of 'HASH message' strings for commits within the session window.""" start_dt = parse_iso(start_iso) end_dt = parse_iso(end_iso) # git log uses local times via --after/--before; pass UTC explicitly as ISO after = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") before = end_dt.strftime("%Y-%m-%dT%H:%M:%SZ") raw = run([ "git", "-C", BACKEND_DIR, "log", "--oneline", f"--after={after}", f"--before={before}", ]) if not raw: return [] return [line for line in raw.splitlines() if line.strip()] def get_diff_stat() -> list[str]: """Return lines from git diff --stat HEAD~1 HEAD.""" raw = run([ "git", "-C", BACKEND_DIR, "diff", "--stat", "HEAD~1", "HEAD", ]) if not raw: return [] lines = raw.splitlines() # Drop the summary line (last line with "N files changed …") return [l for l in lines if l.strip() and " | " in l] def build_entry(session: dict) -> str: start_dt = parse_iso(session["start"]) end_dt = parse_iso(session["end"]) elapsed = (end_dt - start_dt).total_seconds() duration = format_duration(elapsed) date_str = local_date(start_dt) start_str = local_display(start_dt) end_str = local_display(end_dt) desc = session.get("description") or "Claude Code Session" commits = get_commits(session["start"], session["end"]) diff_stat = get_diff_stat() lines: list[str] = [] lines.append(f"## {date_str} {start_str} – {end_str} ({duration})") lines.append(f"**Beschreibung:** {desc}") lines.append("") # Commits section lines.append("### Commits") if commits: for c in commits: lines.append(f"- {c}") else: lines.append("Keine Commits in dieser Session.") lines.append("") # Changed files section lines.append("### Geänderte Dateien") if diff_stat: for l in diff_stat: # Normalize: " path/file | 5 ++-" → "- path/file (+5/-2)" # Keep it simple and just relay the stat line lines.append(f"- {l.strip()}") else: lines.append("Keine Änderungen ermittelbar.") lines.append("") lines.append("---") lines.append("") return "\n".join(lines) def append_to_devlog(entry: str) -> None: # Ensure file exists with a header if not os.path.exists(DEVLOG): with open(DEVLOG, "w", encoding="utf-8") as f: f.write("# TimeMaster – Dev Log\n\n") with open(DEVLOG, "a", encoding="utf-8") as f: f.write(entry) # ── Entry point ─────────────────────────────────────────────────────────────── def main() -> None: if not os.path.exists(DATA_FILE): return # Nothing to document with open(DATA_FILE, "r", encoding="utf-8") as f: data = json.load(f) session = get_last_completed_session(data) if not session: return entry = build_entry(session) append_to_devlog(entry) if __name__ == "__main__": try: main() except Exception: # Never crash Claude Code pass