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.
- package/.claude/settings.local.json +9 -0
- package/claude_code_wrapped/__init__.py +1 -1
- package/claude_code_wrapped/main.py +20 -0
- package/claude_code_wrapped/stats.py +128 -11
- package/claude_code_wrapped/ui.py +199 -52
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/uv.lock +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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 >
|
|
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 >=
|
|
298
|
+
if stats.streak_longest >= 1:
|
|
305
299
|
facts.append(("🔥", f"Your {stats.streak_longest}-day streak was legendary. Consistency wins."))
|
|
306
300
|
|
|
307
|
-
|
|
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:
|
|
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
|
|
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
|
-
#
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
632
|
+
# Slide 4: Streak + Personality (merged)
|
|
496
633
|
personality = determine_personality(stats)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
package/pyproject.toml
CHANGED