claude-code-wrapped 0.1.2

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 @@
1
+ 3.13
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mert Deveci
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Claude Code Wrapped
2
+
3
+ ```
4
+ ░█████╗░██╗░░░░░░█████╗░██╗░░░██╗██████╗░███████╗
5
+ ██╔══██╗██║░░░░░██╔══██╗██║░░░██║██╔══██╗██╔════╝
6
+ ██║░░╚═╝██║░░░░░███████║██║░░░██║██║░░██║█████╗░░
7
+ ██║░░██╗██║░░░░░██╔══██║██║░░░██║██║░░██║██╔══╝░░
8
+ ╚█████╔╝███████╗██║░░██║╚██████╔╝██████╔╝███████╗
9
+ ░╚════╝░╚══════╝╚═╝░░╚═╝░╚═════╝░╚═════╝░╚══════╝
10
+
11
+ C O D E W R A P P E D 2025
12
+ by Banker.so
13
+ ```
14
+
15
+ Your year with Claude Code, Spotify Wrapped style.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # Using uvx (recommended)
21
+ uvx claude-code-wrapped
22
+
23
+ # Using npx
24
+ npx claude-code-wrapped
25
+
26
+ # Or install via pip
27
+ pip install claude-code-wrapped
28
+ claude-code-wrapped
29
+ ```
30
+
31
+ Press `Enter` to advance through your personalized stats.
32
+
33
+ ## What You'll See
34
+
35
+ **Dramatic stat reveals** - one at a time, like Spotify Wrapped:
36
+ - Total messages exchanged
37
+ - Coding sessions
38
+ - Tokens processed
39
+ - Your longest streak
40
+
41
+ **GitHub-style contribution graph** - see your coding patterns at a glance
42
+
43
+ **Your coding personality** - based on your habits:
44
+ - 🦉 Night Owl
45
+ - 🔥 Streak Master
46
+ - ⚡ Terminal Warrior
47
+ - 🎨 The Refactorer
48
+ - 🚀 Empire Builder
49
+ - 🌙 Weekend Warrior
50
+ - 🎯 Perfectionist
51
+ - 💻 Dedicated Dev
52
+
53
+ **Fun facts & bloopers** - like "You coded after midnight 47 times"
54
+
55
+ **Movie-style credits** - featuring your top tools, projects, and models
56
+
57
+ ## Options
58
+
59
+ ```bash
60
+ claude-code-wrapped # Full cinematic experience
61
+ claude-code-wrapped --no-animate # Skip to dashboard view
62
+ claude-code-wrapped --json # Export stats as JSON
63
+ claude-code-wrapped 2025 # View a specific year
64
+ ```
65
+
66
+ ## Requirements
67
+
68
+ - Python 3.12+ (with uvx, pipx, or pip)
69
+ - Or Node.js 16+ (for npx)
70
+ - Claude Code installed (`~/.claude/` directory exists)
71
+
72
+ ## How It Works
73
+
74
+ Reads your local Claude Code conversation history from `~/.claude/projects/` and aggregates:
75
+ - Message counts and timestamps
76
+ - Token usage (input, output, cache)
77
+ - Tool usage (Bash, Read, Edit, etc.)
78
+ - Model preferences (Opus, Sonnet, Haiku)
79
+ - Project activity
80
+
81
+ All data stays local. Nothing is sent anywhere.
82
+
83
+ ## Privacy
84
+
85
+ This tool is completely local and privacy-focused:
86
+
87
+ - **No network requests** - All data is read from your local `~/.claude/` directory
88
+ - **No data collection** - Nothing is sent to any server
89
+ - **No API keys needed** - Works entirely offline
90
+ - **No secrets exposed** - Only aggregated stats are shown, not conversation content
91
+
92
+ ## Author
93
+
94
+ Built by [Mert Deveci](https://x.com/gm_mertd), Maker of [Banker.so](https://banker.so)
95
+
96
+ ## License
97
+
98
+ MIT - see [LICENSE](LICENSE) for details.
package/bin/cli.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, spawnSync } = require('child_process');
4
+
5
+ // Pass through all arguments
6
+ const args = process.argv.slice(2);
7
+
8
+ // Check if a command exists
9
+ function commandExists(cmd) {
10
+ const result = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
11
+ return result.status === 0;
12
+ }
13
+
14
+ // Check if Python module is installed
15
+ function pythonModuleExists() {
16
+ const result = spawnSync('python3', ['-c', 'import claude_code_wrapped'], { stdio: 'ignore' });
17
+ return result.status === 0;
18
+ }
19
+
20
+ // Try to find the best way to run the Python package
21
+ function findExecutor() {
22
+ // Try uvx first (recommended)
23
+ if (commandExists('uvx')) {
24
+ return { cmd: 'uvx', args: ['claude-code-wrapped', ...args] };
25
+ }
26
+
27
+ // Try pipx
28
+ if (commandExists('pipx')) {
29
+ return { cmd: 'pipx', args: ['run', 'claude-code-wrapped', ...args] };
30
+ }
31
+
32
+ // Try direct python module
33
+ if (pythonModuleExists()) {
34
+ return { cmd: 'python3', args: ['-m', 'claude_code_wrapped', ...args] };
35
+ }
36
+
37
+ // No executor found
38
+ console.error('claude-code-wrapped requires Python 3.12+ and one of: uvx, pipx, or pip');
39
+ console.error('');
40
+ console.error('Recommended: Install uv (https://docs.astral.sh/uv/) then run:');
41
+ console.error(' uvx claude-code-wrapped');
42
+ console.error('');
43
+ console.error('Or install with pip:');
44
+ console.error(' pip install claude-code-wrapped');
45
+ console.error(' claude-code-wrapped');
46
+ process.exit(1);
47
+ }
48
+
49
+ const executor = findExecutor();
50
+ const child = spawn(executor.cmd, executor.args, {
51
+ stdio: 'inherit',
52
+ env: process.env
53
+ });
54
+
55
+ child.on('close', (code) => {
56
+ process.exit(code || 0);
57
+ });
58
+
59
+ child.on('error', (err) => {
60
+ console.error('Failed to start:', err.message);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,3 @@
1
+ """Claude Code Wrapped - Your year with Claude Code, Spotify Wrapped style."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,102 @@
1
+ """Claude Code Wrapped - Main entry point."""
2
+
3
+ import argparse
4
+ import sys
5
+ from datetime import datetime
6
+
7
+ from rich.console import Console
8
+
9
+ from .reader import get_claude_dir, load_all_messages
10
+ from .stats import aggregate_stats
11
+ from .ui import render_wrapped
12
+
13
+
14
+ def main():
15
+ """Main entry point for Claude Code Wrapped."""
16
+ parser = argparse.ArgumentParser(
17
+ description="Claude Code Wrapped - Your year with Claude Code",
18
+ formatter_class=argparse.RawDescriptionHelpFormatter,
19
+ epilog="""
20
+ Examples:
21
+ claude-code-wrapped Show your wrapped for current year
22
+ claude-code-wrapped 2025 Show your 2025 wrapped
23
+ claude-code-wrapped --no-animate Skip animations
24
+ """,
25
+ )
26
+ parser.add_argument(
27
+ "year",
28
+ type=int,
29
+ nargs="?",
30
+ default=datetime.now().year,
31
+ help="Year to analyze (default: current year)",
32
+ )
33
+ parser.add_argument(
34
+ "--no-animate",
35
+ action="store_true",
36
+ help="Disable animations for faster display",
37
+ )
38
+ parser.add_argument(
39
+ "--json",
40
+ action="store_true",
41
+ help="Output raw stats as JSON",
42
+ )
43
+
44
+ args = parser.parse_args()
45
+ console = Console()
46
+
47
+ # Check for Claude directory
48
+ try:
49
+ claude_dir = get_claude_dir()
50
+ except FileNotFoundError as e:
51
+ console.print(f"[red]Error:[/red] {e}")
52
+ console.print("\nMake sure you have Claude Code installed and have used it at least once.")
53
+ sys.exit(1)
54
+
55
+ # Load messages
56
+ if not args.json:
57
+ console.print(f"\n[dim]Loading your Claude Code history for {args.year}...[/dim]\n")
58
+
59
+ messages = load_all_messages(claude_dir, year=args.year)
60
+
61
+ if not messages:
62
+ console.print(f"[yellow]No Claude Code activity found for {args.year}.[/yellow]")
63
+ console.print("\nTry a different year or make sure you've used Claude Code.")
64
+ sys.exit(0)
65
+
66
+ # Calculate stats
67
+ stats = aggregate_stats(messages, args.year)
68
+
69
+ # Output
70
+ if args.json:
71
+ import json
72
+ output = {
73
+ "year": stats.year,
74
+ "total_messages": stats.total_messages,
75
+ "total_user_messages": stats.total_user_messages,
76
+ "total_assistant_messages": stats.total_assistant_messages,
77
+ "total_sessions": stats.total_sessions,
78
+ "total_projects": stats.total_projects,
79
+ "total_tokens": stats.total_tokens,
80
+ "total_input_tokens": stats.total_input_tokens,
81
+ "total_output_tokens": stats.total_output_tokens,
82
+ "active_days": stats.active_days,
83
+ "streak_longest": stats.streak_longest,
84
+ "streak_current": stats.streak_current,
85
+ "most_active_hour": stats.most_active_hour,
86
+ "most_active_day": stats.most_active_day[0].isoformat() if stats.most_active_day else None,
87
+ "most_active_day_messages": stats.most_active_day[1] if stats.most_active_day else None,
88
+ "primary_model": stats.primary_model,
89
+ "top_tools": dict(stats.top_tools),
90
+ "top_projects": dict(stats.top_projects),
91
+ "hourly_distribution": stats.hourly_distribution,
92
+ "weekday_distribution": stats.weekday_distribution,
93
+ "estimated_cost_usd": stats.estimated_cost,
94
+ "cost_by_model": stats.cost_by_model,
95
+ }
96
+ print(json.dumps(output, indent=2))
97
+ else:
98
+ render_wrapped(stats, console, animate=not args.no_animate)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,179 @@
1
+ """Pricing data and cost calculation for Claude models.
2
+
3
+ Based on official Anthropic pricing as of December 2025.
4
+ Source: https://docs.anthropic.com/en/docs/about-claude/models
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class ModelPricing:
12
+ """Pricing for a Claude model in USD per million tokens."""
13
+ input_cost: float # Base input tokens
14
+ output_cost: float # Output tokens
15
+ cache_write_cost: float # 5-minute cache writes (1.25x input)
16
+ cache_read_cost: float # Cache hits & refreshes (0.1x input)
17
+
18
+
19
+ # Official Anthropic pricing as of December 2025 (USD per million tokens)
20
+ # Source: https://platform.claude.com/docs/en/about-claude/pricing
21
+ MODEL_PRICING: dict[str, ModelPricing] = {
22
+ # Claude 4.x models
23
+ "claude-opus-4-5-20251101": ModelPricing(5.0, 25.0, 6.25, 0.50),
24
+ "claude-opus-4-20250514": ModelPricing(15.0, 75.0, 18.75, 1.50),
25
+ "claude-opus-4-1-20250620": ModelPricing(15.0, 75.0, 18.75, 1.50),
26
+ "claude-sonnet-4-5-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
27
+ "claude-sonnet-4-20250514": ModelPricing(3.0, 15.0, 3.75, 0.30),
28
+ "claude-haiku-4-5-20251101": ModelPricing(1.0, 5.0, 1.25, 0.10),
29
+
30
+ # Claude 3.x models
31
+ "claude-3-5-sonnet-20241022": ModelPricing(3.0, 15.0, 3.75, 0.30),
32
+ "claude-3-5-sonnet-20240620": ModelPricing(3.0, 15.0, 3.75, 0.30),
33
+ "claude-3-5-haiku-20241022": ModelPricing(0.80, 4.0, 1.0, 0.08),
34
+ "claude-3-opus-20240229": ModelPricing(15.0, 75.0, 18.75, 1.50),
35
+ "claude-3-sonnet-20240229": ModelPricing(3.0, 15.0, 3.75, 0.30),
36
+ "claude-3-haiku-20240307": ModelPricing(0.25, 1.25, 0.30, 0.03),
37
+ }
38
+
39
+ # Simplified model name mappings for display
40
+ MODEL_FAMILY_PRICING: dict[str, ModelPricing] = {
41
+ "opus": ModelPricing(15.0, 75.0, 18.75, 1.50), # Conservative estimate
42
+ "sonnet": ModelPricing(3.0, 15.0, 3.75, 0.30),
43
+ "haiku": ModelPricing(0.80, 4.0, 1.0, 0.08), # Use 3.5 Haiku as default
44
+ }
45
+
46
+
47
+ def get_model_pricing(model_name: str | None) -> ModelPricing | None:
48
+ """Get pricing for a model by name.
49
+
50
+ Handles both full model IDs and simplified names like 'Opus', 'Sonnet', 'Haiku'.
51
+ """
52
+ if not model_name:
53
+ return None
54
+
55
+ model_lower = model_name.lower()
56
+
57
+ # Try exact match first
58
+ if model_lower in MODEL_PRICING:
59
+ return MODEL_PRICING[model_lower]
60
+
61
+ # For simple names like "Opus", "Sonnet", "Haiku", use family pricing
62
+ # This is what we get from the stats module's simplified model names
63
+ for family, pricing in MODEL_FAMILY_PRICING.items():
64
+ if model_lower == family:
65
+ return pricing
66
+
67
+ # Try partial match on full model IDs (for full model names like "claude-3-5-sonnet-...")
68
+ for model_id, pricing in MODEL_PRICING.items():
69
+ if model_lower in model_id or model_id in model_lower:
70
+ return pricing
71
+
72
+ # Fall back to family-based pricing for partial matches
73
+ for family, pricing in MODEL_FAMILY_PRICING.items():
74
+ if family in model_lower:
75
+ return pricing
76
+
77
+ return None
78
+
79
+
80
+ def calculate_cost(
81
+ input_tokens: int,
82
+ output_tokens: int,
83
+ cache_creation_tokens: int = 0,
84
+ cache_read_tokens: int = 0,
85
+ model_name: str | None = None,
86
+ ) -> float | None:
87
+ """Calculate cost in USD for a given token usage.
88
+
89
+ Args:
90
+ input_tokens: Number of input tokens (excluding cache)
91
+ output_tokens: Number of output tokens
92
+ cache_creation_tokens: Number of cache write tokens
93
+ cache_read_tokens: Number of cache read tokens
94
+ model_name: Model name or ID for pricing lookup
95
+
96
+ Returns:
97
+ Cost in USD, or None if pricing unavailable
98
+ """
99
+ pricing = get_model_pricing(model_name)
100
+ if not pricing:
101
+ return None
102
+
103
+ # Convert to millions and calculate
104
+ cost = (
105
+ (input_tokens / 1_000_000) * pricing.input_cost +
106
+ (output_tokens / 1_000_000) * pricing.output_cost +
107
+ (cache_creation_tokens / 1_000_000) * pricing.cache_write_cost +
108
+ (cache_read_tokens / 1_000_000) * pricing.cache_read_cost
109
+ )
110
+
111
+ return cost
112
+
113
+
114
+ def calculate_total_cost_by_model(
115
+ model_usage: dict[str, dict[str, int]]
116
+ ) -> tuple[float, dict[str, float]]:
117
+ """Calculate total cost across all models.
118
+
119
+ Args:
120
+ model_usage: Dict mapping model names to token counts:
121
+ {
122
+ "Sonnet": {"input": 1000, "output": 500, "cache_create": 0, "cache_read": 0},
123
+ "Opus": {"input": 500, "output": 200, ...},
124
+ }
125
+
126
+ Returns:
127
+ Tuple of (total_cost, per_model_costs)
128
+ """
129
+ total = 0.0
130
+ per_model: dict[str, float] = {}
131
+
132
+ for model_name, tokens in model_usage.items():
133
+ cost = calculate_cost(
134
+ input_tokens=tokens.get("input", 0),
135
+ output_tokens=tokens.get("output", 0),
136
+ cache_creation_tokens=tokens.get("cache_create", 0),
137
+ cache_read_tokens=tokens.get("cache_read", 0),
138
+ model_name=model_name,
139
+ )
140
+ if cost is not None:
141
+ per_model[model_name] = cost
142
+ total += cost
143
+
144
+ return total, per_model
145
+
146
+
147
+ def format_cost(cost: float | None) -> str:
148
+ """Format cost for display."""
149
+ if cost is None:
150
+ return "N/A"
151
+ if cost < 0.01:
152
+ return f"${cost:.4f}"
153
+ if cost < 1:
154
+ return f"${cost:.2f}"
155
+ if cost < 100:
156
+ return f"${cost:.2f}"
157
+ if cost < 1000:
158
+ return f"${cost:.0f}"
159
+ return f"${cost:,.0f}"
160
+
161
+
162
+ if __name__ == "__main__":
163
+ # Test pricing lookups
164
+ print("Testing pricing lookups:")
165
+ test_models = ["Opus", "Sonnet", "Haiku", "claude-3-5-sonnet-20241022", "unknown"]
166
+ for model in test_models:
167
+ pricing = get_model_pricing(model)
168
+ print(f" {model}: {pricing}")
169
+
170
+ # Test cost calculation
171
+ print("\nTesting cost calculation:")
172
+ cost = calculate_cost(
173
+ input_tokens=1_000_000,
174
+ output_tokens=500_000,
175
+ cache_creation_tokens=100_000,
176
+ cache_read_tokens=50_000,
177
+ model_name="Sonnet"
178
+ )
179
+ print(f" 1M input + 500K output + 100K cache create + 50K cache read (Sonnet): {format_cost(cost)}")