claude-code-wrapped 0.1.5 → 0.1.6
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.
- package/.claude/settings.local.json +9 -0
- package/claude_code_wrapped/__init__.py +1 -1
- package/claude_code_wrapped/main.py +19 -0
- package/claude_code_wrapped/stats.py +112 -9
- package/claude_code_wrapped/ui.py +122 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/uv.lock +1 -1
|
@@ -92,6 +92,25 @@ Examples:
|
|
|
92
92
|
"weekday_distribution": stats.weekday_distribution,
|
|
93
93
|
"estimated_cost_usd": stats.estimated_cost,
|
|
94
94
|
"cost_by_model": stats.cost_by_model,
|
|
95
|
+
# Averages
|
|
96
|
+
"avg_messages_per_day": round(stats.avg_messages_per_day, 1),
|
|
97
|
+
"avg_messages_per_week": round(stats.avg_messages_per_week, 1),
|
|
98
|
+
"avg_messages_per_month": round(stats.avg_messages_per_month, 1),
|
|
99
|
+
"avg_cost_per_day": round(stats.avg_cost_per_day, 2) if stats.avg_cost_per_day else None,
|
|
100
|
+
"avg_cost_per_week": round(stats.avg_cost_per_week, 2) if stats.avg_cost_per_week else None,
|
|
101
|
+
"avg_cost_per_month": round(stats.avg_cost_per_month, 2) if stats.avg_cost_per_month else None,
|
|
102
|
+
# Code activity
|
|
103
|
+
"total_edits": stats.total_edits,
|
|
104
|
+
"total_writes": stats.total_writes,
|
|
105
|
+
"avg_code_changes_per_day": round(stats.avg_edits_per_day, 1),
|
|
106
|
+
"avg_code_changes_per_week": round(stats.avg_edits_per_week, 1),
|
|
107
|
+
# Monthly breakdown
|
|
108
|
+
"monthly_costs": stats.monthly_costs,
|
|
109
|
+
"monthly_tokens": stats.monthly_tokens,
|
|
110
|
+
# Longest conversation
|
|
111
|
+
"longest_conversation_messages": stats.longest_conversation_messages,
|
|
112
|
+
"longest_conversation_tokens": stats.longest_conversation_tokens,
|
|
113
|
+
"longest_conversation_date": stats.longest_conversation_date.isoformat() if stats.longest_conversation_date else None,
|
|
95
114
|
}
|
|
96
115
|
print(json.dumps(output, indent=2))
|
|
97
116
|
else:
|
|
@@ -71,14 +71,39 @@ class WrappedStats:
|
|
|
71
71
|
|
|
72
72
|
# Fun stats
|
|
73
73
|
longest_conversation_tokens: int = 0
|
|
74
|
-
avg_messages_per_day: float = 0.0
|
|
75
74
|
avg_tokens_per_message: float = 0.0
|
|
76
75
|
|
|
76
|
+
# Averages (messages)
|
|
77
|
+
avg_messages_per_day: float = 0.0
|
|
78
|
+
avg_messages_per_week: float = 0.0
|
|
79
|
+
avg_messages_per_month: float = 0.0
|
|
80
|
+
|
|
81
|
+
# Averages (cost)
|
|
82
|
+
avg_cost_per_day: float = 0.0
|
|
83
|
+
avg_cost_per_week: float = 0.0
|
|
84
|
+
avg_cost_per_month: float = 0.0
|
|
85
|
+
|
|
86
|
+
# Code activity (from Edit/Write tools)
|
|
87
|
+
total_edits: int = 0
|
|
88
|
+
total_writes: int = 0
|
|
89
|
+
avg_edits_per_day: float = 0.0
|
|
90
|
+
avg_edits_per_week: float = 0.0
|
|
91
|
+
|
|
77
92
|
# Cost tracking (per model)
|
|
78
93
|
model_token_usage: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
79
94
|
estimated_cost: float | None = None
|
|
80
95
|
cost_by_model: dict[str, float] = field(default_factory=dict)
|
|
81
96
|
|
|
97
|
+
# Monthly breakdown for cost table
|
|
98
|
+
monthly_costs: dict[str, float] = field(default_factory=dict) # "YYYY-MM" -> cost
|
|
99
|
+
monthly_tokens: dict[str, dict[str, int]] = field(default_factory=dict) # "YYYY-MM" -> {input, output, ...}
|
|
100
|
+
|
|
101
|
+
# Longest conversation tracking
|
|
102
|
+
longest_conversation_messages: int = 0
|
|
103
|
+
longest_conversation_tokens: int = 0
|
|
104
|
+
longest_conversation_session: str | None = None
|
|
105
|
+
longest_conversation_date: datetime | None = None
|
|
106
|
+
|
|
82
107
|
@property
|
|
83
108
|
def total_tokens(self) -> int:
|
|
84
109
|
return (
|
|
@@ -163,6 +188,19 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
|
|
|
163
188
|
projects = Counter()
|
|
164
189
|
daily = defaultdict(lambda: DailyStats(date=datetime.now()))
|
|
165
190
|
|
|
191
|
+
# Track monthly token usage for cost breakdown
|
|
192
|
+
monthly_tokens: dict[str, dict[str, int]] = defaultdict(
|
|
193
|
+
lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0}
|
|
194
|
+
)
|
|
195
|
+
monthly_model_tokens: dict[str, dict[str, dict[str, int]]] = defaultdict(
|
|
196
|
+
lambda: defaultdict(lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0})
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Track per-session message counts for longest conversation
|
|
200
|
+
session_messages: dict[str, int] = Counter()
|
|
201
|
+
session_tokens: dict[str, int] = Counter()
|
|
202
|
+
session_first_time: dict[str, datetime] = {}
|
|
203
|
+
|
|
166
204
|
# Process each message
|
|
167
205
|
for msg in messages:
|
|
168
206
|
stats.total_messages += 1
|
|
@@ -175,6 +213,10 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
|
|
|
175
213
|
# Session tracking
|
|
176
214
|
if msg.session_id:
|
|
177
215
|
sessions.add(msg.session_id)
|
|
216
|
+
session_messages[msg.session_id] += 1
|
|
217
|
+
# Track first timestamp for each session
|
|
218
|
+
if msg.session_id not in session_first_time and msg.timestamp:
|
|
219
|
+
session_first_time[msg.session_id] = msg.timestamp
|
|
178
220
|
|
|
179
221
|
# Project tracking
|
|
180
222
|
project_name = extract_project_name(msg.project)
|
|
@@ -218,6 +260,25 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
|
|
|
218
260
|
stats.model_token_usage[raw_model]["cache_create"] += msg.usage.cache_creation_tokens
|
|
219
261
|
stats.model_token_usage[raw_model]["cache_read"] += msg.usage.cache_read_tokens
|
|
220
262
|
|
|
263
|
+
# Track monthly token usage for cost breakdown
|
|
264
|
+
if msg.timestamp:
|
|
265
|
+
month_key = msg.timestamp.strftime("%Y-%m")
|
|
266
|
+
monthly_tokens[month_key]["input"] += msg.usage.input_tokens
|
|
267
|
+
monthly_tokens[month_key]["output"] += msg.usage.output_tokens
|
|
268
|
+
monthly_tokens[month_key]["cache_create"] += msg.usage.cache_creation_tokens
|
|
269
|
+
monthly_tokens[month_key]["cache_read"] += msg.usage.cache_read_tokens
|
|
270
|
+
|
|
271
|
+
# Also track per-model per-month for accurate cost calculation
|
|
272
|
+
if raw_model and raw_model != '<synthetic>':
|
|
273
|
+
monthly_model_tokens[month_key][raw_model]["input"] += msg.usage.input_tokens
|
|
274
|
+
monthly_model_tokens[month_key][raw_model]["output"] += msg.usage.output_tokens
|
|
275
|
+
monthly_model_tokens[month_key][raw_model]["cache_create"] += msg.usage.cache_creation_tokens
|
|
276
|
+
monthly_model_tokens[month_key][raw_model]["cache_read"] += msg.usage.cache_read_tokens
|
|
277
|
+
|
|
278
|
+
# Track per-session tokens for longest conversation
|
|
279
|
+
if msg.session_id:
|
|
280
|
+
session_tokens[msg.session_id] += msg.usage.total_tokens
|
|
281
|
+
|
|
221
282
|
# Tool usage
|
|
222
283
|
for tool in msg.tool_calls:
|
|
223
284
|
stats.tool_calls[tool] += 1
|
|
@@ -280,20 +341,62 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
|
|
|
280
341
|
# Streaks
|
|
281
342
|
stats.streak_longest, stats.streak_current = calculate_streaks(daily, year)
|
|
282
343
|
|
|
283
|
-
#
|
|
284
|
-
if stats.active_days > 0:
|
|
285
|
-
stats.avg_messages_per_day = stats.total_messages / stats.active_days
|
|
286
|
-
|
|
287
|
-
if stats.total_assistant_messages > 0:
|
|
288
|
-
stats.avg_tokens_per_message = stats.total_tokens / stats.total_assistant_messages
|
|
289
|
-
|
|
290
|
-
# Calculate estimated cost
|
|
344
|
+
# Calculate estimated cost first (needed for averages)
|
|
291
345
|
from .pricing import calculate_total_cost_by_model
|
|
292
346
|
if stats.model_token_usage:
|
|
293
347
|
stats.estimated_cost, stats.cost_by_model = calculate_total_cost_by_model(
|
|
294
348
|
stats.model_token_usage
|
|
295
349
|
)
|
|
296
350
|
|
|
351
|
+
# Calculate monthly costs
|
|
352
|
+
stats.monthly_tokens = dict(monthly_tokens)
|
|
353
|
+
for month_key, model_usage in monthly_model_tokens.items():
|
|
354
|
+
month_cost, _ = calculate_total_cost_by_model(dict(model_usage))
|
|
355
|
+
stats.monthly_costs[month_key] = month_cost
|
|
356
|
+
|
|
357
|
+
# Find longest conversation
|
|
358
|
+
if session_messages:
|
|
359
|
+
longest_session = max(session_messages.items(), key=lambda x: x[1])
|
|
360
|
+
stats.longest_conversation_session = longest_session[0]
|
|
361
|
+
stats.longest_conversation_messages = longest_session[1]
|
|
362
|
+
if longest_session[0] in session_tokens:
|
|
363
|
+
stats.longest_conversation_tokens = session_tokens[longest_session[0]]
|
|
364
|
+
if longest_session[0] in session_first_time:
|
|
365
|
+
stats.longest_conversation_date = session_first_time[longest_session[0]]
|
|
366
|
+
|
|
367
|
+
# Calculate time periods for averages
|
|
368
|
+
today = datetime.now()
|
|
369
|
+
if year == today.year:
|
|
370
|
+
total_days = (today - datetime(year, 1, 1)).days + 1
|
|
371
|
+
else:
|
|
372
|
+
total_days = 366 if year % 4 == 0 else 365
|
|
373
|
+
total_weeks = max(1, total_days / 7)
|
|
374
|
+
total_months = max(1, total_days / 30.44) # Average days per month
|
|
375
|
+
|
|
376
|
+
# Message averages (over total time period, not just active days)
|
|
377
|
+
if total_days > 0:
|
|
378
|
+
stats.avg_messages_per_day = stats.total_messages / total_days
|
|
379
|
+
stats.avg_messages_per_week = stats.total_messages / total_weeks
|
|
380
|
+
stats.avg_messages_per_month = stats.total_messages / total_months
|
|
381
|
+
|
|
382
|
+
# Cost averages
|
|
383
|
+
if stats.estimated_cost is not None and total_days > 0:
|
|
384
|
+
stats.avg_cost_per_day = stats.estimated_cost / total_days
|
|
385
|
+
stats.avg_cost_per_week = stats.estimated_cost / total_weeks
|
|
386
|
+
stats.avg_cost_per_month = stats.estimated_cost / total_months
|
|
387
|
+
|
|
388
|
+
# Token averages
|
|
389
|
+
if stats.total_assistant_messages > 0:
|
|
390
|
+
stats.avg_tokens_per_message = stats.total_tokens / stats.total_assistant_messages
|
|
391
|
+
|
|
392
|
+
# Code activity from Edit/Write tools
|
|
393
|
+
stats.total_edits = stats.tool_calls.get("Edit", 0)
|
|
394
|
+
stats.total_writes = stats.tool_calls.get("Write", 0)
|
|
395
|
+
total_code_changes = stats.total_edits + stats.total_writes
|
|
396
|
+
if total_days > 0:
|
|
397
|
+
stats.avg_edits_per_day = total_code_changes / total_days
|
|
398
|
+
stats.avg_edits_per_week = total_code_changes / total_weeks
|
|
399
|
+
|
|
297
400
|
return stats
|
|
298
401
|
|
|
299
402
|
|
|
@@ -357,6 +357,70 @@ def simplify_model_name(model: str) -> str:
|
|
|
357
357
|
return model
|
|
358
358
|
|
|
359
359
|
|
|
360
|
+
def create_monthly_cost_table(stats: WrappedStats) -> Panel:
|
|
361
|
+
"""Create a monthly cost breakdown table like ccusage."""
|
|
362
|
+
from .pricing import format_cost
|
|
363
|
+
|
|
364
|
+
table = Table(
|
|
365
|
+
show_header=True,
|
|
366
|
+
header_style=Style(color=COLORS["white"], bold=True),
|
|
367
|
+
border_style=Style(color=COLORS["dark"]),
|
|
368
|
+
box=None,
|
|
369
|
+
padding=(0, 1),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
table.add_column("Month", style=Style(color=COLORS["gray"]))
|
|
373
|
+
table.add_column("Input", justify="right", style=Style(color=COLORS["blue"]))
|
|
374
|
+
table.add_column("Output", justify="right", style=Style(color=COLORS["orange"]))
|
|
375
|
+
table.add_column("Cache", justify="right", style=Style(color=COLORS["purple"]))
|
|
376
|
+
table.add_column("Cost", justify="right", style=Style(color=COLORS["green"], bold=True))
|
|
377
|
+
|
|
378
|
+
# Sort months chronologically
|
|
379
|
+
sorted_months = sorted(stats.monthly_costs.keys())
|
|
380
|
+
|
|
381
|
+
for month_key in sorted_months:
|
|
382
|
+
cost = stats.monthly_costs.get(month_key, 0)
|
|
383
|
+
tokens = stats.monthly_tokens.get(month_key, {})
|
|
384
|
+
|
|
385
|
+
# Format month name
|
|
386
|
+
try:
|
|
387
|
+
month_date = datetime.strptime(month_key, "%Y-%m")
|
|
388
|
+
month_name = month_date.strftime("%b %Y")
|
|
389
|
+
except ValueError:
|
|
390
|
+
month_name = month_key
|
|
391
|
+
|
|
392
|
+
input_tokens = tokens.get("input", 0)
|
|
393
|
+
output_tokens = tokens.get("output", 0)
|
|
394
|
+
cache_tokens = tokens.get("cache_create", 0) + tokens.get("cache_read", 0)
|
|
395
|
+
|
|
396
|
+
table.add_row(
|
|
397
|
+
month_name,
|
|
398
|
+
format_tokens(input_tokens),
|
|
399
|
+
format_tokens(output_tokens),
|
|
400
|
+
format_tokens(cache_tokens),
|
|
401
|
+
format_cost(cost),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Add total row
|
|
405
|
+
if sorted_months:
|
|
406
|
+
table.add_row("", "", "", "", "") # Separator
|
|
407
|
+
table.add_row(
|
|
408
|
+
"Total",
|
|
409
|
+
format_tokens(stats.total_input_tokens),
|
|
410
|
+
format_tokens(stats.total_output_tokens),
|
|
411
|
+
format_tokens(stats.total_cache_creation_tokens + stats.total_cache_read_tokens),
|
|
412
|
+
format_cost(stats.estimated_cost) if stats.estimated_cost else "N/A",
|
|
413
|
+
style=Style(bold=True),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return Panel(
|
|
417
|
+
table,
|
|
418
|
+
title="Monthly Cost Breakdown",
|
|
419
|
+
border_style=Style(color=COLORS["green"]),
|
|
420
|
+
padding=(0, 1),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
360
424
|
def create_credits_roll(stats: WrappedStats) -> list[Text]:
|
|
361
425
|
"""Create end credits content."""
|
|
362
426
|
from .pricing import format_cost
|
|
@@ -413,7 +477,59 @@ def create_credits_roll(stats: WrappedStats) -> list[Text]:
|
|
|
413
477
|
timeline.append(" [ENTER]", style=Style(color=COLORS["dark"]))
|
|
414
478
|
frames.append(timeline)
|
|
415
479
|
|
|
416
|
-
# Frame 3:
|
|
480
|
+
# Frame 3: Averages
|
|
481
|
+
from .pricing import format_cost
|
|
482
|
+
averages = Text()
|
|
483
|
+
averages.append("\n\n\n")
|
|
484
|
+
averages.append(" A V E R A G E S\n\n", style=Style(color=COLORS["blue"], bold=True))
|
|
485
|
+
averages.append(" Messages\n", style=Style(color=COLORS["white"], bold=True))
|
|
486
|
+
averages.append(f" Per day: {stats.avg_messages_per_day:.1f}\n", style=Style(color=COLORS["gray"]))
|
|
487
|
+
averages.append(f" Per week: {stats.avg_messages_per_week:.1f}\n", style=Style(color=COLORS["gray"]))
|
|
488
|
+
averages.append(f" Per month: {stats.avg_messages_per_month:.1f}\n", style=Style(color=COLORS["gray"]))
|
|
489
|
+
if stats.estimated_cost is not None:
|
|
490
|
+
averages.append("\n Cost\n", style=Style(color=COLORS["white"], bold=True))
|
|
491
|
+
averages.append(f" Per day: {format_cost(stats.avg_cost_per_day)}\n", style=Style(color=COLORS["gray"]))
|
|
492
|
+
averages.append(f" Per week: {format_cost(stats.avg_cost_per_week)}\n", style=Style(color=COLORS["gray"]))
|
|
493
|
+
averages.append(f" Per month: {format_cost(stats.avg_cost_per_month)}\n", style=Style(color=COLORS["gray"]))
|
|
494
|
+
averages.append("\n\n")
|
|
495
|
+
averages.append(" [ENTER]", style=Style(color=COLORS["dark"]))
|
|
496
|
+
frames.append(averages)
|
|
497
|
+
|
|
498
|
+
# Frame 4: Code Activity
|
|
499
|
+
code_activity = Text()
|
|
500
|
+
code_activity.append("\n\n\n")
|
|
501
|
+
code_activity.append(" C O D E A C T I V I T Y\n\n", style=Style(color=COLORS["orange"], bold=True))
|
|
502
|
+
total_code_changes = stats.total_edits + stats.total_writes
|
|
503
|
+
code_activity.append(" File Changes\n", style=Style(color=COLORS["white"], bold=True))
|
|
504
|
+
code_activity.append(f" Edits: {stats.total_edits:,}\n", style=Style(color=COLORS["gray"]))
|
|
505
|
+
code_activity.append(f" Writes: {stats.total_writes:,}\n", style=Style(color=COLORS["gray"]))
|
|
506
|
+
code_activity.append(f" Total: {total_code_changes:,}\n", style=Style(color=COLORS["orange"], bold=True))
|
|
507
|
+
code_activity.append("\n Averages\n", style=Style(color=COLORS["white"], bold=True))
|
|
508
|
+
code_activity.append(f" Per day: {stats.avg_edits_per_day:.1f}\n", style=Style(color=COLORS["gray"]))
|
|
509
|
+
code_activity.append(f" Per week: {stats.avg_edits_per_week:.1f}\n", style=Style(color=COLORS["gray"]))
|
|
510
|
+
code_activity.append("\n\n")
|
|
511
|
+
code_activity.append(" [ENTER]", style=Style(color=COLORS["dark"]))
|
|
512
|
+
frames.append(code_activity)
|
|
513
|
+
|
|
514
|
+
# Frame 5: Longest Conversation
|
|
515
|
+
if stats.longest_conversation_messages > 0:
|
|
516
|
+
longest = Text()
|
|
517
|
+
longest.append("\n\n\n")
|
|
518
|
+
longest.append(" L O N G E S T C O N V E R S A T I O N\n\n", style=Style(color=COLORS["purple"], bold=True))
|
|
519
|
+
longest.append(f" Messages ", style=Style(color=COLORS["white"], bold=True))
|
|
520
|
+
longest.append(f"{stats.longest_conversation_messages:,}\n", style=Style(color=COLORS["purple"], bold=True))
|
|
521
|
+
if stats.longest_conversation_tokens > 0:
|
|
522
|
+
longest.append(f" Tokens ", style=Style(color=COLORS["white"], bold=True))
|
|
523
|
+
longest.append(f"{format_tokens(stats.longest_conversation_tokens)}\n", style=Style(color=COLORS["orange"], bold=True))
|
|
524
|
+
if stats.longest_conversation_date:
|
|
525
|
+
longest.append(f" Date ", style=Style(color=COLORS["white"], bold=True))
|
|
526
|
+
longest.append(f"{stats.longest_conversation_date.strftime('%B %d, %Y')}\n", style=Style(color=COLORS["gray"]))
|
|
527
|
+
longest.append("\n That's one epic coding session!\n", style=Style(color=COLORS["gray"]))
|
|
528
|
+
longest.append("\n\n")
|
|
529
|
+
longest.append(" [ENTER]", style=Style(color=COLORS["dark"]))
|
|
530
|
+
frames.append(longest)
|
|
531
|
+
|
|
532
|
+
# Frame 6: Cast (models)
|
|
417
533
|
cast = Text()
|
|
418
534
|
cast.append("\n\n\n")
|
|
419
535
|
cast.append(" S T A R R I N G\n\n", style=Style(color=COLORS["purple"], bold=True))
|
|
@@ -424,7 +540,7 @@ def create_credits_roll(stats: WrappedStats) -> list[Text]:
|
|
|
424
540
|
cast.append(" [ENTER]", style=Style(color=COLORS["dark"]))
|
|
425
541
|
frames.append(cast)
|
|
426
542
|
|
|
427
|
-
# Frame
|
|
543
|
+
# Frame 6: Projects
|
|
428
544
|
if stats.top_projects:
|
|
429
545
|
projects = Text()
|
|
430
546
|
projects.append("\n\n\n")
|
|
@@ -559,6 +675,10 @@ def render_wrapped(stats: WrappedStats, console: Console | None = None, animate:
|
|
|
559
675
|
)
|
|
560
676
|
console.print(lists)
|
|
561
677
|
|
|
678
|
+
# Monthly cost table
|
|
679
|
+
if stats.monthly_costs:
|
|
680
|
+
console.print(create_monthly_cost_table(stats))
|
|
681
|
+
|
|
562
682
|
# Insights
|
|
563
683
|
insights = Text()
|
|
564
684
|
if stats.most_active_day:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED