claude-code-log 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/.claude/settings.local.json +2 -1
  2. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/CHANGELOG.md +6 -0
  3. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/PKG-INFO +1 -1
  4. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/models.py +17 -14
  5. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/parser.py +32 -30
  6. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/renderer.py +63 -79
  7. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/templates/transcript.html +0 -80
  8. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/pyproject.toml +1 -1
  9. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/scripts/style_guide_output/index_style_guide.html +45 -69
  10. claude_code_log-0.2.4/scripts/style_guide_output/transcript_style_guide.html +430 -0
  11. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_command_handling.py +1 -3
  12. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_data/edge_cases.jsonl +3 -2
  13. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_template_rendering.py +1 -2
  14. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_todowrite_rendering.py +1 -1
  15. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/uv.lock +1 -1
  16. claude_code_log-0.2.3/scripts/style_guide_output/transcript_style_guide.html +0 -689
  17. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/.github/workflows/ci.yml +0 -0
  18. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/.gitignore +0 -0
  19. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/CLAUDE.md +0 -0
  20. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/LICENSE +0 -0
  21. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/README.md +0 -0
  22. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/__init__.py +0 -0
  23. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/cli.py +0 -0
  24. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/converter.py +0 -0
  25. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/py.typed +0 -0
  26. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/claude_code_log/templates/index.html +0 -0
  27. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/justfile +0 -0
  28. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/scripts/generate_style_guide.py +0 -0
  29. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/scripts/style_guide_output/index.html +0 -0
  30. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/README.md +0 -0
  31. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/__init__.py +0 -0
  32. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_data/representative_messages.jsonl +0 -0
  33. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_data/session_b.jsonl +0 -0
  34. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_data/todowrite_examples.jsonl +0 -0
  35. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_date_filtering.py +0 -0
  36. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_filtering.py +0 -0
  37. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_markdown_rendering.py +0 -0
  38. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_message_filtering.py +0 -0
  39. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_message_types.py +0 -0
  40. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_path_conversion.py +0 -0
  41. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_template_data.py +0 -0
  42. {claude_code_log-0.2.3 → claude_code_log-0.2.4}/test/test_template_utils.py +0 -0
@@ -28,7 +28,8 @@
28
28
  "Bash(open /tmp/test_output.html)",
29
29
  "Bash(open /Users/dain/workspace/claude-code-log/scripts/style_guide_output/transcript_style_guide.html)",
30
30
  "Bash(open /tmp/test_preview.html)",
31
- "Bash(open /tmp/test_improved_preview.html)"
31
+ "Bash(open /tmp/test_improved_preview.html)",
32
+ "Bash(open /tmp/test_fixed_preview.html)"
32
33
  ],
33
34
  "deny": []
34
35
  }
@@ -5,6 +5,12 @@ All notable changes to claude-code-log will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.4] - 2025-06-18
9
+
10
+ ### Changed
11
+
12
+ - **More rrror handling**: Add better error reporting with line numbers and render fallbacks
13
+
8
14
  ## [0.2.3] - 2025-06-16
9
15
 
10
16
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-log
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Convert Claude Code transcript JSONL files to HTML
5
5
  Project-URL: Homepage, https://github.com/daaain/claude-code-log
6
6
  Project-URL: Issues, https://github.com/daaain/claude-code-log/issues
