#!/usr/bin/env python3 """ List recent Claude Code sessions with descriptive information. Usage: python list-sessions.py [--project-dir ] [--limit ] """ import argparse import json import os import sys from datetime import datetime from pathlib import Path def get_project_sessions_dir(project_dir: Path) -> Path: """Convert project directory to Claude sessions directory name.""" # Claude converts paths like /Users/foo/bar to -Users-foo-bar normalized = str(project_dir.resolve()).replace("/", "-") return Path.home() / ".claude" / "projects" / normalized def parse_jsonl_metadata(jsonl_path: Path) -> dict: """Extract metadata from a JSONL session file.""" try: with open(jsonl_path, "r") as f: lines = f.readlines() # Parse first line for session metadata first_line = json.loads(lines[0]) # Find first user message with actual content first_user_msg = "No messages" for line in lines[:10]: # Check first 10 lines data = json.loads(line) if data.get("type") == "user": content = data.get("message", {}).get("content", "") # Handle both string and array content formats if isinstance(content, str): msg_text = content elif isinstance(content, list): # Extract text from array format text_parts = [ item.get("text", "") for item in content if isinstance(item, dict) and item.get("type") == "text" ] msg_text = " ".join(text_parts) else: msg_text = str(content) # Clean up IDE noise if "" in msg_text: msg_text = msg_text.split("")[-1].strip() if "" in msg_text: msg_text = msg_text.split("")[-1].strip() # Skip empty or warmup messages if msg_text and msg_text.strip() and msg_text not in ["Warmup", ""]: first_user_msg = msg_text.strip() break # Truncate long messages if len(first_user_msg) > 80: first_user_msg = first_user_msg[:77] + "..." # Count total messages message_count = sum( 1 for line in lines if json.loads(line).get("type") in ["user", "assistant"] ) # Get timestamp and format it timestamp = first_line.get("timestamp", "") if timestamp: dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) formatted_time = dt.strftime("%b %d %H:%M") else: formatted_time = "Unknown" # Get git branch git_branch = first_line.get("gitBranch", "unknown") # Get file modification time as fallback mtime = datetime.fromtimestamp(jsonl_path.stat().st_mtime) return { "path": jsonl_path, "first_message": first_user_msg, "timestamp": formatted_time, "mtime": mtime, "message_count": message_count, "git_branch": git_branch, } except Exception as e: return { "path": jsonl_path, "first_message": f"Error reading file: {e}", "timestamp": "Unknown", "mtime": datetime.fromtimestamp(jsonl_path.stat().st_mtime), "message_count": 0, "git_branch": "unknown", } def main(): parser = argparse.ArgumentParser( description="List recent Claude Code sessions with descriptive information." ) parser.add_argument( "--project-dir", type=Path, default=Path.cwd(), help="Project directory (default: current working directory)", ) parser.add_argument( "--limit", type=int, default=5, help="Number of recent sessions to show (default: 5)", ) parser.add_argument( "--format", choices=["human", "json"], default="human", help="Output format (default: human)", ) args = parser.parse_args() # Find sessions directory sessions_dir = get_project_sessions_dir(args.project_dir) if not sessions_dir.exists(): print(f"No sessions found for project: {args.project_dir}", file=sys.stderr) print(f"Expected sessions directory: {sessions_dir}", file=sys.stderr) sys.exit(1) # Find all JSONL files jsonl_files = list(sessions_dir.glob("*.jsonl")) if not jsonl_files: print(f"No session files found in: {sessions_dir}", file=sys.stderr) sys.exit(1) # Parse metadata for all files sessions = [parse_jsonl_metadata(f) for f in jsonl_files] # Sort by modification time (most recent first) sessions.sort(key=lambda s: s["mtime"], reverse=True) # Limit results sessions = sessions[: args.limit] # Output if args.format == "json": output = [ { "index": i + 1, "path": str(s["path"]), "first_message": s["first_message"], "timestamp": s["timestamp"], "message_count": s["message_count"], "git_branch": s["git_branch"], } for i, s in enumerate(sessions) ] print(json.dumps(output, indent=2)) else: print("\nRecent sessions:") print("=" * 80) for i, s in enumerate(sessions, 1): print( f"{i}. [{s['timestamp']}] \"{s['first_message']}\" " f"({s['message_count']} messages, branch: {s['git_branch']})" ) print(f" Path: {s['path']}") print() if __name__ == "__main__": main()