ccwrap 0.1.0
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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +181 -0
- package/dist/data/commentary.d.ts +13 -0
- package/dist/data/commentary.js +102 -0
- package/dist/data/parser.d.ts +2 -0
- package/dist/data/parser.js +358 -0
- package/dist/data/types.d.ts +109 -0
- package/dist/data/types.js +1 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +60 -0
- package/dist/video/Composition.d.ts +8 -0
- package/dist/video/Composition.js +50 -0
- package/dist/video/Root.d.ts +2 -0
- package/dist/video/Root.js +81 -0
- package/dist/video/components/AnimatedNumber.d.ts +10 -0
- package/dist/video/components/AnimatedNumber.js +16 -0
- package/dist/video/components/FadeIn.d.ts +8 -0
- package/dist/video/components/FadeIn.js +18 -0
- package/dist/video/components/GlowOrb.d.ts +8 -0
- package/dist/video/components/GlowOrb.js +19 -0
- package/dist/video/components/ParticleField.d.ts +5 -0
- package/dist/video/components/ParticleField.js +36 -0
- package/dist/video/index.d.ts +1 -0
- package/dist/video/index.js +3 -0
- package/dist/video/slides/ArchetypeSlide.d.ts +7 -0
- package/dist/video/slides/ArchetypeSlide.js +34 -0
- package/dist/video/slides/BusiestDaySlide.d.ts +7 -0
- package/dist/video/slides/BusiestDaySlide.js +23 -0
- package/dist/video/slides/CostSlide.d.ts +7 -0
- package/dist/video/slides/CostSlide.js +12 -0
- package/dist/video/slides/IntroSlide.d.ts +5 -0
- package/dist/video/slides/IntroSlide.js +21 -0
- package/dist/video/slides/ModelSlide.d.ts +7 -0
- package/dist/video/slides/ModelSlide.js +45 -0
- package/dist/video/slides/PeakHoursSlide.d.ts +7 -0
- package/dist/video/slides/PeakHoursSlide.js +48 -0
- package/dist/video/slides/SessionSlide.d.ts +7 -0
- package/dist/video/slides/SessionSlide.js +22 -0
- package/dist/video/slides/SummarySlide.d.ts +7 -0
- package/dist/video/slides/SummarySlide.js +52 -0
- package/dist/video/slides/TokensSlide.d.ts +7 -0
- package/dist/video/slides/TokensSlide.js +25 -0
- package/dist/video/styles.d.ts +45 -0
- package/dist/video/styles.js +84 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ccwrapped contributors
|
|
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,99 @@
|
|
|
1
|
+
# ccwrap
|
|
2
|
+
|
|
3
|
+
Your Claude Code usage stats, meme-ified into a shareable video. Like Spotify Wrapped, but for your AI coding assistant.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ccwrap
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g ccwrap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Last month (default)
|
|
21
|
+
ccwrap
|
|
22
|
+
|
|
23
|
+
# Last week
|
|
24
|
+
ccwrap --period week
|
|
25
|
+
|
|
26
|
+
# Last quarter
|
|
27
|
+
ccwrap --period quarter
|
|
28
|
+
|
|
29
|
+
# All time
|
|
30
|
+
ccwrap --period all
|
|
31
|
+
|
|
32
|
+
# Custom date range
|
|
33
|
+
ccwrap --since 2025-06-01
|
|
34
|
+
ccwrap --since 2025-06-01 --until 2025-12-31
|
|
35
|
+
|
|
36
|
+
# Dark mode - environmental impact commentary
|
|
37
|
+
ccwrap --mode dark
|
|
38
|
+
|
|
39
|
+
# Custom output path
|
|
40
|
+
ccwrap -o my-wrapped.mp4
|
|
41
|
+
|
|
42
|
+
# Just print stats, no video
|
|
43
|
+
ccwrap --stats-only
|
|
44
|
+
|
|
45
|
+
# Output stats as JSON
|
|
46
|
+
ccwrap --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Time Periods
|
|
50
|
+
|
|
51
|
+
By default, ccwrap recaps the **last 30 days**. Use `--period` for presets or `--since`/`--until` for custom ranges.
|
|
52
|
+
|
|
53
|
+
| Flag | Period |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `--period week` | Last 7 days |
|
|
56
|
+
| `--period month` | Last 30 days (default) |
|
|
57
|
+
| `--period quarter` | Last 90 days |
|
|
58
|
+
| `--period all` | All time |
|
|
59
|
+
| `--since YYYY-MM-DD` | Custom start date (until today) |
|
|
60
|
+
| `--since ... --until ...` | Custom date range |
|
|
61
|
+
|
|
62
|
+
## AI Commentary
|
|
63
|
+
|
|
64
|
+
Set `ANTHROPIC_API_KEY` to get AI-generated personalized commentary on each slide. Without it, pre-written fallback quips are used.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
68
|
+
ccwrap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Modes
|
|
72
|
+
|
|
73
|
+
| Sassy (default) | Dark (`--mode dark`) |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Purple/cyan neon aesthetic | Blood red/stone aesthetic |
|
|
76
|
+
| Meme humor ("that's 50 coffees") | Environmental impact ("4.5L of water used") |
|
|
77
|
+
| Affectionate roasting | Uncomfortable truths, but funny |
|
|
78
|
+
|
|
79
|
+
## Video Slides
|
|
80
|
+
|
|
81
|
+
1. **Intro** - Claude Code Wrapped with period-aware tagline
|
|
82
|
+
2. **Tokens** - Total tokens with counting animation
|
|
83
|
+
3. **Cost** - Dollar amount + fun equivalents
|
|
84
|
+
4. **Model** - Favorite model reveal + usage breakdown
|
|
85
|
+
5. **Busiest Day** - Peak day with activity chart
|
|
86
|
+
6. **Sessions** - Marathon stats, streaks, scores
|
|
87
|
+
7. **Peak Hours** - 24h activity histogram
|
|
88
|
+
8. **Archetype** - Your coding personality
|
|
89
|
+
9. **Summary** - Shareable card with all stats
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Node.js >= 18
|
|
94
|
+
- Chrome/Chromium (auto-downloaded by Remotion if not found)
|
|
95
|
+
- Claude Code usage data in `~/.claude/projects/`
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
[MIT](LICENSE)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { loadAndComputeStats } from "./data/parser.js";
|
|
6
|
+
import { generateCommentary } from "./data/commentary.js";
|
|
7
|
+
import { renderVideo } from "./render.js";
|
|
8
|
+
function resolveDateRange(options) {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
11
|
+
if (options.since && options.period) {
|
|
12
|
+
console.error(chalk.red("Error: --period and --since are mutually exclusive. Use one or the other."));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Default --period to "month" when neither --period nor --since is provided
|
|
16
|
+
if (!options.period && !options.since) {
|
|
17
|
+
options.period = "month";
|
|
18
|
+
}
|
|
19
|
+
if (options.until && !options.since) {
|
|
20
|
+
console.error(chalk.red("Error: --until requires --since."));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// Custom date range
|
|
24
|
+
if (options.since) {
|
|
25
|
+
const since = new Date(options.since + "T00:00:00");
|
|
26
|
+
if (isNaN(since.getTime())) {
|
|
27
|
+
console.error(chalk.red(`Error: Invalid --since date "${options.since}". Use YYYY-MM-DD format.`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
let until = today;
|
|
31
|
+
if (options.until) {
|
|
32
|
+
until = new Date(options.until + "T23:59:59.999");
|
|
33
|
+
if (isNaN(until.getTime())) {
|
|
34
|
+
console.error(chalk.red(`Error: Invalid --until date "${options.until}". Use YYYY-MM-DD format.`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Clamp future dates
|
|
39
|
+
if (until > today)
|
|
40
|
+
until = today;
|
|
41
|
+
if (since > today) {
|
|
42
|
+
console.error(chalk.red("Error: --since date is in the future."));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (since > until) {
|
|
46
|
+
console.error(chalk.red("Error: --since date is after --until date."));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const fmtOpts = { month: "short", day: "numeric" };
|
|
50
|
+
const sinceStr = since.toLocaleDateString("en-US", fmtOpts);
|
|
51
|
+
const untilYear = until.getFullYear();
|
|
52
|
+
const sinceYear = since.getFullYear();
|
|
53
|
+
const untilStr = until.toLocaleDateString("en-US", { ...fmtOpts, year: sinceYear !== untilYear ? "numeric" : undefined });
|
|
54
|
+
const periodLabel = `${sinceStr} – ${untilStr}, ${untilYear}`;
|
|
55
|
+
return { range: { since, until }, periodLabel };
|
|
56
|
+
}
|
|
57
|
+
// Preset period
|
|
58
|
+
const period = (["week", "month", "quarter", "all"].includes(options.period ?? "") ? options.period : "month");
|
|
59
|
+
if (period === "all") {
|
|
60
|
+
return { range: undefined, periodLabel: "Your all-time" };
|
|
61
|
+
}
|
|
62
|
+
const daysBack = period === "week" ? 7 : period === "quarter" ? 90 : 30;
|
|
63
|
+
const since = new Date(today);
|
|
64
|
+
since.setDate(since.getDate() - daysBack);
|
|
65
|
+
since.setHours(0, 0, 0, 0);
|
|
66
|
+
const labels = {
|
|
67
|
+
week: "Your week",
|
|
68
|
+
month: "Your month",
|
|
69
|
+
quarter: "Your quarter",
|
|
70
|
+
};
|
|
71
|
+
return { range: { since, until: today }, periodLabel: labels[period] };
|
|
72
|
+
}
|
|
73
|
+
const banner = `
|
|
74
|
+
██████╗ ██████╗██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗
|
|
75
|
+
██╔════╝██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
|
|
76
|
+
██║ ██║ ██║ █╗ ██║██████╔╝███████║██████╔╝██████╔╝█████╗ ██║ ██║
|
|
77
|
+
██║ ██║ ██║███╗██║██╔══██╗██╔══██║██╔═══╝ ██╔═══╝ ██╔══╝ ██║ ██║
|
|
78
|
+
╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
|
|
79
|
+
╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
|
|
80
|
+
`;
|
|
81
|
+
const darkBanner = `
|
|
82
|
+
██████╗ ██████╗██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗
|
|
83
|
+
██╔════╝██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
|
|
84
|
+
██║ ██║ ██║ █╗ ██║██████╔╝███████║██████╔╝██████╔╝█████╗ ██║ ██║
|
|
85
|
+
██║ ██║ ██║███╗██║██╔══██╗██╔══██║██╔═══╝ ██╔═══╝ ██╔══╝ ██║ ██║
|
|
86
|
+
╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
|
|
87
|
+
╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
|
|
88
|
+
`;
|
|
89
|
+
program
|
|
90
|
+
.name("ccwrap")
|
|
91
|
+
.description("Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video")
|
|
92
|
+
.version("0.1.0")
|
|
93
|
+
.option("-o, --output <path>", "Output video file path", "ccwrapped.mp4")
|
|
94
|
+
.option("-m, --mode <mode>", "Commentary mode: sassy or dark", "sassy")
|
|
95
|
+
.option("-p, --period <period>", "Time period: week, month, quarter, all (default: month)")
|
|
96
|
+
.option("--since <date>", "Start date (YYYY-MM-DD), mutually exclusive with --period")
|
|
97
|
+
.option("--until <date>", "End date (YYYY-MM-DD), requires --since")
|
|
98
|
+
.option("--json", "Output stats as JSON instead of video")
|
|
99
|
+
.option("--stats-only", "Print stats to console without rendering video")
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const mode = options.mode === "dark" ? "dark" : "sassy";
|
|
102
|
+
const isDark = mode === "dark";
|
|
103
|
+
console.log(isDark ? chalk.red(darkBanner) : chalk.hex("#7c3aed")(banner));
|
|
104
|
+
console.log(chalk.gray(isDark
|
|
105
|
+
? " Your AI usage. The planet remembers.\n"
|
|
106
|
+
: " Your AI-assisted coding, recapped.\n"));
|
|
107
|
+
// Resolve date range
|
|
108
|
+
const { range, periodLabel } = resolveDateRange(options);
|
|
109
|
+
// Step 1: Load and compute stats
|
|
110
|
+
const periodHint = range
|
|
111
|
+
? chalk.gray(` (${periodLabel})`)
|
|
112
|
+
: "";
|
|
113
|
+
const spinner = ora({ text: `Scanning Claude Code usage data...${periodHint}`, color: isDark ? "red" : "magenta" }).start();
|
|
114
|
+
let stats;
|
|
115
|
+
try {
|
|
116
|
+
stats = await loadAndComputeStats(range, periodLabel);
|
|
117
|
+
spinner.succeed(chalk.green("Usage data loaded!"));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
spinner.fail(chalk.red(`Failed to load data: ${err.message}`));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
// Print summary
|
|
124
|
+
console.log();
|
|
125
|
+
const h = isDark ? chalk.red : chalk.hex("#7c3aed");
|
|
126
|
+
console.log(h("═══ Your Stats ═══"));
|
|
127
|
+
console.log(chalk.white(` Tokens: ${chalk.hex("#f59e0b").bold(stats.totalTokens.toLocaleString())}`));
|
|
128
|
+
console.log(chalk.white(` Cost: ${chalk.hex("#10b981").bold(`$${stats.totalCost.toFixed(2)}`)}`));
|
|
129
|
+
console.log(chalk.white(` Sessions: ${chalk.hex("#7c3aed").bold(stats.totalSessions.toString())}`));
|
|
130
|
+
console.log(chalk.white(` Messages: ${chalk.hex("#ec4899").bold(stats.totalMessages.toLocaleString())}`));
|
|
131
|
+
console.log(chalk.white(` Days Active: ${chalk.hex("#06b6d4").bold(stats.totalDaysActive.toString())}`));
|
|
132
|
+
console.log(chalk.white(` Fav Model: ${chalk.hex("#f43f5e").bold(stats.favoriteModel)}`));
|
|
133
|
+
console.log(chalk.white(` Archetype: ${h.bold(stats.archetype)}`));
|
|
134
|
+
if (isDark) {
|
|
135
|
+
console.log(chalk.white(` Water: ${chalk.hex("#06b6d4").bold(`${stats.waterLiters.toFixed(1)}L`)}`));
|
|
136
|
+
console.log(chalk.white(` CO₂: ${chalk.gray.bold(`${(stats.co2Grams / 1000).toFixed(1)}kg`)}`));
|
|
137
|
+
console.log(chalk.white(` Energy: ${chalk.hex("#f59e0b").bold(`${stats.kwhUsed.toFixed(1)} kWh`)}`));
|
|
138
|
+
}
|
|
139
|
+
console.log(chalk.gray(` "${stats.archetypeDescription}"`));
|
|
140
|
+
console.log();
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (options.statsOnly) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Step 2: Generate AI commentary
|
|
149
|
+
const commentarySpinner = ora({
|
|
150
|
+
text: isDark ? "Generating dark commentary..." : "Generating sassy commentary...",
|
|
151
|
+
color: isDark ? "red" : "magenta",
|
|
152
|
+
}).start();
|
|
153
|
+
let commentary;
|
|
154
|
+
try {
|
|
155
|
+
commentary = await generateCommentary(stats, mode);
|
|
156
|
+
commentarySpinner.succeed(chalk.green("Commentary generated!"));
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
commentarySpinner.fail(chalk.yellow(`Commentary generation failed, using fallbacks`));
|
|
160
|
+
// Fallbacks are handled inside generateCommentary
|
|
161
|
+
commentary = await generateCommentary(stats, mode);
|
|
162
|
+
}
|
|
163
|
+
// Step 3: Render video
|
|
164
|
+
const renderSpinner = ora({ text: "Rendering your Wrapped video...", color: isDark ? "red" : "magenta" }).start();
|
|
165
|
+
try {
|
|
166
|
+
const outputPath = await renderVideo(stats, commentary, mode, options.output);
|
|
167
|
+
renderSpinner.succeed(chalk.green(`Video rendered!`));
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(h(` 🎬 ${chalk.white.bold(outputPath)}`));
|
|
170
|
+
console.log(chalk.gray(isDark
|
|
171
|
+
? " Share it and make your coworkers contemplate their carbon footprint."
|
|
172
|
+
: " Share it in Slack and flex on your coworkers."));
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
renderSpinner.fail(chalk.red(`Render failed: ${err.message}`));
|
|
177
|
+
console.error(err);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
program.parse();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WrappedStats } from "./types";
|
|
2
|
+
export interface Commentary {
|
|
3
|
+
tokensLine: string;
|
|
4
|
+
costLine: string;
|
|
5
|
+
modelLine: string;
|
|
6
|
+
busiestDayLine: string;
|
|
7
|
+
sessionLine: string;
|
|
8
|
+
peakHoursLine: string;
|
|
9
|
+
archetypeLine: string;
|
|
10
|
+
summaryLine: string;
|
|
11
|
+
}
|
|
12
|
+
export type Mode = "sassy" | "dark";
|
|
13
|
+
export declare function generateCommentary(stats: WrappedStats, mode: Mode): Promise<Commentary>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
function buildPrompt(stats, mode) {
|
|
3
|
+
const modeDescription = mode === "dark"
|
|
4
|
+
? `DARK MODE: Your commentary should be darkly humorous with environmental/existential undertones.
|
|
5
|
+
Think: water usage, carbon footprint, e-waste, heat death of the universe, humanity's dependence on AI.
|
|
6
|
+
Examples of the vibe:
|
|
7
|
+
- "That's 100 gallons of water gone. We're not finding Nemo now."
|
|
8
|
+
- "Those tokens generated enough heat to melt a small glacier. You're welcome, polar bears."
|
|
9
|
+
- "You spent $X talking to a machine. Your therapist charges less and at least pretends to care."
|
|
10
|
+
- "Your longest session was 8 hours. The data center fans were begging for mercy."
|
|
11
|
+
Make it funny but with that uncomfortable kernel of truth. Dark comedy, not depressing.`
|
|
12
|
+
: `SASSY MODE: Your commentary should be hilariously sassy, roasty, and meme-culture aware.
|
|
13
|
+
Think: unhinged twitter energy, gen-z humor, affectionate bullying.
|
|
14
|
+
Examples of the vibe:
|
|
15
|
+
- "Bestie you spent more on tokens than my monthly grocery bill. Touch grass."
|
|
16
|
+
- "Opus? Oh we got a trust fund baby over here."
|
|
17
|
+
- "You coded at 3am on a Tuesday. That's not dedication, that's a cry for help."
|
|
18
|
+
Make it genuinely funny. Internet humor. The kind of thing people screenshot and share.`;
|
|
19
|
+
const periodContext = stats.periodLabel.startsWith("Your ")
|
|
20
|
+
? `This recap covers ${stats.periodLabel.toLowerCase().replace("your ", "the user's last ")} of usage.`
|
|
21
|
+
: `This recap covers the period ${stats.periodLabel}.`;
|
|
22
|
+
return `You are writing short, punchy one-liner commentary for a "Claude Code Wrapped" recap video — a shareable stats recap of someone's AI coding assistant usage. Each line appears as a caption on an animated slide.
|
|
23
|
+
|
|
24
|
+
${periodContext}
|
|
25
|
+
|
|
26
|
+
${modeDescription}
|
|
27
|
+
|
|
28
|
+
Here are the user's stats:
|
|
29
|
+
- Total tokens: ${stats.totalTokens.toLocaleString()} (${stats.warAndPeaceEquivalent.toFixed(1)}x War and Peace)
|
|
30
|
+
- Total estimated cost: $${stats.totalCost.toFixed(2)} (${stats.coffeeEquivalent} coffees worth)
|
|
31
|
+
- Favorite model: ${stats.favoriteModel}
|
|
32
|
+
- Busiest day: ${stats.busiestDay.date} with ${stats.busiestDay.tokenCount.toLocaleString()} tokens and ${stats.busiestDay.messageCount} messages
|
|
33
|
+
- Longest session: ${Math.round(stats.longestSession.durationMinutes)} minutes (${(stats.longestSession.durationMinutes / 60).toFixed(1)} hours)
|
|
34
|
+
- Total sessions: ${stats.totalSessions}
|
|
35
|
+
- Total messages: ${stats.totalMessages.toLocaleString()}
|
|
36
|
+
- Active days: ${stats.totalDaysActive}
|
|
37
|
+
- Peak coding hour: ${stats.busiestHour}:00
|
|
38
|
+
- Night owl score: ${stats.nightOwlScore}% (% of tokens during 10pm-5am)
|
|
39
|
+
- Weekend warrior score: ${stats.weekendWarriorScore}%
|
|
40
|
+
- Longest streak: ${stats.streakDays} consecutive days
|
|
41
|
+
- Archetype: "${stats.archetype}" - ${stats.archetypeDescription}
|
|
42
|
+
|
|
43
|
+
Generate exactly 8 one-liner captions (max 120 chars each). They should be FUNNY and SHAREABLE.
|
|
44
|
+
Output as JSON with these exact keys: tokensLine, costLine, modelLine, busiestDayLine, sessionLine, peakHoursLine, archetypeLine, summaryLine
|
|
45
|
+
|
|
46
|
+
Return ONLY the JSON object, no markdown fences or other text.`;
|
|
47
|
+
}
|
|
48
|
+
// Hardcoded dark mode fallbacks
|
|
49
|
+
const darkFallbacks = {
|
|
50
|
+
tokensLine: "Each token consumed water, power, and a tiny piece of the planet. You're welcome, Earth.",
|
|
51
|
+
costLine: "You paid for the privilege of accelerating compute demand. The grid felt that.",
|
|
52
|
+
modelLine: "You picked the model. The data center picked up the energy bill.",
|
|
53
|
+
busiestDayLine: "Your biggest day kept servers spinning hot enough to heat a small apartment.",
|
|
54
|
+
sessionLine: "That session burned more electricity than your fridge uses in a week. Priorities.",
|
|
55
|
+
peakHoursLine: "Late-night prompts mean late-night cooling systems. The night shift thanks you.",
|
|
56
|
+
archetypeLine: "Every archetype is just a fun way to say 'heavy compute consumer.' But sure, it's personality.",
|
|
57
|
+
summaryLine: "You coded with AI. The ice caps continued to melt. Coincidence? Definitely. Probably.",
|
|
58
|
+
};
|
|
59
|
+
// Hardcoded sassy fallbacks
|
|
60
|
+
const sassyFallbacks = {
|
|
61
|
+
tokensLine: "That's a LOT of tokens bestie. Your AI dependency is showing.",
|
|
62
|
+
costLine: "You could've bought something nice. Instead you bought robot words. No regrets though.",
|
|
63
|
+
modelLine: "Your model choice says a lot about you. We won't say what, but it says A LOT.",
|
|
64
|
+
busiestDayLine: "Your biggest day was UNHINGED. The vibes were immaculate, the productivity was concerning.",
|
|
65
|
+
sessionLine: "That marathon session? Iconic. Unhealthy, but iconic.",
|
|
66
|
+
peakHoursLine: "Your peak hours reveal things about your sleep schedule that we're choosing not to address.",
|
|
67
|
+
archetypeLine: "This archetype was chosen for you by math. The math doesn't lie. You might not like the math.",
|
|
68
|
+
summaryLine: "Outsourcing your thinking at scale. Honestly? Respect. Keep going.",
|
|
69
|
+
};
|
|
70
|
+
export async function generateCommentary(stats, mode) {
|
|
71
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
console.log(" No ANTHROPIC_API_KEY found, using pre-written commentary.");
|
|
74
|
+
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const client = new Anthropic({ apiKey });
|
|
78
|
+
const message = await client.messages.create({
|
|
79
|
+
model: "claude-haiku-4-5-20251001",
|
|
80
|
+
max_tokens: 1024,
|
|
81
|
+
messages: [{ role: "user", content: buildPrompt(stats, mode) }],
|
|
82
|
+
});
|
|
83
|
+
const text = message.content[0]?.type === "text" ? message.content[0].text : "";
|
|
84
|
+
const parsed = JSON.parse(text);
|
|
85
|
+
// Validate all keys exist
|
|
86
|
+
const keys = [
|
|
87
|
+
"tokensLine", "costLine", "modelLine", "busiestDayLine",
|
|
88
|
+
"sessionLine", "peakHoursLine", "archetypeLine", "summaryLine",
|
|
89
|
+
];
|
|
90
|
+
const fallback = mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
if (!parsed[key] || typeof parsed[key] !== "string") {
|
|
93
|
+
parsed[key] = fallback[key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.log(` AI commentary failed (${err.message}), using pre-written commentary.`);
|
|
100
|
+
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
101
|
+
}
|
|
102
|
+
}
|