From e1f3ace2671636b3d8c87a84dc6ff634e3c70027 Mon Sep 17 00:00:00 2001 From: Raed Atoui Date: Tue, 27 Jan 2026 13:58:16 -0500 Subject: [PATCH] export json --- claude_code_log/cli.py | 4 +- claude_code_log/converter.py | 4 +- claude_code_log/json/__init__.py | 5 + claude_code_log/json/renderer.py | 156 +++++++++++++++++++++++++++++++ claude_code_log/renderer.py | 4 + 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 claude_code_log/json/__init__.py create mode 100644 claude_code_log/json/renderer.py diff --git a/claude_code_log/cli.py b/claude_code_log/cli.py index 2c6e3af..fe0ee45 100644 --- a/claude_code_log/cli.py +++ b/claude_code_log/cli.py @@ -490,9 +490,9 @@ def _clear_output_files(input_path: Path, all_projects: bool, file_ext: str) -> "-f", "--format", "output_format", - type=click.Choice(["html", "md", "markdown"]), + type=click.Choice(["html", "md", "markdown", "json"]), default="html", - help="Output format (default: html). Supports html, md, or markdown.", + help="Output format (default: html). Supports html, md/markdown, or json.", ) @click.option( "--image-export-mode", diff --git a/claude_code_log/converter.py b/claude_code_log/converter.py index 2a67308..e228862 100644 --- a/claude_code_log/converter.py +++ b/claude_code_log/converter.py @@ -2058,7 +2058,9 @@ def process_projects_hierarchy( # Generate index (always regenerate if outdated) ext = get_file_extension(output_format) - index_path = projects_path / f"index.{ext}" + # JSON uses a different index filename + index_filename = "all-projects-summary.json" if ext == "json" else f"index.{ext}" + index_path = projects_path / index_filename renderer = get_renderer(output_format, image_export_mode) index_regenerated = False if renderer.is_outdated(index_path) or from_date or to_date or any_cache_updated: diff --git a/claude_code_log/json/__init__.py b/claude_code_log/json/__init__.py new file mode 100644 index 0000000..8740b5f --- /dev/null +++ b/claude_code_log/json/__init__.py @@ -0,0 +1,5 @@ +"""JSON renderer for Claude Code transcripts.""" + +from .renderer import JsonRenderer + +__all__ = ["JsonRenderer"] diff --git a/claude_code_log/json/renderer.py b/claude_code_log/json/renderer.py new file mode 100644 index 0000000..a8b3b2b --- /dev/null +++ b/claude_code_log/json/renderer.py @@ -0,0 +1,156 @@ +"""JSON renderer implementation for Claude Code transcripts.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Optional, TYPE_CHECKING + +from ..cache import get_library_version +from ..models import TranscriptEntry +from ..renderer import Renderer + +if TYPE_CHECKING: + from ..cache import CacheManager + + +class JsonRenderer(Renderer): + """JSON renderer - exports raw transcript data.""" + + def __init__(self) -> None: + """Initialize the JSON renderer.""" + super().__init__() + + def generate( + self, + messages: list[TranscriptEntry], + title: Optional[str] = None, + combined_transcript_link: Optional[str] = None, + output_dir: Optional[Path] = None, + ) -> str: + """Serialize messages to JSON. + + Args: + messages: List of transcript entries to serialize. + title: Optional title for the export. + combined_transcript_link: Unused (for interface compatibility). + output_dir: Unused (for interface compatibility). + + Returns: + JSON string containing the serialized messages. + """ + return json.dumps( + { + "version": get_library_version(), + "title": title or "Claude Transcript", + "total_messages": len(messages), + "messages": [msg.model_dump(mode="json") for msg in messages], + }, + indent=2, + default=str, + ensure_ascii=False, + ) + + def generate_session( + self, + messages: list[TranscriptEntry], + session_id: str, + title: Optional[str] = None, + cache_manager: Optional["CacheManager"] = None, + output_dir: Optional[Path] = None, + ) -> str: + """Generate JSON for a single session. + + Args: + messages: All transcript entries. + session_id: Session ID to filter by. + title: Optional title for the export. + cache_manager: Unused (for interface compatibility). + output_dir: Unused (for interface compatibility). + + Returns: + JSON string containing the serialized session messages. + """ + session_messages = [msg for msg in messages if msg.sessionId == session_id] + return self.generate( + session_messages, + title or f"Session {session_id[:8]}", + ) + + def generate_projects_index( + self, + project_summaries: list[dict[str, Any]], + from_date: Optional[str] = None, + to_date: Optional[str] = None, + ) -> str: + """Generate a JSON projects index. + + Args: + project_summaries: List of project summary dictionaries. + from_date: Optional date filter start. + to_date: Optional date filter end. + + Returns: + JSON string containing the projects index. + """ + # Prepare project data for JSON serialization + projects: list[dict[str, Any]] = [] + total_messages = 0 + total_sessions = 0 + + for summary in project_summaries: + sessions = summary.get("sessions", []) + total_sessions += len(sessions) + total_messages += summary.get("message_count", 0) + + # Convert Path objects to strings for JSON serialization + project_data = { + "name": summary.get("name", ""), + "path": str(summary.get("path", "")), + "jsonl_count": summary.get("jsonl_count", 0), + "message_count": summary.get("message_count", 0), + "total_input_tokens": summary.get("total_input_tokens", 0), + "total_output_tokens": summary.get("total_output_tokens", 0), + "total_cache_creation_tokens": summary.get( + "total_cache_creation_tokens", 0 + ), + "total_cache_read_tokens": summary.get("total_cache_read_tokens", 0), + "earliest_timestamp": summary.get("earliest_timestamp", ""), + "latest_timestamp": summary.get("latest_timestamp", ""), + "working_directories": summary.get("working_directories", []), + "is_archived": summary.get("is_archived", False), + "sessions": sessions, + } + projects.append(project_data) + + return json.dumps( + { + "version": get_library_version(), + "total_projects": len(projects), + "total_sessions": total_sessions, + "total_messages": total_messages, + "date_range": {"from": from_date, "to": to_date}, + "projects": projects, + }, + indent=2, + default=str, + ensure_ascii=False, + ) + + def is_outdated(self, file_path: Path) -> bool: + """Check if a JSON file is outdated based on version field. + + Args: + file_path: Path to the JSON file to check. + + Returns: + True if the file should be regenerated, False if current. + """ + if not file_path.exists(): + return True + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("version") != get_library_version() + except (OSError, json.JSONDecodeError, UnicodeDecodeError): + return True diff --git a/claude_code_log/renderer.py b/claude_code_log/renderer.py index 1f3c349..6499c36 100644 --- a/claude_code_log/renderer.py +++ b/claude_code_log/renderer.py @@ -2310,6 +2310,10 @@ def get_renderer(format: str, image_export_mode: Optional[str] = None) -> Render # For Markdown, default to referenced mode mode = image_export_mode or "referenced" return MarkdownRenderer(image_export_mode=mode) + elif format == "json": + from .json.renderer import JsonRenderer + + return JsonRenderer() raise ValueError(f"Unsupported format: {format}")