#!/usr/bin/env python3 """ View and analyze Claude Code logs. Usage: python3 log-viewer.py [--tail N] [--grep PATTERN] [--since DATE] [--type TYPE] """ import argparse import json import os import re import sys from datetime import datetime, timedelta from pathlib import Path from typing import List, Optional CLAUDE_DIR = Path.home() / ".claude" LOG_DIR = CLAUDE_DIR / "logs" def get_log_files(log_type: Optional[str] = None) -> List[Path]: """Get list of log files, optionally filtered by type.""" if not LOG_DIR.exists(): return [] files = list(LOG_DIR.glob("*.log")) if log_type: files = [f for f in files if log_type in f.name] return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True) def parse_log_line(line: str) -> Optional[dict]: """Parse a log line into structured data.""" # Try JSON format try: return json.loads(line) except json.JSONDecodeError: pass # Try timestamp format: [YYYY-MM-DD HH:MM:SS] message match = re.match(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.+)', line) if match: return { "timestamp": match.group(1), "message": match.group(2) } # Plain text return {"message": line.strip()} if line.strip() else None def filter_by_date(entries: List[dict], since: str) -> List[dict]: """Filter entries by date.""" try: since_date = datetime.strptime(since, "%Y-%m-%d") except ValueError: try: # Try relative (e.g., "1d", "7d", "1h") if since.endswith("d"): days = int(since[:-1]) since_date = datetime.now() - timedelta(days=days) elif since.endswith("h"): hours = int(since[:-1]) since_date = datetime.now() - timedelta(hours=hours) else: return entries except ValueError: return entries result = [] for entry in entries: ts = entry.get("timestamp", "") if ts: try: entry_date = datetime.strptime(ts[:19], "%Y-%m-%d %H:%M:%S") if entry_date >= since_date: result.append(entry) except ValueError: result.append(entry) # Include if can't parse else: result.append(entry) return result def grep_entries(entries: List[dict], pattern: str) -> List[dict]: """Filter entries by grep pattern.""" regex = re.compile(pattern, re.IGNORECASE) result = [] for entry in entries: message = entry.get("message", "") if regex.search(message): result.append(entry) return result def tail_file(path: Path, n: int = 50) -> List[str]: """Get last N lines from a file.""" try: with open(path) as f: lines = f.readlines() return lines[-n:] except Exception: return [] def format_entry(entry: dict) -> str: """Format a log entry for display.""" ts = entry.get("timestamp", "") msg = entry.get("message", "") if ts: return f"[{ts}] {msg}" return msg def list_logs(): """List available log files.""" files = get_log_files() if not files: print("No log files found.") return print(f"\nšŸ“‹ Log Files ({len(files)})\n") print(f"{'File':<40} {'Size':<10} {'Modified'}") print("-" * 70) for f in files: stat = f.stat() size = stat.st_size if size > 1024 * 1024: size_str = f"{size / 1024 / 1024:.1f}M" elif size > 1024: size_str = f"{size / 1024:.1f}K" else: size_str = f"{size}B" mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M") print(f"{f.name:<40} {size_str:<10} {mtime}") print("") def view_log(filename: str, tail: int = 50, grep: Optional[str] = None, since: Optional[str] = None): """View a specific log file.""" # Find the log file log_path = LOG_DIR / filename if not log_path.exists(): # Try with .log extension log_path = LOG_DIR / f"{filename}.log" if not log_path.exists(): print(f"Log file not found: {filename}") return lines = tail_file(log_path, tail * 2 if grep else tail) # Get more if filtering entries = [] for line in lines: entry = parse_log_line(line) if entry: entries.append(entry) if since: entries = filter_by_date(entries, since) if grep: entries = grep_entries(entries, grep) # Limit to tail after filtering entries = entries[-tail:] print(f"\nšŸ“œ {log_path.name} (last {len(entries)} entries)\n") print("-" * 70) for entry in entries: print(format_entry(entry)) print("") def main(): parser = argparse.ArgumentParser(description="View Claude Code logs") parser.add_argument("file", nargs="?", help="Log file to view") parser.add_argument("--list", "-l", action="store_true", help="List log files") parser.add_argument("--tail", "-n", type=int, default=50, help="Number of lines to show (default: 50)") parser.add_argument("--grep", "-g", type=str, help="Filter by pattern") parser.add_argument("--since", "-s", type=str, help="Show entries since date (YYYY-MM-DD or 1d/7d/1h)") parser.add_argument("--type", "-t", type=str, help="Filter log files by type (maintenance, etc.)") args = parser.parse_args() if args.list: list_logs() elif args.file: view_log(args.file, tail=args.tail, grep=args.grep, since=args.since) else: # Default: show most recent log files = get_log_files(args.type) if files: view_log(files[0].name, tail=args.tail, grep=args.grep, since=args.since) else: print("No log files found. Run some automation scripts first.") if __name__ == "__main__": main()