claude-code-wrapped 0.1.6 โ†’ 0.1.8

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.
@@ -1,3 +1,3 @@
1
1
  """Claude Code Wrapped - Your year with Claude Code, Spotify Wrapped style."""
2
2
 
3
- __version__ = "0.1.6"
3
+ __version__ = "0.1.8"
@@ -87,6 +87,7 @@ 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,
@@ -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
@@ -279,9 +283,16 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
279
283
  if msg.session_id:
280
284
  session_tokens[msg.session_id] += msg.usage.total_tokens
281
285
 
282
- # Tool usage
286
+ # Tool usage (separate MCPs from regular tools)
283
287
  for tool in msg.tool_calls:
284
- 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
285
296
 
286
297
  # Time-based stats
287
298
  if msg.timestamp:
@@ -331,6 +342,9 @@ def aggregate_stats(messages: list[Message], year: int) -> WrappedStats:
331
342
  # Top tools
332
343
  stats.top_tools = stats.tool_calls.most_common(10)
333
344
 
345
+ # Top MCPs
346
+ stats.top_mcps = stats.mcp_servers.most_common(5)
347
+
334
348
  # Top projects
335
349
  stats.top_projects = projects.most_common(5)
336
350
 
@@ -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:
@@ -595,29 +575,72 @@ def render_wrapped(stats: WrappedStats, console: Console | None = None, animate:
595
575
  wait_for_keypress()
596
576
  console.clear()
597
577
 
598
- # Dramatic stat reveals
599
- slides = [
600
- (f"{stats.total_messages:,}", "MESSAGES", "conversations with Claude", COLORS["orange"]),
601
- (str(stats.total_sessions), "SESSIONS", "coding adventures", COLORS["purple"]),
602
- (format_tokens(stats.total_tokens), "TOKENS", "processed through the AI", COLORS["green"]),
603
- (f"{stats.streak_longest}", "DAY STREAK", "your longest run", COLORS["blue"]),
604
- ]
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()
605
587
 
606
- for value, label, subtitle, color in slides:
607
- console.print(Align.center(create_dramatic_stat(value, label, subtitle, color)))
608
- wait_for_keypress()
609
- 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()
610
609
 
611
- # Personality reveal
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()
631
+
632
+ # Slide 4: Streak + Personality (merged)
612
633
  personality = determine_personality(stats)
613
- personality_text = Text()
614
- personality_text.append("\n\n\n\n")
615
- personality_text.append(f" {personality['emoji']}\n\n", style=Style(bold=True))
616
- personality_text.append(f" You are\n", style=Style(color=COLORS["gray"]))
617
- personality_text.append(f" {personality['title']}\n\n", style=Style(color=COLORS["purple"], bold=True))
618
- personality_text.append(f" {personality['description']}\n\n\n", style=Style(color=COLORS["white"]))
619
- personality_text.append(" [ENTER]", style=Style(color=COLORS["dark"]))
620
- 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))
621
644
  wait_for_keypress()
622
645
  console.clear()
623
646
 
@@ -675,6 +698,10 @@ def render_wrapped(stats: WrappedStats, console: Console | None = None, animate:
675
698
  )
676
699
  console.print(lists)
677
700
 
701
+ # MCPs (if any)
702
+ if stats.top_mcps:
703
+ console.print(create_top_list(stats.top_mcps, "MCP Servers", COLORS["purple"]))
704
+
678
705
  # Monthly cost table
679
706
  if stats.monthly_costs:
680
707
  console.print(create_monthly_cost_table(stats))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-wrapped",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.6"
3
+ version = "0.1.8"
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.6"
7
+ version = "0.1.7"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "rich" },