@@ -150,20 +150,23 @@ TranscriptEntry = Union[
150
150
 
151
151
  def parse_content_item(item_data: Dict[str, Any]) -> ContentItem:
152
152
  """Parse a content item based on its type field."""
153
- content_type = item_data.get("type", "")
154
-
155
- if content_type == "text":
156
- return TextContent.model_validate(item_data)
157
- elif content_type == "tool_use":
158
- return ToolUseContent.model_validate(item_data)
159
- elif content_type == "tool_result":
160
- return ToolResultContent.model_validate(item_data)
161
- elif content_type == "thinking":
162
- return ThinkingContent.model_validate(item_data)
163
- elif content_type == "image":
164
- return ImageContent.model_validate(item_data)
165
- else:
166
- # Fallback to text content for unknown types
153
+ try:
154
+ content_type = item_data.get("type", "")
155
+
156
+ if content_type == "text":
157
+ return TextContent.model_validate(item_data)
158
+ elif content_type == "tool_use":
159
+ return ToolUseContent.model_validate(item_data)
160
+ elif content_type == "tool_result":
161
+ return ToolResultContent.model_validate(item_data)
162
+ elif content_type == "thinking":
163
+ return ThinkingContent.model_validate(item_data)
164
+ elif content_type == "image":
165
+ return ImageContent.model_validate(item_data)
166
+ else:
167
+ # Fallback to text content for unknown types
168
+ return TextContent(type="text", text=str(item_data))
169
+ except AttributeError:
167
170
  return TextContent(type="text", text=str(item_data))
168
171
 
169
172
 
@@ -3,8 +3,8 @@
3
3
 
4
4
  import json
5
5
  from pathlib import Path
6
- import traceback
7
- from typing import List, Optional, Union, Dict
6
+ import re
7
+ from typing import Any, List, Optional, Union
8
8
  from datetime import datetime
9
9
  import dateparser
10
10
 
@@ -101,53 +101,55 @@ def filter_messages_by_date(
101
101
  def load_transcript(jsonl_path: Path) -> List[TranscriptEntry]:
102
102
  """Load and parse JSONL transcript file."""
103
103
  messages: List[TranscriptEntry] = []
104
- unique_errors: Dict[str, int] = {}
105
- unhandled_types: Dict[str, int] = {}
106
104
 
107
105
  with open(jsonl_path, "r", encoding="utf-8") as f:
108
- for line in f:
106
+ for line_no, line in enumerate(f):
109
107
  line = line.strip()
110
108
  if line:
111
109
  try:
112
- entry_dict = json.loads(line)
113
- entry_type = entry_dict.get("type", "unknown, missing type")
110
+ entry_dict: dict[str, Any] | str = json.loads(line)
111
+ if not isinstance(entry_dict, dict):
112
+ print(
113
+ f"Line {line_no} of {jsonl_path} is not a JSON object: {line}"
114
+ )
115
+ continue
116
+
117
+ entry_type: str | None = entry_dict.get("type")
114
118
 
115
119
  if entry_type in ["user", "assistant", "summary"]:
116
120
  # Parse using Pydantic models
117
121
  entry = parse_transcript_entry(entry_dict)
118
122
  messages.append(entry)
119
123
  else:
120
- # Track unhandled message types
121
- unhandled_types[entry_type] = (
122
- unhandled_types.get(entry_type, 0) + 1
124
+ print(
125
+ f"Line {line_no} of {jsonl_path} is not a recognised message type: {line}"
123
126
  )
124
127
  except json.JSONDecodeError as e:
125
- error_key = f"JSON decode error: {str(e)}"
126
- unique_errors[error_key] = unique_errors.get(error_key, 0) + 1
128
+ print(
129
+ f"Line {line_no} of {jsonl_path} | JSON decode error: {str(e)}"
130
+ )
127
131
  except ValueError as e:
128
132
  # Extract a more descriptive error message
129
133
  error_msg = str(e)
130
134
  if "validation error" in error_msg.lower():
131
- error_key = f"Validation error: {str(e)[:200]}..."
135
+ err_no_url = re.sub(
136
+ r" For further information visit https://errors.pydantic(.*)\n?",
137
+ "",
138
+ error_msg,
139
+ )
140
+ print(f"Line {line_no} of {jsonl_path} | {err_no_url}")
132
141
  else:
133
- error_key = f"ValueError: {error_msg[:200]}..."
134
- unique_errors[error_key] = unique_errors.get(error_key, 0) + 1
142
+ print(
143
+ f"Line {line_no} of {jsonl_path} | ValueError: {error_msg}"
144
+ "\n{traceback.format_exc()}"
145
+ )
135
146
  except Exception as e:
136
- error_key = f"Unexpected error: {str(e)}\n{traceback.format_exc()}"
137
- unique_errors[error_key] = unique_errors.get(error_key, 0) + 1
138
-
139
- # Print summary of errors if any occurred
140
- if unique_errors or unhandled_types:
141
- print(f"\nParsing summary for {jsonl_path.name}:")
142
- if unhandled_types:
143
- print("Unhandled message types:")
144
- for msg_type, count in unhandled_types.items():
145
- print(f" - {msg_type}: {count} occurrences")
146
- if unique_errors:
147
- print("Parsing errors:")
148
- for error, count in unique_errors.items():
149
- print(f" - {error}: {count} occurrences")
150
- print()
147
+ print(
148
+ f"Line {line_no} of {jsonl_path} | Unexpected error: {str(e)}"
149
+ "\n{traceback.format_exc()}"
150
+ )
151
+
152
+ print()
151
153
 
152
154
  return messages
153
155
 
@@ -37,41 +37,6 @@ def escape_html(text: str) -> str:
37
37
  return html.escape(text)
38
38
 
39
39
 
40
- def create_collapsible_details(
41
- summary: str, content: str, css_classes: str = ""
42
- ) -> str:
43
- """Create a collapsible details element with consistent styling and preview functionality."""
44
- class_attr = ' class="collapsible-details"'
45
- wrapper_classes = f"tool-content{' ' + css_classes if css_classes else ''}"
46
-
47
- # Extract first few lines for preview (up to ~3-5 lines, roughly 200 chars)
48
- import re
49
-
50
- # Remove HTML tags and get plain text for preview
51
- plain_text = re.sub(r"<[^>]+>", "", content)
52
- # Get first ~200 characters, break at word boundaries
53
- preview_text = plain_text[:200]
54
- if len(plain_text) > 200:
55
- preview_text = preview_text.rsplit(" ", 1)[0] + "..."
56
-
57
- # Render preview as markdown for better formatting
58
- preview_html = render_markdown(preview_text)
59
-
60
- return f"""
61
- <div class="{wrapper_classes}">
62
- <details{class_attr}>
63
- <summary>
64
- {summary}
65
- <div class="preview-content">{preview_html}</div>
66
- </summary>
67
- <div class="details-content">
68
- {content}
69
- </div>
70
- </details>
71
- </div>
72
- """
73
-
74
-
75
40
  def render_markdown(text: str) -> str:
76
41
  """Convert markdown text to HTML using mistune."""
77
42
  # Configure mistune with GitHub-flavored markdown features
@@ -153,27 +118,36 @@ def format_todowrite_content(tool_use: ToolUseContent) -> str:
153
118
  # Build todo list HTML
154
119
  todo_items: List[str] = []
155
120
  for todo in todos_data:
156
- todo_id = escape_html(str(todo.get("id", "")))
157
- content = escape_html(str(todo.get("content", "")))
158
- status = todo.get("status", "pending")
159
- priority = todo.get("priority", "medium")
160
- status_emoji = status_emojis.get(status, "")
161
-
162
- # Determine checkbox state
163
- checked = "checked" if status == "completed" else ""
164
- disabled = "disabled" if status == "completed" else ""
165
-
166
- # CSS class for styling
167
- item_class = f"todo-item {status} {priority}"
168
-
169
- todo_items.append(f"""
170
- <div class="{item_class}">
171
- <input type="checkbox" {checked} {disabled} readonly>
172
- <span class="todo-status">{status_emoji}</span>
173
- <span class="todo-content">{content}</span>
174
- <span class="todo-id">#{todo_id}</span>
175
- </div>
176
- """)
121
+ try:
122
+ todo_id = escape_html(str(todo.get("id", "")))
123
+ content = escape_html(str(todo.get("content", "")))
124
+ status = todo.get("status", "pending")
125
+ priority = todo.get("priority", "medium")
126
+ status_emoji = status_emojis.get(status, "⏳")
127
+
128
+ # Determine checkbox state
129
+ checked = "checked" if status == "completed" else ""
130
+ disabled = "disabled" if status == "completed" else ""
131
+
132
+ # CSS class for styling
133
+ item_class = f"todo-item {status} {priority}"
134
+
135
+ todo_items.append(f"""
136
+ <div class="{item_class}">
137
+ <input type="checkbox" {checked} {disabled} readonly>
138
+ <span class="todo-status">{status_emoji}</span>
139
+ <span class="todo-content">{content}</span>
140
+ <span class="todo-id">#{todo_id}</span>
141
+ </div>
142
+ """)
143
+ except AttributeError:
144
+ todo_items.append(f"""
145
+ <div class="todo-item pending medium">
146
+ <input type="checkbox" readonly>
147
+ <span class="todo-status">⏳</span>
148
+ <span class="todo-content">{str(todo)}</span>
149
+ </div>
150
+ """)
177
151
 
178
152
  todos_html = "".join(todo_items)
179
153
 
@@ -207,16 +181,18 @@ def format_tool_use_content(tool_use: ToolUseContent) -> str:
207
181
  except (TypeError, ValueError):
208
182
  escaped_input = escape_html(str(tool_use.input))
209
183
 
210
- summary = f"<strong>🛠️ Tool Use:</strong> {escaped_name} (ID: {escaped_id})"
211
- content = f"""
212
- <div class="tool-input">
213
- <strong>Input:</strong>
214
- <pre>{escaped_input}</pre>
215
- </div>
184
+ return f"""
185
+ <div class="tool-content tool-use">
186
+ <details>
187
+ <summary><strong>🛠️ Tool Use:</strong> {escaped_name} (ID: {escaped_id})</summary>
188
+ <div class="tool-input">
189
+ <strong>Input:</strong>
190
+ <pre>{escaped_input}</pre>
191
+ </div>
192
+ </details>
193
+ </div>
216
194
  """
217
195
 
218
- return create_collapsible_details(summary, content, "tool-use")
219
-
220
196
 
221
197
  def format_tool_result_content(tool_result: ToolResultContent) -> str:
222
198
  """Format tool result content as HTML."""
@@ -230,27 +206,37 @@ def format_tool_result_content(tool_result: ToolResultContent) -> str:
230
206
  content_parts: List[str] = []
231
207
  for item in tool_result.content:
232
208
  if item.get("type") == "text":
233
- text_value = item.get("text")
234
- if isinstance(text_value, str):
235
- content_parts.append(text_value)
209
+ content_parts.append(item.get("text", ""))
236
210
  escaped_content = escape_html("\n".join(content_parts))
237
211
 
238
212
  error_indicator = " (🚨 Error)" if tool_result.is_error else ""
239
213
 
240
- summary = f"<strong>🧰 Tool Result{error_indicator}:</strong> {escaped_id}"
241
- content = f'<div class="tool-input"><pre>{escaped_content}</pre></div>'
242
-
243
- return create_collapsible_details(summary, content, "tool-result")
214
+ return f"""
215
+ <div class="tool-content tool-result">
216
+ <details>
217
+ <summary><strong>🧰 Tool Result{error_indicator}:</strong> {escaped_id}</summary>
218
+ <div class="tool-input">
219
+ <pre>{escaped_content}</pre>
220
+ </div>
221
+ </details>
222
+ </div>
223
+ """
244
224
 
245
225
 
246
226
  def format_thinking_content(thinking: ThinkingContent) -> str:
247
227
  """Format thinking content as HTML."""
248
228
  escaped_thinking = escape_html(thinking.thinking)
249
229
 
250
- summary = "<strong>💭 Thinking</strong>"
251
- content = f'<div class="thinking-text"><pre>{escaped_thinking}</pre></div>'
252
-
253
- return create_collapsible_details(summary, content, "thinking-content")
230
+ return f"""
231
+ <div class="tool-content thinking-content">
232
+ <details>
233
+ <summary><strong>💭 Thinking</strong></summary>
234
+ <div class="thinking-text">
235
+ <pre>{escaped_thinking}</pre>
236
+ </div>
237
+ </details>
238
+ </div>
239
+ """
254
240
 
255
241
 
256
242
  def format_image_content(image: ImageContent) -> str:
@@ -618,11 +604,9 @@ def generate_html(messages: List[TranscriptEntry], title: Optional[str] = None)
618
604
  if command_args:
619
605
  content_parts.append(f"<strong>Args:</strong> {escaped_command_args}")
620
606
  if command_contents:
621
- details_content = (
622
- f"<div class='content'>{escaped_command_contents}</div>"
607
+ content_parts.append(
608
+ f"<details><summary>Content</summary><div class='content'>{escaped_command_contents}</div></details>"
623
609
  )
624
- details_html = create_collapsible_details("Content", details_content)
625
- content_parts.append(details_html)
626
610
 
627
611
  content_html = "<br>".join(content_parts)
628
612
  message_type = "system"
@@ -60,86 +60,6 @@
60
60
  margin-bottom: 8px;
61
61
  }
62
62
 
63
- /* Collapsible details preview functionality */
64
- .collapsible-details {
65
- position: relative;
66
- }
67
-
68
- .collapsible-details summary {
69
- position: relative;
70
- cursor: pointer;
71
- }
72
-
73
- /* Preview content styling - shown when closed */
74
- .collapsible-details:not([open]) .preview-content {
75
- display: block;
76
- margin-top: 8px;
77
- padding: 6px 8px;
78
- background-color: rgba(255, 255, 255, 0.3);
79
- border-radius: 3px;
80
- font-size: 0.9em;
81
- font-weight: normal;
82
- color: #555;
83
- line-height: 1.3;
84
- white-space: pre-wrap;
85
- word-wrap: break-word;
86
- position: relative;
87
- max-height: 4em;
88
- overflow: hidden;
89
- }
90
-
91
- /* Add subtle fade effect to preview content */
92
- .collapsible-details:not([open]) .preview-content::after {
93
- content: "";
94
- position: absolute;
95
- bottom: 0;
96
- left: 0;
97
- right: 0;
98
- height: 1em;
99
- background: linear-gradient(transparent, rgba(255, 255, 255, 0.3));
100
- pointer-events: none;
101
- }
102
-
103
- /* Hide preview content when details is open */
104
- .collapsible-details[open] .preview-content {
105
- display: none;
106
- }
107
-
108
- /* Style the full details content to match preview */
109
- .collapsible-details .details-content {
110
- margin-top: 8px;
111
- padding: 6px 8px;
112
- background-color: rgba(255, 255, 255, 0.3);
113
- border-radius: 3px;
114
- font-size: 0.9em;
115
- font-weight: normal;
116
- color: #555;
117
- line-height: 1.3;
118
- }
119
-
120
- /* Hide details content when closed */
121
- .collapsible-details:not([open]) .details-content {
122
- display: none;
123
- }
124
-
125
- /* Remove extra styling from nested elements */
126
- .collapsible-details .details-content .tool-input {
127
- background-color: transparent;
128
- border: none;
129
- box-shadow: none;
130
- padding: 0;
131
- margin: 0;
132
- }
133
-
134
- .collapsible-details .details-content pre {
135
- background-color: transparent;
136
- padding: 0;
137
- margin: 0;
138
- font-family: inherit;
139
- font-size: inherit;
140
- color: inherit;
141
- }
142
-
143
63
  .tool-content {
144
64
  background-color: #f8f9fa66;
145
65
  border-radius: 4px;
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-log"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Convert Claude Code transcript JSONL files to HTML"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"