Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions claude_code_log/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion claude_code_log/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions claude_code_log/json/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""JSON renderer for Claude Code transcripts."""

from .renderer import JsonRenderer

__all__ = ["JsonRenderer"]
156 changes: 156 additions & 0 deletions claude_code_log/json/renderer.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions claude_code_log/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")


Expand Down