claude-code-log 0.3.2__tar.gz → 0.3.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 (36) hide show
  1. claude_code_log-0.3.2/.claude/settings.json.bak → claude_code_log-0.3.4/.claude/settings.json +4 -4
  2. claude_code_log-0.3.4/.github/workflows/claude_code.yml +46 -0
  3. claude_code_log-0.3.4/.github/workflows/claude_code_login.yml +21 -0
  4. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/.gitignore +1 -0
  5. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/CHANGELOG.md +18 -0
  6. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/PKG-INFO +7 -1
  7. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/README.md +5 -0
  8. claude_code_log-0.3.4/claude_code_log/cache.py +449 -0
  9. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/cli.py +67 -2
  10. claude_code_log-0.3.4/claude_code_log/converter.py +626 -0
  11. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/models.py +23 -1
  12. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/parser.py +37 -5
  13. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/timeline.html +34 -37
  14. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/timeline_styles.css +23 -5
  15. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/transcript.html +6 -2
  16. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/justfile +7 -3
  17. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/pyproject.toml +2 -1
  18. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/uv.lock +12 -1
  19. claude_code_log-0.3.2/claude_code_log/converter.py +0 -346
  20. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/.claude/settings.local.json +0 -0
  21. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/.github/workflows/ci.yml +0 -0
  22. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/.github/workflows/docs.yml +0 -0
  23. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/CLAUDE.md +0 -0
  24. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/LICENSE +0 -0
  25. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/__init__.py +0 -0
  26. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/py.typed +0 -0
  27. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/renderer.py +0 -0
  28. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/filter_styles.css +0 -0
  29. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/global_styles.css +0 -0
  30. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/message_styles.css +0 -0
  31. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/project_card_styles.css +0 -0
  32. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/session_nav.html +0 -0
  33. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/session_nav_styles.css +0 -0
  34. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/components/todo_styles.css +0 -0
  35. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/templates/index.html +0 -0
  36. {claude_code_log-0.3.2 → claude_code_log-0.3.4}/claude_code_log/utils.py +0 -0
@@ -17,19 +17,19 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "uv run ruff check --fix"
20
+ "command": "uv run ruff check --fix 2>&1"
21
21
  },
22
22
  {
23
23
  "type": "command",
24
- "command": "uv run ty check"
24
+ "command": "uv run ty check 2>&1"
25
25
  },
26
26
  {
27
27
  "type": "command",
28
- "command": "uv run pyright"
28
+ "command": "uv run pyright 2>&1"
29
29
  },
30
30
  {
31
31
  "type": "command",
32
- "command": "uv run pytest"
32
+ "command": "uv run pytest 2>&1"
33
33
  }
34
34
  ]
35
35
  }
