Skip to main content

scripts-session-index-generator

#!/usr/bin/env python3 """

title: "Session Index Generator" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Claude Code Session Index Generator" keywords: ['generator', 'index', 'session'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "session-index-generator.py" language: python executable: true usage: "python3 scripts/session-index-generator.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false

Claude Code Session Index Generator

Scans ~/.claude/projects directory and generates a comprehensive markdown index of all Claude Code sessions and agent executions, organized by project.

Features:

  • Markdown table format with creation and modification timestamps
  • File size tracking
  • Clickable file:// links
  • Project-based organization
  • Optional JSON output
  • Filtering by project path

Usage: python session-index-generator.py [options]

Options: -o, --output PATH Output file path (default: ./CLAUDE-SESSION-INDEX.md) -j, --json Also output JSON format -p, --project PATTERN Filter to projects matching pattern -v, --verbose Verbose output """ import os import sys import json import argparse from pathlib import Path from collections import defaultdict from datetime import datetime

def parse_project_path(path_str): """Convert encoded project path to human-readable format""" # Remove the base path and decode parts = path_str.replace('/path/to/user/.claude/projects/', '') parts = parts.replace('-Users-halcasteel-', '/') parts = parts.replace('-Users-halcasteel', '') parts = parts.replace('-', '/') return parts

def get_file_metadata(filepath): """Get file metadata like size, creation time, and modification time""" try: stat = os.stat(filepath) return { 'size': stat.st_size, 'created': datetime.fromtimestamp(stat.st_birthtime if hasattr(stat, 'st_birthtime') else stat.st_ctime), 'modified': datetime.fromtimestamp(stat.st_mtime) } except Exception as e: return None

def scan_sessions(base_path, project_filter=None, verbose=False): """Scan all session and agent files""" base_path = Path(base_path)

if not base_path.exists():
print(f"Error: Path {base_path} does not exist", file=sys.stderr)
return None

# Organize sessions by project
projects = defaultdict(lambda: {'sessions': [], 'agents': []})

if verbose:
print(f"Scanning {base_path}...")

# Walk through all JSONL files
file_count = 0
for jsonl_file in base_path.rglob('*.jsonl'):
file_count += 1
if verbose and file_count % 50 == 0:
print(f" Processed {file_count} files...")

# Get project directory
project_dir = jsonl_file.parent.name
filename = jsonl_file.name

# Apply project filter if specified
if project_filter and project_filter.lower() not in project_dir.lower():
continue

# Get metadata
metadata = get_file_metadata(jsonl_file)

# Categorize as agent or session
if filename.startswith('agent-'):
projects[project_dir]['agents'].append({
'file': filename,
'path': str(jsonl_file),
'metadata': metadata
})
else:
projects[project_dir]['sessions'].append({
'file': filename,
'path': str(jsonl_file),
'metadata': metadata
})

if verbose:
print(f"Scan complete. Found {file_count} files in {len(projects)} projects.")

return projects

def generate_markdown(projects, verbose=False): """Generate markdown index from project data""" # Sort projects alphabetically sorted_projects = sorted(projects.items())

# Generate markdown
md_lines = []
md_lines.append("# Claude Code Session Index")
md_lines.append("")
md_lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
md_lines.append("")

# Summary statistics
total_sessions = sum(len(p[1]['sessions']) for p in sorted_projects)
total_agents = sum(len(p[1]['agents']) for p in sorted_projects)
total_projects = len(sorted_projects)

md_lines.append("## Summary")
md_lines.append("")
md_lines.append(f"- **Total Projects:** {total_projects}")
md_lines.append(f"- **Total Sessions:** {total_sessions}")
md_lines.append(f"- **Total Agent Executions:** {total_agents}")
md_lines.append(f"- **Total Files:** {total_sessions + total_agents}")
md_lines.append("")

# Table of contents
md_lines.append("## Table of Contents")
md_lines.append("")
for project_dir, data in sorted_projects:
readable_path = parse_project_path(project_dir)
anchor = readable_path.replace('/', '-').replace('~', '').replace(' ', '-').lower()
session_count = len(data['sessions'])
agent_count = len(data['agents'])
md_lines.append(f"- [{readable_path}](#{anchor}) ({session_count} sessions, {agent_count} agents)")
md_lines.append("")

# Detailed project sections
md_lines.append("---")
md_lines.append("")
md_lines.append("## Projects")
md_lines.append("")

for project_dir, data in sorted_projects:
readable_path = parse_project_path(project_dir)

md_lines.append(f"### {readable_path}")
md_lines.append("")
md_lines.append(f"**Location:** `{project_dir}`")
md_lines.append("")

# Sessions
if data['sessions']:
md_lines.append(f"#### Sessions ({len(data['sessions'])})")
md_lines.append("")

# Sort by modification time (most recent first)
sorted_sessions = sorted(
data['sessions'],
key=lambda x: x['metadata']['modified'] if x['metadata'] else datetime.min,
reverse=True
)

# Table header
md_lines.append("| Session ID | Created | Last Updated | Size |")
md_lines.append("|------------|---------|--------------|------|")

for session in sorted_sessions:
file_path = session['path']
filename = session['file']
uuid = filename.replace('.jsonl', '')

if session['metadata']:
size_kb = session['metadata']['size'] / 1024
created = session['metadata']['created'].strftime('%Y-%m-%d %H:%M')
modified = session['metadata']['modified'].strftime('%Y-%m-%d %H:%M')
md_lines.append(f"| [{uuid}](file://{file_path}) | {created} | {modified} | {size_kb:.1f} KB |")
else:
md_lines.append(f"| [{uuid}](file://{file_path}) | - | - | - |")

md_lines.append("")

# Agent executions
if data['agents']:
md_lines.append(f"#### Agent Executions ({len(data['agents'])})")
md_lines.append("")

# Sort by modification time (most recent first)
sorted_agents = sorted(
data['agents'],
key=lambda x: x['metadata']['modified'] if x['metadata'] else datetime.min,
reverse=True
)

# Table header
md_lines.append("| Agent ID | Created | Last Updated | Size |")
md_lines.append("|----------|---------|--------------|------|")

for agent in sorted_agents:
file_path = agent['path']
filename = agent['file']
agent_id = filename.replace('agent-', '').replace('.jsonl', '')

if agent['metadata']:
size_kb = agent['metadata']['size'] / 1024
created = agent['metadata']['created'].strftime('%Y-%m-%d %H:%M')
modified = agent['metadata']['modified'].strftime('%Y-%m-%d %H:%M')
md_lines.append(f"| [agent-{agent_id}](file://{file_path}) | {created} | {modified} | {size_kb:.1f} KB |")
else:
md_lines.append(f"| [agent-{agent_id}](file://{file_path}) | - | - | - |")

md_lines.append("")

md_lines.append("---")
md_lines.append("")

return '\n'.join(md_lines)

def generate_json(projects): """Generate JSON output from project data""" json_data = { 'generated': datetime.now().isoformat(), 'projects': [] }

for project_dir, data in sorted(projects.items()):
project_data = {
'name': parse_project_path(project_dir),
'location': project_dir,
'sessions': [],
'agents': []
}

# Add sessions
for session in data['sessions']:
session_data = {
'id': session['file'].replace('.jsonl', ''),
'path': session['path']
}
if session['metadata']:
session_data.update({
'created': session['metadata']['created'].isoformat(),
'modified': session['metadata']['modified'].isoformat(),
'size': session['metadata']['size']
})
project_data['sessions'].append(session_data)

# Add agents
for agent in data['agents']:
agent_data = {
'id': agent['file'].replace('.jsonl', ''),
'path': agent['path']
}
if agent['metadata']:
agent_data.update({
'created': agent['metadata']['created'].isoformat(),
'modified': agent['metadata']['modified'].isoformat(),
'size': agent['metadata']['size']
})
project_data['agents'].append(agent_data)

json_data['projects'].append(project_data)

return json.dumps(json_data, indent=2)

def main(): parser = argparse.ArgumentParser( description='Generate comprehensive index of Claude Code sessions', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=doc ) parser.add_argument('-o', '--output', default='./CLAUDE-SESSION-INDEX.md', help='Output markdown file path (default: ./CLAUDE-SESSION-INDEX.md)') parser.add_argument('-j', '--json', action='store_true', help='Also generate JSON output') parser.add_argument('-p', '--project', help='Filter to projects matching pattern') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')

args = parser.parse_args()

# Scan sessions
base_path = Path.home() / '.claude' / 'projects'
projects = scan_sessions(base_path, args.project, args.verbose)

if projects is None:
return 1

if not projects:
print("No projects found matching filter criteria" if args.project else "No projects found")
return 1

# Generate markdown
markdown_content = generate_markdown(projects, args.verbose)

# Write markdown output
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)

with open(output_path, 'w') as f:
f.write(markdown_content)

# Summary stats
total_sessions = sum(len(p['sessions']) for p in projects.values())
total_agents = sum(len(p['agents']) for p in projects.values())

print(f"✓ Generated session index: {output_path}")
print(f" Projects: {len(projects)}")
print(f" Sessions: {total_sessions}")
print(f" Agents: {total_agents}")

# Generate JSON if requested
if args.json:
json_path = output_path.with_suffix('.json')
json_content = generate_json(projects)

with open(json_path, 'w') as f:
f.write(json_content)

print(f"✓ Generated JSON index: {json_path}")

return 0

if name == 'main': sys.exit(main())