claude-code-wrapped 0.1.5 → 0.1.7

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:api.github.com)",
5
+ "WebFetch(domain:github.com)",
6
+ "WebFetch(domain:raw.githubusercontent.com)"
7
+ ]
8
+ }
9
+ }
@@ -1,3 +1,3 @@
1
1
  """Claude Code Wrapped - Your year with Claude Code, Spotify Wrapped style."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.7"
@@ -87,11 +87,31 @@ Examples:
87
87
  "most_active_day_messages": stats.most_active_day[1] if stats.most_active_day else None,
88
88
  "primary_model": stats.primary_model,
89
89
  "top_tools": dict(stats.top_tools),
90
+ "top_mcps": dict(stats.top_mcps),
90
91
  "top_projects": dict(stats.top_projects),
91
92
  "hourly_distribution": stats.hourly_distribution,
92
93
  "weekday_distribution": stats.weekday_distribution,
93
94
  "estimated_cost_usd": stats.estimated_cost,
94
95
  "cost_by_model": stats.cost_by_model,
96
+ # Averages
97
+ "avg_messages_per_day": round(stats.avg_messages_per_day, 1),
98
+ "avg_messages_per_week": round(stats.avg_messages_per_week, 1),
99
+ "avg_messages_per_month": round(stats.avg_messages_per_month, 1),
100
+ "avg_cost_per_day": round(stats.avg_cost_per_day, 2) if stats.avg_cost_per_day else None,
101
+ "avg_cost_per_week": round(stats.avg_cost_per_week, 2) if stats.avg_cost_per_week else None,
102
+ "avg_cost_per_month": round(stats.avg_cost_per_month, 2) if stats.avg_cost_per_month else None,
103
+ # Code activity
104
+ "total_edits": stats.total_edits,
105
+ "total_writes": stats.total_writes,
106
+ "avg_code_changes_per_day": round(stats.avg_edits_per_day, 1),
107
+ "avg_code_changes_per_week": round(stats.avg_edits_per_week, 1),
108
+ # Monthly breakdown
109
+ "monthly_costs": stats.monthly_costs,
110
+ "monthly_tokens": stats.monthly_tokens,
111
+ # Longest conversation
112
+ "longest_conversation_messages": stats.longest_conversation_messages,
113
+ "longest_conversation_tokens": stats.longest_conversation_tokens,
114
+ "longest_conversation_date": stats.longest_conversation_date.isoformat() if stats.longest_conversation_date else None,
95
115
  }
96
116
  print(json.dumps(output, indent=2))
97
117
  else:
@@ -52,6 +52,10 @@ class WrappedStats:
52
52
  tool_calls: Counter = field(default_factory=Counter)
53
53
  top_tools: list[tuple[str, int]] = field(default_factory=list)
54
54
 
55
+ # MCP server usage (extracted from mcp__server__tool format)
56
+ mcp_servers: Counter = field(default_factory=Counter)
57
+ top_mcps: list[tuple[str, int]] = field(default_factory=list)
58
+
55
59
  # Model usage
56
60
  models_used: Counter = field(default_factory=Counter)
57
61
  primary_model: str | None = None
@@ -71,14 +75,39 @@ class WrappedStats:
71
75
 
72
76
  # Fun stats
73
77
  longest_conversation_tokens: int = 0
74
- avg_messages_per_day: float = 0.0
75
78
  avg_tokens_per_message: float = 0.0
76
79
 
80
+ # Averages (messages)
81
+ avg_messages_per_day: float = 0.0
82
+ avg_messages_per_week: float = 0.0
83
+ avg_messages_per_month: float = 0.0
84
+
85
+ # Averages (cost)
86
+ avg_cost_per_day: float = 0.0
87
+ avg_cost_per_week: float = 0.0
88
+ avg_cost_per_month: float = 0.0
89
+
90
+ # Code activity (from Edit/Write tools)
91
+ total_edits: int = 0
92
+ total_writes: int = 0
93
+ avg_edits_per_day: float = 0.0
94
+ avg_edits_per_week: float = 0.0
95
+
77
96
  # Cost tracking (per model)
78
97
  model_token_usage: dict[str, dict[str, int]] = field(default_factory=dict)
79
98
  estimated_cost: float | None = None
80
99
  cost_by_model: dict[str, float] = field(default_factory=dict)
81
100
 
101
+ # Monthly breakdown for cost table
102
+ monthly_costs: dict[str, float] = field(default_factory=dict) # "YYYY-MM" -> cost
103
+ monthly_tokens: dict[str, dict[str, int]] = field(default_factory=dict) # "YYYY-MM" -> {input, output, ...}
104
+
105
+ # Longest conversation tracking
106
+ longest_conversation_messages: int = 0
107
+ longest_conversation_tokens: int = 0
108
+ longest_conversation_session: str | None = None
109
+ longest_conversation_date: datetime | None = None
110
+
82
111
  @property
83
112
  def total_tokens(self) -> int:
84
113
  return (
@@ -163,6 +192,19 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
163
192
  projects = Counter()
164
193
  daily = defaultdict(lambda: DailyStats(date=datetime.now()))
165
194
 
195
+ # Track monthly token usage for cost breakdown
196
+ monthly_tokens: dict[str, dict[str, int]] = defaultdict(
197
+ lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0}
198
+ )
199
+ monthly_model_tokens: dict[str, dict[str, dict[str, int]]] = defaultdict(
200
+ lambda: defaultdict(lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0})
201
+ )
202
+
203
+ # Track per-session message counts for longest conversation
204
+ session_messages: dict[str, int] = Counter()
205
+ session_tokens: dict[str, int] = Counter()
206
+ session_first_time: dict[str, datetime] = {}
207
+
166
208
  # Process each message
167
209
  for msg in messages:
168
210
  stats.total_messages += 1
@@ -175,6 +217,10 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
175
217
  # Session tracking
176
218
  if msg.session_id:
177
219
  sessions.add(msg.session_id)
220
+ session_messages[msg.session_id] += 1
221
+ # Track first timestamp for each session
222
+ if msg.session_id not in session_first_time and msg.timestamp:
223
+ session_first_time[msg.session_id] = msg.timestamp
178
224
 
179
225
  # Project tracking
180
226
  project_name = extract_project_name(msg.project)
@@ -218,9 +264,35 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
218
264
  stats.model_token_usage[raw_model]["cache_create"] += msg.usage.cache_creation_tokens
219
265
  stats.model_token_usage[raw_model]["cache_read"] += msg.usage.cache_read_tokens
220
266
 
221
- # Tool usage
267
+ # Track monthly token usage for cost breakdown
268
+ if msg.timestamp:
269
+ month_key = msg.timestamp.strftime("%Y-%m")
270
+ monthly_tokens[month_key]["input"] += msg.usage.input_tokens
271
+ monthly_tokens[month_key]["output"] += msg.usage.output_tokens
272
+ monthly_tokens[month_key]["cache_create"] += msg.usage.cache_creation_tokens
273
+ monthly_tokens[month_key]["cache_read"] += msg.usage.cache_read_tokens
274
+
275
+ # Also track per-model per-month for accurate cost calculation
276
+ if raw_model and raw_model != '<synthetic>':
277
+ monthly_model_tokens[month_key][raw_model]["input"] += msg.usage.input_tokens
278
+ monthly_model_tokens[month_key][raw_model]["output"] += msg.usage.output_tokens
279
+ monthly_model_tokens[month_key][raw_model]["cache_create"] += msg.usage.cache_creation_tokens
280
+ monthly_model_tokens[month_key][raw_model]["cache_read"] += msg.usage.cache_read_tokens
281
+
282
+ # Track per-session tokens for longest conversation
283
+ if msg.session_id:
284
+ session_tokens[msg.session_id] += msg.usage.total_tokens
285
+
286
+ # Tool usage (separate MCPs from regular tools)
222
287
  for tool in msg.tool_calls:
223
- stats.tool_calls[tool] += 1
288
+ if tool.startswith("mcp__"):
289
+ # Extract MCP server name: mcp__servername__toolname -> servername
290
+ parts = tool.split("__")
291
+ if len(parts) >= 2:
292
+ mcp_server = parts[1]
293
+ stats.mcp_servers[mcp_server] += 1
294
+ else:
295
+ stats.tool_calls[tool] += 1
224
296
 
225
297
  # Time-based stats
226
298
  if msg.timestamp:
@@ -270,6 +342,9 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
270
342
  # Top tools
271
343
  stats.top_tools = stats.tool_calls.most_common(10)
272
344
 
345
+ # Top MCPs
346
+ stats.top_mcps = stats.mcp_servers.most_common(5)
347
+
273
348
  # Top projects
274
349
  stats.top_projects = projects.most_common(5)
275
350
 
@@ -280,20 +355,62 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
280
355
  # Streaks
281
356
  stats.streak_longest, stats.streak_current = calculate_streaks(daily, year)
282
357
 
283
- # Averages
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
358
+ # Calculate estimated cost first (needed for averages)
291
359
  from .pricing import calculate_total_cost_by_model
292
360
  if stats.model_token_usage:
293
361
  stats.estimated_cost, stats.cost_by_model = calculate_total_cost_by_model(
294
362
  stats.model_token_usage
295
363
  )
296
364
 
365
+ # Calculate monthly costs
366
+ stats.monthly_tokens = dict(monthly_tokens)
367
+ for month_key, model_usage in monthly_model_tokens.items():
368
+ month_cost, _ = calculate_total_cost_by_model(dict(model_usage))
369
+ stats.monthly_costs[month_key] = month_cost
370
+
371
+ # Find longest conversation
372
+ if session_messages:
373
+ longest_session = max(session_messages.items(), key=lambda x: x[1])
374
+ stats.longest_conversation_session = longest_session[0]
375
+ stats.longest_conversation_messages = longest_session[1]
376
+ if longest_session[0] in session_tokens:
377
+ stats.longest_conversation_tokens = session_tokens[longest_session[0]]
378
+ if longest_session[0] in session_first_time:
379
+ stats.longest_conversation_date = session_first_time[longest_session[0]]
380
+
381
+ # Calculate time periods for averages
382
+ today = datetime.now()
383
+ if year == today.year:
384
+ total_days = (today - datetime(year, 1, 1)).days + 1
385
+ else:
386
+ total_days = 366 if year % 4 == 0 else 365
387
+ total_weeks = max(1, total_days / 7)
388
+ total_months = max(1, total_days / 30.44) # Average days per month
389
+
390
+ # Message averages (over total time period, not just active days)
391
+ if total_days > 0:
392
+ stats.avg_messages_per_day = stats.total_messages / total_days
393
+ stats.avg_messages_per_week = stats.total_messages / total_weeks
394
+ stats.avg_messages_per_month = stats.total_messages / total_months
395
+
396
+ # Cost averages
397
+ if stats.estimated_cost is not None and total_days > 0:
398
+ stats.avg_cost_per_day = stats.estimated_cost / total_days
399
+ stats.avg_cost_per_week = stats.estimated_cost / total_weeks
400
+ stats.avg_cost_per_month = stats.estimated_cost / total_months
401
+
402
+ # Token averages
403
+ if stats.total_assistant_messages > 0:
404
+ stats.avg_tokens_per_message = stats.total_tokens / stats.total_assistant_messages
405
+
406
+ # Code activity from Edit/Write tools
407
+ stats.total_edits = stats.tool_calls.get("Edit", 0)
408
+ stats.total_writes = stats.tool_calls.get("Write", 0)
409
+ total_code_changes = stats.total_edits + stats.total_writes
410
+ if total_days > 0:
411
+ stats.avg_edits_per_day = total_code_changes / total_days
412
+ stats.avg_edits_per_week = total_code_changes / total_weeks
413
+
297
414
  return stats
298
415
 
299
416
 
@@ -52,7 +52,7 @@ def wait_for_keypress():
52
52
  return '\n'
53
53
 
54
54
 
55
- def create_dramatic_stat(value: str, label: str, subtitle: str = "", color: str = COLORS["orange"]) -> Text:
55
+ def create_dramatic_stat(value: str, label: str, subtitle: str = "", color: str = COLORS["orange"], extra_lines: list[tuple[str, str]] = None) -> Text:
56
56
  """Create a dramatic full-screen stat reveal."""
57
57
  text = Text()
58
58
  text.append("\n\n\n\n\n")
@@ -60,6 +60,10 @@ def create_dramatic_stat(value: str, label: str, subtitle: str = "", color: str
60
60
  text.append(f"{label}\n\n", style=Style(color=COLORS["white"], bold=True))
61
61
  if subtitle:
62
62
  text.append(subtitle, style=Style(color=COLORS["gray"]))
63
+ if extra_lines:
64
+ text.append("\n\n")
65
+ for line, line_color in extra_lines:
66
+ text.append(f"{line}\n", style=Style(color=line_color))
63
67
  text.append("\n\n\n\n")
64
68
  text.append("press [ENTER] to continue", style=Style(color=COLORS["dark"]))
65
69
  return text
@@ -277,12 +281,12 @@ def determine_personality(stats: WrappedStats) -> dict:
277
281
 
278
282
 
279
283
  def get_fun_facts(stats: WrappedStats) -> list[tuple[str, str]]:
280
- """Generate fun facts / bloopers based on stats."""
284
+ """Generate fun facts / bloopers based on stats - only 3 key facts."""
281
285
  facts = []
282
286
 
283
- # Late night coding
287
+ # Late night coding (midnight to 5am)
284
288
  late_night = sum(stats.hourly_distribution[0:5])
285
- if late_night > 100:
289
+ if late_night > 0:
286
290
  facts.append(("🌙", f"You coded after midnight {late_night:,} times. Sleep is overrated."))
287
291
 
288
292
  # Most active day insight
@@ -290,35 +294,11 @@ def get_fun_facts(stats: WrappedStats) -> list[tuple[str, str]]:
290
294
  day_name = stats.most_active_day[0].strftime("%A")
291
295
  facts.append(("📅", f"Your biggest day was a {day_name}. {stats.most_active_day[1]:,} messages. Epic."))
292
296
 
293
- # Tool obsession
294
- if stats.top_tools:
295
- top_tool, count = stats.top_tools[0]
296
- facts.append(("🔧", f"You used {top_tool} {count:,} times. It's basically muscle memory now."))
297
-
298
- # If they use Opus a lot
299
- opus_count = stats.models_used.get("Opus", 0)
300
- if opus_count > 1000:
301
- facts.append(("🎭", f"You summoned Opus {opus_count:,} times. Only the best for you."))
302
-
303
297
  # Streak fact
304
- if stats.streak_longest >= 7:
298
+ if stats.streak_longest >= 1:
305
299
  facts.append(("🔥", f"Your {stats.streak_longest}-day streak was legendary. Consistency wins."))
306
300
 
307
- # Multi-project
308
- if stats.total_projects >= 3:
309
- facts.append(("🏗️", f"You juggled {stats.total_projects} projects. Multitasking champion."))
310
-
311
- # Token usage perspective
312
- if stats.total_tokens > 1_000_000_000:
313
- books = stats.total_tokens // 100_000 # ~100k tokens per book
314
- facts.append(("📚", f"You processed enough tokens for ~{books:,} books. Wow."))
315
-
316
- # Weekend warrior
317
- weekend = stats.weekday_distribution[5] + stats.weekday_distribution[6]
318
- if weekend > 1000:
319
- facts.append(("🏖️", f"Even weekends weren't safe. {weekend:,} weekend messages."))
320
-
321
- return facts[:5] # Limit to 5 facts
301
+ return facts
322
302
 
323
303
 
324
304
  def create_fun_facts_slide(facts: list[tuple[str, str]]) -> Text:
@@ -357,6 +337,70 @@ def simplify_model_name(model: str) -> str:
357
337
  return model
358
338
 
359
339
 
340
+ def create_monthly_cost_table(stats: WrappedStats) -> Panel:
341
+ """Create a monthly cost breakdown table like ccusage."""
342
+ from .pricing import format_cost
343
+
344
+ table = Table(
345
+ show_header=True,
346
+ header_style=Style(color=COLORS["white"], bold=True),
347
+ border_style=Style(color=COLORS["dark"]),
348
+ box=None,
349
+ padding=(0, 1),
350
+ )
351
+
352
+ table.add_column("Month", style=Style(color=COLORS["gray"]))
353
+ table.add_column("Input", justify="right", style=Style(color=COLORS["blue"]))
354
+ table.add_column("Output", justify="right", style=Style(color=COLORS["orange"]))
355
+ table.add_column("Cache", justify="right", style=Style(color=COLORS["purple"]))
356
+ table.add_column("Cost", justify="right", style=Style(color=COLORS["green"], bold=True))
357
+
358
+ # Sort months chronologically
359
+ sorted_months = sorted(stats.monthly_costs.keys())
360
+
361
+ for month_key in sorted_months:
362
+ cost = stats.monthly_costs.get(month_key, 0)
363
+ tokens = stats.monthly_tokens.get(month_key, {})
364
+
365
+ # Format month name
366
+ try:
367
+ month_date = datetime.strptime(month_key, "%Y-%m")
368
+ month_name = month_date.strftime("%b %Y")
369
+ except ValueError:
370
+ month_name = month_key
371
+
372
+ input_tokens = tokens.get("input", 0)
373
+ output_tokens = tokens.get("output", 0)
374
+ cache_tokens = tokens.get("cache_create", 0) + tokens.get("cache_read", 0)
375
+
376
+ table.add_row(
377
+ month_name,
378
+ format_tokens(input_tokens),
379
+ format_tokens(output_tokens),
380
+ format_tokens(cache_tokens),
381
+ format_cost(cost),
382
+ )
383
+
384
+ # Add total row
385
+ if sorted_months:
386
+ table.add_row("", "", "", "", "") # Separator
387
+ table.add_row(
388
+ "Total",
389
+ format_tokens(stats.total_input_tokens),
390
+ format_tokens(stats.total_output_tokens),
391
+ format_tokens(stats.total_cache_creation_tokens + stats.total_cache_read_tokens),
392
+ format_cost(stats.estimated_cost) if stats.estimated_cost else "N/A",
393
+ style=Style(bold=True),
394
+ )
395
+
396
+ return Panel(
397
+ table,
398
+ title="Monthly Cost Breakdown",
399
+ border_style=Style(color=COLORS["green"]),
400
+ padding=(0, 1),
401
+ )
402
+
403
+
360
404
  def create_credits_roll(stats: WrappedStats) -> list[Text]:
361
405
  """Create end credits content."""
362
406
  from .pricing import format_cost
@@ -413,7 +457,59 @@ def create_credits_roll(stats: WrappedStats) -> list[Text]:
413
457
  timeline.append(" [ENTER]", style=Style(color=COLORS["dark"]))
414
458
  frames.append(timeline)
415
459
 
416
- # Frame 3: Cast (models)
460
+ # Frame 3: Averages
461
+ from .pricing import format_cost
462
+ averages = Text()
463
+ averages.append("\n\n\n")
464
+ averages.append(" A V E R A G E S\n\n", style=Style(color=COLORS["blue"], bold=True))
465
+ averages.append(" Messages\n", style=Style(color=COLORS["white"], bold=True))
466
+ averages.append(f" Per day: {stats.avg_messages_per_day:.1f}\n", style=Style(color=COLORS["gray"]))
467
+ averages.append(f" Per week: {stats.avg_messages_per_week:.1f}\n", style=Style(color=COLORS["gray"]))
468
+ averages.append(f" Per month: {stats.avg_messages_per_month:.1f}\n", style=Style(color=COLORS["gray"]))
469
+ if stats.estimated_cost is not None:
470
+ averages.append("\n Cost\n", style=Style(color=COLORS["white"], bold=True))
471
+ averages.append(f" Per day: {format_cost(stats.avg_cost_per_day)}\n", style=Style(color=COLORS["gray"]))
472
+ averages.append(f" Per week: {format_cost(stats.avg_cost_per_week)}\n", style=Style(color=COLORS["gray"]))
473
+ averages.append(f" Per month: {format_cost(stats.avg_cost_per_month)}\n", style=Style(color=COLORS["gray"]))
474
+ averages.append("\n\n")
475
+ averages.append(" [ENTER]", style=Style(color=COLORS["dark"]))
476
+ frames.append(averages)
477
+
478
+ # Frame 4: Code Activity
479
+ code_activity = Text()
480
+ code_activity.append("\n\n\n")
481
+ code_activity.append(" C O D E A C T I V I T Y\n\n", style=Style(color=COLORS["orange"], bold=True))
482
+ total_code_changes = stats.total_edits + stats.total_writes
483
+ code_activity.append(" File Changes\n", style=Style(color=COLORS["white"], bold=True))
484
+ code_activity.append(f" Edits: {stats.total_edits:,}\n", style=Style(color=COLORS["gray"]))
485
+ code_activity.append(f" Writes: {stats.total_writes:,}\n", style=Style(color=COLORS["gray"]))
486
+ code_activity.append(f" Total: {total_code_changes:,}\n", style=Style(color=COLORS["orange"], bold=True))
487
+ code_activity.append("\n Averages\n", style=Style(color=COLORS["white"], bold=True))
488
+ code_activity.append(f" Per day: {stats.avg_edits_per_day:.1f}\n", style=Style(color=COLORS["gray"]))
489
+ code_activity.append(f" Per week: {stats.avg_edits_per_week:.1f}\n", style=Style(color=COLORS["gray"]))
490
+ code_activity.append("\n\n")
491
+ code_activity.append(" [ENTER]", style=Style(color=COLORS["dark"]))
492
+ frames.append(code_activity)
493
+
494
+ # Frame 5: Longest Conversation
495
+ if stats.longest_conversation_messages > 0:
496
+ longest = Text()
497
+ longest.append("\n\n\n")
498
+ 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))
499
+ longest.append(f" Messages ", style=Style(color=COLORS["white"], bold=True))
500
+ longest.append(f"{stats.longest_conversation_messages:,}\n", style=Style(color=COLORS["purple"], bold=True))
501
+ if stats.longest_conversation_tokens > 0:
502
+ longest.append(f" Tokens ", style=Style(color=COLORS["white"], bold=True))
503
+ longest.append(f"{format_tokens(stats.longest_conversation_tokens)}\n", style=Style(color=COLORS["orange"], bold=True))
504
+ if stats.longest_conversation_date:
505
+ longest.append(f" Date ", style=Style(color=COLORS["white"], bold=True))
506
+ longest.append(f"{stats.longest_conversation_date.strftime('%B %d, %Y')}\n", style=Style(color=COLORS["gray"]))
507
+ longest.append("\n That's one epic coding session!\n", style=Style(color=COLORS["gray"]))
508
+ longest.append("\n\n")
509
+ longest.append(" [ENTER]", style=Style(color=COLORS["dark"]))
510
+ frames.append(longest)
511
+
512
+ # Frame 6: Cast (models)
417
513
  cast = Text()
418
514
  cast.append("\n\n\n")
419
515
  cast.append(" S T A R R I N G\n\n", style=Style(color=COLORS["purple"], bold=True))
@@ -424,7 +520,7 @@ def create_credits_roll(stats: WrappedStats) -> list[Text]:
424
520
  cast.append(" [ENTER]", style=Style(color=COLORS["dark"]))
425
521
  frames.append(cast)
426
522
 
427
- # Frame 4: Projects
523
+ # Frame 6: Projects
428
524
  if stats.top_projects:
429
525
  projects = Text()
430
526
  projects.append("\n\n\n")
@@ -479,29 +575,72 @@ def render_wrapped(stats: WrappedStats, console: Console | None = None, animate:
479
575
  wait_for_keypress()
480
576
  console.clear()
481
577
 
482
- # Dramatic stat reveals
483
- slides = [
484
- (f"{stats.total_messages:,}", "MESSAGES", "conversations with Claude", COLORS["orange"]),
485
- (str(stats.total_sessions), "SESSIONS", "coding adventures", COLORS["purple"]),
486
- (format_tokens(stats.total_tokens), "TOKENS", "processed through the AI", COLORS["green"]),
487
- (f"{stats.streak_longest}", "DAY STREAK", "your longest run", COLORS["blue"]),
488
- ]
578
+ # Slide 1: Messages with date range
579
+ first_date = stats.first_message_date.strftime("%d %B") if stats.first_message_date else "the beginning"
580
+ last_date = stats.last_message_date.strftime("%d %B %Y") if stats.last_message_date else "today"
581
+ messages_subtitle = f"From {first_date} to {last_date}"
582
+ console.print(Align.center(create_dramatic_stat(
583
+ f"{stats.total_messages:,}", "MESSAGES", messages_subtitle, COLORS["orange"]
584
+ )))
585
+ wait_for_keypress()
586
+ console.clear()
489
587
 
490
- for value, label, subtitle, color in slides:
491
- console.print(Align.center(create_dramatic_stat(value, label, subtitle, color)))
492
- wait_for_keypress()
493
- console.clear()
588
+ # Slide 2: Averages
589
+ from .pricing import format_cost
590
+ averages_text = Text()
591
+ averages_text.append("\n\n\n\n")
592
+ averages_text.append("On average, you sent\n\n", style=Style(color=COLORS["gray"]))
593
+ averages_text.append(f"{stats.avg_messages_per_day:.0f}", style=Style(color=COLORS["orange"], bold=True))
594
+ averages_text.append(" messages per day\n", style=Style(color=COLORS["white"]))
595
+ averages_text.append(f"{stats.avg_messages_per_week:.0f}", style=Style(color=COLORS["blue"], bold=True))
596
+ averages_text.append(" messages per week\n", style=Style(color=COLORS["white"]))
597
+ averages_text.append(f"{stats.avg_messages_per_month:.0f}", style=Style(color=COLORS["purple"], bold=True))
598
+ averages_text.append(" messages per month\n\n", style=Style(color=COLORS["white"]))
599
+ if stats.estimated_cost is not None:
600
+ averages_text.append("Costing about ", style=Style(color=COLORS["gray"]))
601
+ averages_text.append(f"{format_cost(stats.avg_cost_per_day)}/day", style=Style(color=COLORS["green"], bold=True))
602
+ averages_text.append(f" · {format_cost(stats.avg_cost_per_week)}/week", style=Style(color=COLORS["green"]))
603
+ averages_text.append(f" · {format_cost(stats.avg_cost_per_month)}/month\n", style=Style(color=COLORS["green"]))
604
+ averages_text.append("\n\n\n")
605
+ averages_text.append("press [ENTER] to continue", style=Style(color=COLORS["dark"]))
606
+ console.print(Align.center(averages_text))
607
+ wait_for_keypress()
608
+ console.clear()
609
+
610
+ # Slide 3: Tokens
611
+ def format_tokens_dramatic(tokens: int) -> str:
612
+ if tokens >= 1_000_000_000:
613
+ return f"{tokens / 1_000_000_000:.1f} Bn"
614
+ if tokens >= 1_000_000:
615
+ return f"{tokens / 1_000_000:.0f} M"
616
+ if tokens >= 1_000:
617
+ return f"{tokens / 1_000:.0f} K"
618
+ return str(tokens)
619
+
620
+ tokens_text = Text()
621
+ tokens_text.append("\n\n\n\n\n")
622
+ tokens_text.append("That's\n\n", style=Style(color=COLORS["gray"]))
623
+ tokens_text.append(f"{format_tokens_dramatic(stats.total_tokens)}\n", style=Style(color=COLORS["green"], bold=True))
624
+ tokens_text.append("TOKENS\n\n", style=Style(color=COLORS["white"], bold=True))
625
+ tokens_text.append("processed through the AI", style=Style(color=COLORS["gray"]))
626
+ tokens_text.append("\n\n\n\n")
627
+ tokens_text.append("press [ENTER] to continue", style=Style(color=COLORS["dark"]))
628
+ console.print(Align.center(tokens_text))
629
+ wait_for_keypress()
630
+ console.clear()
494
631
 
495
- # Personality reveal
632
+ # Slide 4: Streak + Personality (merged)
496
633
  personality = determine_personality(stats)
497
- personality_text = Text()
498
- personality_text.append("\n\n\n\n")
499
- personality_text.append(f" {personality['emoji']}\n\n", style=Style(bold=True))
500
- personality_text.append(f" You are\n", style=Style(color=COLORS["gray"]))
501
- personality_text.append(f" {personality['title']}\n\n", style=Style(color=COLORS["purple"], bold=True))
502
- personality_text.append(f" {personality['description']}\n\n\n", style=Style(color=COLORS["white"]))
503
- personality_text.append(" [ENTER]", style=Style(color=COLORS["dark"]))
504
- console.print(Align.center(personality_text))
634
+ streak_text = Text()
635
+ streak_text.append("\n\n\n\n")
636
+ streak_text.append(f"{stats.streak_longest}\n", style=Style(color=COLORS["blue"], bold=True))
637
+ streak_text.append("DAY STREAK\n\n", style=Style(color=COLORS["white"], bold=True))
638
+ streak_text.append(f"{personality['emoji']} ", style=Style(bold=True))
639
+ streak_text.append(f"{personality['title']}\n", style=Style(color=COLORS["purple"], bold=True))
640
+ streak_text.append(f"{personality['description']}\n", style=Style(color=COLORS["gray"]))
641
+ streak_text.append("\n\n\n")
642
+ streak_text.append("press [ENTER] to continue", style=Style(color=COLORS["dark"]))
643
+ console.print(Align.center(streak_text))
505
644
  wait_for_keypress()
506
645
  console.clear()
507
646
 
@@ -559,6 +698,14 @@ def render_wrapped(stats: WrappedStats, console: Console | None = None, animate:
559
698
  )
560
699
  console.print(lists)
561
700
 
701
+ # MCPs (if any)
702
+ if stats.top_mcps:
703
+ console.print(create_top_list(stats.top_mcps, "MCP Servers", COLORS["purple"]))
704
+
705
+ # Monthly cost table
706
+ if stats.monthly_costs:
707
+ console.print(create_monthly_cost_table(stats))
708
+
562
709
  # Insights
563
710
  insights = Text()
564
711
  if stats.most_active_day:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-wrapped",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Your year with Claude Code - Spotify Wrapped style terminal experience",
5
5
  "bin": {
6
6
  "claude-code-wrapped": "./bin/cli.js"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-wrapped"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "Your year with Claude Code - Spotify Wrapped style terminal experience"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
package/uv.lock CHANGED
@@ -4,7 +4,7 @@ requires-python = ">=3.12"
4
4
 
5
5
  [[package]]
6
6
  name = "claude-code-wrapped"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "rich" },