@@ -0,0 +1,46 @@
1
+ name: Claude PR Assistant - Authorized Users Only
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude-code-action:
15
+ # Only respond to @claude mentions from daaain
16
+ if: |
17
+ (
18
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
19
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
20
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
21
+ (github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
22
+ ) && github.actor == 'daaain'
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ pull-requests: read
27
+ issues: read
28
+ id-token: write
29
+ actions: write
30
+ steps:
31
+ - name: Checkout repository
32
+ uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 1
35
+
36
+ - name: Run Claude PR Action
37
+ uses: grll/claude-code-action@beta
38
+ with:
39
+ use_oauth: true
40
+ claude_access_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
41
+ claude_refresh_token: ${{ secrets.CLAUDE_REFRESH_TOKEN }}
42
+ claude_expires_at: ${{ secrets.CLAUDE_EXPIRES_AT }}
43
+ secrets_admin_pat: ${{ secrets.SECRETS_ADMIN_PAT }}
44
+ timeout_minutes: "120"
45
+ allowed_tools: "Bash,Edit,MultiEdit,View,GlobTool,Glob,GrepTool,Grep,BatchTool,Batch,LS,Read,Write,Replace,NotebookEditCell,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files,mcp__github_file_ops__update_claude_comment,WebSearch,WebFetch"
46
+ max_turns: "500"
@@ -0,0 +1,21 @@
1
+ name: Claude OAuth
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ code:
7
+ description: 'Authorization code (leave empty for step 1)'
8
+ required: false
9
+
10
+ permissions:
11
+ actions: write # Required for cache management
12
+ contents: read # Required for basic repository access
13
+
14
+ jobs:
15
+ auth:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: grll/claude-code-login@v1
19
+ with:
20
+ code: ${{ inputs.code }}
21
+ secrets_admin_pat: ${{ secrets.SECRETS_ADMIN_PAT }}
@@ -178,3 +178,4 @@ test_output
178
178
  .DS_Store
179
179
  test/test_data/*.html
180
180
  .claude-trace
181
+ .specstory
@@ -6,6 +6,24 @@ 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
8
 
9
+ ## [0.3.4] - 2025-07-13
10
+
11
+ ### Changed
12
+
13
+ - **Implement caching (writes processed JSON files into .claude project directories)**
14
+ - **Extend ToolUseResult to handle List[ContentItem] to support MCP tool results**
15
+ - **Power to Claude**
16
+ - **Add Claude Code OAuth workflows**
17
+
18
+
19
+ ## [0.3.3] - 2025-07-05
20
+
21
+ ### Changed
22
+
23
+ - **Hide groups in the timeline instead of items + bug fixes**
24
+ - **Get tooltip config working + improve rendering and styling**
25
+
26
+
9
27
  ## [0.3.2] - 2025-07-03
10
28
 
11
29
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-log
3
- Version: 0.3.2
3
+ Version: 0.3.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
@@ -16,6 +16,7 @@ Requires-Dist: dateparser>=1.0.0
16
16
  Requires-Dist: jinja2>=3.0.0
17
17
  Requires-Dist: mistune>=3.1.3
18
18
  Requires-Dist: pydantic>=2.0.0
19
+ Requires-Dist: toml>=0.10.2
19
20
  Description-Content-Type: text/markdown
20
21
 
21
22
  # Claude Code Log
@@ -273,6 +274,8 @@ uv run claude-code-log
273
274
 
274
275
  ## TODO
275
276
 
277
+ - add a bit of padding to the last message time so the timeline doesn't look empty when opening
278
+ - tutorial overlay
276
279
  - integrate `claude-trace` request logs if present?
277
280
  - Shortcut / command to resume a specific conversation by session ID $ claude --resume 550e8400-e29b-41d4-a716-446655440000?
278
281
  - Localised number formatting and timezone adjustment runtime? For this we'd need to make Jinja template variables more granular
@@ -285,3 +288,6 @@ uv run claude-code-log
285
288
  - system blocks like `init` also don't render perfectly, losing new lines
286
289
  - add `ccusage` like daily summary and maybe some textual summary too based on Claude generate session summaries?
287
290
  – import logs from @claude Github Actions
291
+ - stream logs from @claude Github Actions, see [octotail](https://github.com/getbettr/octotail)
292
+ - extend into a VS Code extension that reads the JSONL real-time and displays stats like current context usage and implements a UI to see messages, todos, permissions, config, MCP status, etc
293
+ - feed the filtered user messages to headless claude CLI to distill the user intent from the session
@@ -253,6 +253,8 @@ uv run claude-code-log
253
253
 
254
254
  ## TODO
255
255
 
256
+ - add a bit of padding to the last message time so the timeline doesn't look empty when opening
257
+ - tutorial overlay
256
258
  - integrate `claude-trace` request logs if present?
257
259
  - Shortcut / command to resume a specific conversation by session ID $ claude --resume 550e8400-e29b-41d4-a716-446655440000?
258
260
  - Localised number formatting and timezone adjustment runtime? For this we'd need to make Jinja template variables more granular
@@ -265,3 +267,6 @@ uv run claude-code-log
265
267
  - system blocks like `init` also don't render perfectly, losing new lines
266
268
  - add `ccusage` like daily summary and maybe some textual summary too based on Claude generate session summaries?
267
269
  – import logs from @claude Github Actions
270
+ - stream logs from @claude Github Actions, see [octotail](https://github.com/getbettr/octotail)
271
+ - extend into a VS Code extension that reads the JSONL real-time and displays stats like current context usage and implements a UI to see messages, todos, permissions, config, MCP status, etc
272
+ - feed the filtered user messages to headless claude CLI to distill the user intent from the session
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env python3
2
+ """Cache management for Claude Code Log to improve performance."""
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, cast
7
+ from datetime import datetime
8
+ from pydantic import BaseModel
9
+
10
+ from .models import TranscriptEntry
11
+
12
+
13
+ class CachedFileInfo(BaseModel):
14
+ """Information about a cached JSONL file."""
15
+
16
+ file_path: str
17
+ source_mtime: float
18
+ cached_mtime: float
19
+ message_count: int
20
+ session_ids: List[str]
21
+
22
+
23
+ class SessionCacheData(BaseModel):
24
+ """Cached session-level information."""
25
+
26
+ session_id: str
27
+ summary: Optional[str] = None
28
+ first_timestamp: str
29
+ last_timestamp: str
30
+ message_count: int
31
+ first_user_message: str
32
+ total_input_tokens: int = 0
33
+ total_output_tokens: int = 0
34
+ total_cache_creation_tokens: int = 0
35
+ total_cache_read_tokens: int = 0
36
+
37
+
38
+ class ProjectCache(BaseModel):
39
+ """Project-level cache index structure for index.json."""
40
+
41
+ version: str
42
+ cache_created: str
43
+ last_updated: str
44
+ project_path: str
45
+
46
+ # File-level cache information
47
+ cached_files: Dict[str, CachedFileInfo]
48
+
49
+ # Aggregated project information
50
+ total_message_count: int = 0
51
+ total_input_tokens: int = 0
52
+ total_output_tokens: int = 0
53
+ total_cache_creation_tokens: int = 0
54
+ total_cache_read_tokens: int = 0
55
+
56
+ # Session metadata
57
+ sessions: Dict[str, SessionCacheData]
58
+
59
+ # Timeline information
60
+ earliest_timestamp: str = ""
61
+ latest_timestamp: str = ""
62
+
63
+
64
+ class CacheManager:
65
+ """Manages cache operations for a project directory."""
66
+
67
+ def __init__(self, project_path: Path, library_version: str):
68
+ """Initialize cache manager for a project.
69
+
70
+ Args:
71
+ project_path: Path to the project directory containing JSONL files
72
+ library_version: Current version of the library for cache invalidation
73
+ """
74
+ self.project_path = project_path
75
+ self.library_version = library_version
76
+ self.cache_dir = project_path / "cache"
77
+ self.index_file = self.cache_dir / "index.json"
78
+
79
+ # Ensure cache directory exists
80
+ self.cache_dir.mkdir(exist_ok=True)
81
+
82
+ # Load existing cache index if available
83
+ self._project_cache: Optional[ProjectCache] = None
84
+ self._load_project_cache()
85
+
86
+ def _load_project_cache(self) -> None:
87
+ """Load the project cache index from disk."""
88
+ if self.index_file.exists():
89
+ try:
90
+ with open(self.index_file, "r", encoding="utf-8") as f:
91
+ cache_data = json.load(f)
92
+ self._project_cache = ProjectCache.model_validate(cache_data)
93
+
94
+ # Check if cache version matches current library version
95
+ if self._project_cache.version != self.library_version:
96
+ print(
97
+ f"Cache version mismatch: {self._project_cache.version} != {self.library_version}, invalidating cache"
98
+ )
99
+ self.clear_cache()
100
+ self._project_cache = None
101
+ except Exception as e:
102
+ print(f"Warning: Failed to load cache index, will rebuild: {e}")
103
+ self._project_cache = None
104
+
105
+ # Initialize empty cache if none exists
106
+ if self._project_cache is None:
107
+ self._project_cache = ProjectCache(
108
+ version=self.library_version,
109
+ cache_created=datetime.now().isoformat(),
110
+ last_updated=datetime.now().isoformat(),
111
+ project_path=str(self.project_path),
112
+ cached_files={},
113
+ sessions={},
114
+ )
115
+
116
+ def _save_project_cache(self) -> None:
117
+ """Save the project cache index to disk."""
118
+ if self._project_cache is None:
119
+ return
120
+
121
+ self._project_cache.last_updated = datetime.now().isoformat()
122
+
123
+ with open(self.index_file, "w", encoding="utf-8") as f:
124
+ json.dump(self._project_cache.model_dump(), f, indent=2)
125
+
126
+ def _get_cache_file_path(self, jsonl_path: Path) -> Path:
127
+ """Get the cache file path for a given JSONL file."""
128
+ return self.cache_dir / f"{jsonl_path.stem}.json"
129
+
130
+ def is_file_cached(self, jsonl_path: Path) -> bool:
131
+ """Check if a JSONL file has a valid cache entry."""
132
+ if self._project_cache is None:
133
+ return False
134
+
135
+ file_key = jsonl_path.name
136
+ if file_key not in self._project_cache.cached_files:
137
+ return False
138
+
139
+ # Check if source file exists and modification time matches
140
+ if not jsonl_path.exists():
141
+ return False
142
+
143
+ cached_info = self._project_cache.cached_files[file_key]
144
+ source_mtime = jsonl_path.stat().st_mtime
145
+
146
+ # Cache is valid if modification times match and cache file exists
147
+ cache_file = self._get_cache_file_path(jsonl_path)
148
+ return (
149
+ abs(source_mtime - cached_info.source_mtime) < 1.0 and cache_file.exists()
150
+ )
151
+
152
+ def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry]]:
153
+ """Load cached transcript entries for a JSONL file."""
154
+ if not self.is_file_cached(jsonl_path):
155
+ return None
156
+
157
+ cache_file = self._get_cache_file_path(jsonl_path)
158
+ try:
159
+ with open(cache_file, "r", encoding="utf-8") as f:
160
+ cache_data = json.load(f)
161
+
162
+ # Expect timestamp-keyed format - flatten all entries
163
+ entries_data: List[Dict[str, Any]] = []
164
+ for timestamp_entries in cache_data.values():
165
+ if isinstance(timestamp_entries, list):
166
+ # Type cast to ensure Pyright knows this is List[Dict[str, Any]]
167
+ entries_data.extend(cast(List[Dict[str, Any]], timestamp_entries))
168
+
169
+ # Deserialize back to TranscriptEntry objects
170
+ from .models import parse_transcript_entry
171
+
172
+ entries = [
173
+ parse_transcript_entry(entry_dict) for entry_dict in entries_data
174
+ ]
175
+ return entries
176
+ except Exception as e:
177
+ print(f"Warning: Failed to load cached entries from {cache_file}: {e}")
178
+ return None
179
+
180
+ def load_cached_entries_filtered(
181
+ self, jsonl_path: Path, from_date: Optional[str], to_date: Optional[str]
182
+ ) -> Optional[List[TranscriptEntry]]:
183
+ """Load cached entries with efficient timestamp-based filtering."""
184
+ if not self.is_file_cached(jsonl_path):
185
+ return None
186
+
187
+ cache_file = self._get_cache_file_path(jsonl_path)
188
+ try:
189
+ with open(cache_file, "r", encoding="utf-8") as f:
190
+ cache_data = json.load(f)
191
+
192
+ # If no date filtering needed, fall back to regular loading
193
+ if not from_date and not to_date:
194
+ return self.load_cached_entries(jsonl_path)
195
+
196
+ # Parse date filters
197
+ from .parser import parse_timestamp
198
+ import dateparser
199
+
200
+ from_dt = None
201
+ to_dt = None
202
+
203
+ if from_date:
204
+ from_dt = dateparser.parse(from_date)
205
+ if from_dt and (
206
+ from_date in ["today", "yesterday"] or "days ago" in from_date
207
+ ):
208
+ from_dt = from_dt.replace(hour=0, minute=0, second=0, microsecond=0)
209
+
210
+ if to_date:
211
+ to_dt = dateparser.parse(to_date)
212
+ if to_dt:
213
+ if to_date in ["today", "yesterday"] or "days ago" in to_date:
214
+ to_dt = to_dt.replace(
215
+ hour=23, minute=59, second=59, microsecond=999999
216
+ )
217
+ else:
218
+ # For simple date strings like "2023-01-01", set to end of day
219
+ to_dt = to_dt.replace(
220
+ hour=23, minute=59, second=59, microsecond=999999
221
+ )
222
+
223
+ # Filter entries by timestamp
224
+ filtered_entries_data: List[Dict[str, Any]] = []
225
+
226
+ for timestamp_key, timestamp_entries in cache_data.items():
227
+ if timestamp_key == "_no_timestamp":
228
+ # Always include entries without timestamps (like summaries)
229
+ if isinstance(timestamp_entries, list):
230
+ # Type cast to ensure Pyright knows this is List[Dict[str, Any]]
231
+ filtered_entries_data.extend(
232
+ cast(List[Dict[str, Any]], timestamp_entries)
233
+ )
234
+ else:
235
+ # Check if timestamp falls within range
236
+ message_dt = parse_timestamp(timestamp_key)
237
+ if message_dt:
238
+ # Convert to naive datetime for comparison
239
+ if message_dt.tzinfo:
240
+ message_dt = message_dt.replace(tzinfo=None)
241
+
242
+ # Apply date filtering
243
+ if from_dt and message_dt < from_dt:
244
+ continue
245
+ if to_dt and message_dt > to_dt:
246
+ continue
247
+
248
+ if isinstance(timestamp_entries, list):
249
+ # Type cast to ensure Pyright knows this is List[Dict[str, Any]]
250
+ filtered_entries_data.extend(
251
+ cast(List[Dict[str, Any]], timestamp_entries)
252
+ )
253
+
254
+ # Deserialize filtered entries
255
+ from .models import parse_transcript_entry
256
+
257
+ entries = [
258
+ parse_transcript_entry(entry_dict)
259
+ for entry_dict in filtered_entries_data
260
+ ]
261
+ return entries
262
+ except Exception as e:
263
+ print(
264
+ f"Warning: Failed to load filtered cached entries from {cache_file}: {e}"
265
+ )
266
+ return None
267
+
268
+ def save_cached_entries(
269
+ self, jsonl_path: Path, entries: List[TranscriptEntry]
270
+ ) -> None:
271
+ """Save parsed transcript entries to cache with timestamp-based structure."""
272
+ cache_file = self._get_cache_file_path(jsonl_path)
273
+
274
+ try:
275
+ # Create timestamp-keyed cache structure for efficient date filtering
276
+ cache_data: Dict[str, Any] = {}
277
+
278
+ for entry in entries:
279
+ # Get timestamp - use empty string as fallback for entries without timestamps
280
+ timestamp = (
281
+ getattr(entry, "timestamp", "")
282
+ if hasattr(entry, "timestamp")
283
+ else ""
284
+ )
285
+ if not timestamp:
286
+ # Use a special key for entries without timestamps (like summaries)
287
+ timestamp = "_no_timestamp"
288
+
289
+ # Store entry data under timestamp
290
+ if timestamp not in cache_data:
291
+ cache_data[timestamp] = []
292
+
293
+ cache_data[timestamp].append(entry.model_dump())
294
+
295
+ with open(cache_file, "w", encoding="utf-8") as f:
296
+ json.dump(cache_data, f, indent=2)
297
+
298
+ # Update cache index
299
+ if self._project_cache is not None:
300
+ source_mtime = jsonl_path.stat().st_mtime
301
+ cached_mtime = cache_file.stat().st_mtime
302
+
303
+ # Extract session IDs from entries
304
+ session_ids: List[str] = []
305
+ for entry in entries:
306
+ if hasattr(entry, "sessionId"):
307
+ session_id = getattr(entry, "sessionId", "")
308
+ if session_id:
309
+ session_ids.append(session_id)
310
+ session_ids = list(set(session_ids)) # Remove duplicates
311
+
312
+ self._project_cache.cached_files[jsonl_path.name] = CachedFileInfo(
313
+ file_path=str(jsonl_path),
314
+ source_mtime=source_mtime,
315
+ cached_mtime=cached_mtime,
316
+ message_count=len(entries),
317
+ session_ids=session_ids,
318
+ )
319
+
320
+ self._save_project_cache()
321
+ except Exception as e:
322
+ print(f"Warning: Failed to save cached entries to {cache_file}: {e}")
323
+
324
+ def update_session_cache(self, session_data: Dict[str, SessionCacheData]) -> None:
325
+ """Update cached session information."""
326
+ if self._project_cache is None:
327
+ return
328
+
329
+ self._project_cache.sessions.update(
330
+ {session_id: data for session_id, data in session_data.items()}
331
+ )
332
+ self._save_project_cache()
333
+
334
+ def update_project_aggregates(
335
+ self,
336
+ total_message_count: int,
337
+ total_input_tokens: int,
338
+ total_output_tokens: int,
339
+ total_cache_creation_tokens: int,
340
+ total_cache_read_tokens: int,
341
+ earliest_timestamp: str,
342
+ latest_timestamp: str,
343
+ ) -> None:
344
+ """Update project-level aggregate information."""
345
+ if self._project_cache is None:
346
+ return
347
+
348
+ self._project_cache.total_message_count = total_message_count
349
+ self._project_cache.total_input_tokens = total_input_tokens
350
+ self._project_cache.total_output_tokens = total_output_tokens
351
+ self._project_cache.total_cache_creation_tokens = total_cache_creation_tokens
352
+ self._project_cache.total_cache_read_tokens = total_cache_read_tokens
353
+ self._project_cache.earliest_timestamp = earliest_timestamp
354
+ self._project_cache.latest_timestamp = latest_timestamp
355
+
356
+ self._save_project_cache()
357
+
358
+ def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
359
+ """Get list of JSONL files that need to be reprocessed."""
360
+ modified_files: List[Path] = []
361
+
362
+ for jsonl_file in jsonl_files:
363
+ if not self.is_file_cached(jsonl_file):
364
+ modified_files.append(jsonl_file)
365
+
366
+ return modified_files
367
+
368
+ def get_cached_project_data(self) -> Optional[ProjectCache]:
369
+ """Get the cached project data if available."""
370
+ return self._project_cache
371
+
372
+ def clear_cache(self) -> None:
373
+ """Clear all cache files and reset the project cache."""
374
+ if self.cache_dir.exists():
375
+ for cache_file in self.cache_dir.glob("*.json"):
376
+ if cache_file.name != "index.json": # Don't delete the index file here
377
+ try:
378
+ cache_file.unlink()
379
+ except Exception as e:
380
+ print(f"Warning: Failed to delete cache file {cache_file}: {e}")
381
+
382
+ if self.index_file.exists():
383
+ try:
384
+ self.index_file.unlink()
385
+ except Exception as e:
386
+ print(f"Warning: Failed to delete cache index {self.index_file}: {e}")
387
+
388
+ # Reset the project cache
389
+ self._project_cache = ProjectCache(
390
+ version=self.library_version,
391
+ cache_created=datetime.now().isoformat(),
392
+ last_updated=datetime.now().isoformat(),
393
+ project_path=str(self.project_path),
394
+ cached_files={},
395
+ sessions={},
396
+ )
397
+
398
+ def get_cache_stats(self) -> Dict[str, Any]:
399
+ """Get cache statistics for reporting."""
400
+ if self._project_cache is None:
401
+ return {"cache_enabled": False}
402
+
403
+ return {
404
+ "cache_enabled": True,
405
+ "cached_files_count": len(self._project_cache.cached_files),
406
+ "total_cached_messages": self._project_cache.total_message_count,
407
+ "total_sessions": len(self._project_cache.sessions),
408
+ "cache_created": self._project_cache.cache_created,
409
+ "last_updated": self._project_cache.last_updated,
410
+ }
411
+
412
+
413
+ def get_library_version() -> str:
414
+ """Get the current library version from pyproject.toml."""
415
+ try:
416
+ import toml
417
+
418
+ project_root = Path(__file__).parent.parent
419
+ pyproject_path = project_root / "pyproject.toml"
420
+
421
+ if pyproject_path.exists():
422
+ with open(pyproject_path, "r") as f:
423
+ pyproject_data = toml.load(f)
424
+ return pyproject_data.get("project", {}).get("version", "unknown")
425
+ except ImportError:
426
+ # toml is not available, try parsing manually
427
+ pass
428
+ except Exception:
429
+ pass
430
+
431
+ # Fallback: try to read version manually
432
+ try:
433
+ project_root = Path(__file__).parent.parent
434
+ pyproject_path = project_root / "pyproject.toml"
435
+
436
+ if pyproject_path.exists():
437
+ with open(pyproject_path, "r") as f:
438
+ content = f.read()
439
+
440
+ # Simple regex to extract version
441
+ import re
442
+
443
+ version_match = re.search(r'version\s*=\s*"([^"]+)"', content)
444
+ if version_match:
445
+ return version_match.group(1)
446
+ except Exception:
447
+ pass
448
+
449
+ return "unknown"