Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,39 @@ cc-wrapped

## Usage Options

| Option | Description |
| --------------- | ------------------------------------ |
| `--year, -y` | Generate wrapped for a specific year |
| `--help, -h` | Show help message |
| `--version, -v` | Show version number |
| Option | Description |
| --------------- | ------------------------------------------------ |
| `--year, -y` | Generate wrapped for a specific year |
| `--month, -m` | Generate wrapped for a specific month (1-12) |
| `--all, -a` | Generate all 12 monthly wraps + yearly summary |
| `--help, -h` | Show help message |
| `--version, -v` | Show version number |

### Examples

```bash
# Generate yearly wrapped (default)
cc-wrapped

# Generate wrapped for a specific year
cc-wrapped --year 2025

# Generate wrapped for a specific month
cc-wrapped --month 12 # December of current year
cc-wrapped -m 6 --year 2025 # June 2025

# Generate ALL monthly + yearly wrapped images (13 total)
cc-wrapped --all # All months + yearly for current year
cc-wrapped --all --year 2025 # All months + yearly for 2025
```

## Features

- Sessions, messages, tokens, projects, and streaks
- GitHub-style activity heatmap
- **Monthly activity bar chart** showing usage across all 12 months
- **Generate monthly or yearly summaries** — view stats for any specific month
- **Batch generation** — create all 12 monthly images + yearly in one command
- Top models and providers breakdown
- Usage cost (when available)
- Shareable PNG image
Expand All @@ -82,9 +105,17 @@ The wrapped image displays natively in terminals that support inline images:
The tool generates:

1. **Terminal Summary** — Quick stats overview in your terminal
2. **PNG Image** — A beautiful, shareable wrapped card saved to your home directory
2. **PNG Image(s)** — Beautiful, shareable wrapped cards saved to your home directory
3. **Clipboard** — Automatically copies the image to your clipboard

### Output Files

| Mode | Filename(s) |
| ---- | ----------- |
| Yearly (default) | `cc-wrapped-2025.png` |
| Single month | `cc-wrapped-2025-12.png` (December) |
| All (`--all`) | `cc-wrapped-2025-01.png` through `cc-wrapped-2025-12.png` + `cc-wrapped-2025.png` |

## Data Source

Claude Code Wrapped reads data from your local Claude Code installation:
Expand Down
105 changes: 100 additions & 5 deletions src/image/template.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClaudeCodeStats, WeekdayActivity } from "../types";
import type { ClaudeCodeStats, MonthlyActivity, WeekdayActivity } from "../types";
import { formatNumberFull, formatCostFull, formatDate } from "../utils/format";
import { ActivityHeatmap } from "./heatmap";
import { colors, typography, spacing, layout, components } from "./design-tokens";
Expand Down Expand Up @@ -49,7 +49,7 @@ export function WrappedTemplate({ stats }: { stats: ClaudeCodeStats }) {
borderRadius: layout.radius.full,
}}
/>
<Header year={stats.year} />
<Header year={stats.year} monthName={stats.monthName} />

<div style={{ marginTop: spacing[8], display: "flex", flexDirection: "row", gap: spacing[16], alignItems: "flex-start" }}>
<HeroStatItem
Expand Down Expand Up @@ -92,6 +92,31 @@ export function WrappedTemplate({ stats }: { stats: ClaudeCodeStats }) {
<ActivityHeatmap dailyActivity={stats.dailyActivity} year={stats.year} maxStreakDays={stats.maxStreakDays} />
</Section>

<div
style={{
marginTop: spacing[8],
display: "flex",
flexDirection: "column",
backgroundColor: colors.surface,
borderRadius: layout.radius.lg,
padding: spacing[8],
border: `1px solid ${colors.surfaceBorder}`,
}}
>
<span
style={{
fontSize: components.sectionHeader.fontSize,
fontWeight: components.sectionHeader.fontWeight,
color: components.sectionHeader.color,
letterSpacing: components.sectionHeader.letterSpacing,
textTransform: components.sectionHeader.textTransform,
}}
>
Monthly
</span>
<MonthlyBarChart monthlyActivity={stats.monthlyActivity} />
</div>

<div
style={{
marginTop: spacing[8],
Expand All @@ -115,7 +140,9 @@ export function WrappedTemplate({ stats }: { stats: ClaudeCodeStats }) {
);
}

function Header({ year }: { year: number }) {
function Header({ year, monthName }: { year: number; monthName?: string }) {
const displayLabel = monthName ? `${monthName} ${year}` : String(year);

return (
<div
style={{
Expand Down Expand Up @@ -174,14 +201,14 @@ function Header({ year }: { year: number }) {
</span>
<span
style={{
fontSize: typography.size["3xl"],
fontSize: monthName ? typography.size["2xl"] : typography.size["3xl"],
fontWeight: typography.weight.bold,
letterSpacing: typography.letterSpacing.normal,
color: colors.accent.primary,
lineHeight: typography.lineHeight.none,
}}
>
{year}
{displayLabel}
</span>
</div>
</div>
Expand Down Expand Up @@ -309,6 +336,74 @@ function WeeklyBarChart({ weekdayActivity }: { weekdayActivity: WeekdayActivity
);
}

const MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MONTHLY_BAR_HEIGHT = 80;
const MONTHLY_BAR_WIDTH = 28;
const MONTHLY_BAR_GAP = 6;

function MonthlyBarChart({ monthlyActivity }: { monthlyActivity: MonthlyActivity }) {
const { counts, mostActiveMonth, maxCount } = monthlyActivity;

return (
<div style={{ display: "flex", flexDirection: "column", gap: spacing[2] }}>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
gap: MONTHLY_BAR_GAP,
height: MONTHLY_BAR_HEIGHT,
}}
>
{counts.map((count, i) => {
const heightPercent = maxCount > 0 ? count / maxCount : 0;
const barHeight = Math.max(6, Math.round(heightPercent * MONTHLY_BAR_HEIGHT));
const isHighlighted = i === mostActiveMonth;

return (
<div
key={i}
style={{
width: MONTHLY_BAR_WIDTH,
height: barHeight,
backgroundColor: isHighlighted ? colors.accent.primary : colors.streak.level4,
borderRadius: 4,
}}
/>
);
})}
</div>

<div
style={{
display: "flex",
flexDirection: "row",
gap: MONTHLY_BAR_GAP,
}}
>
{MONTH_LABELS.map((label, i) => {
const isHighlighted = i === mostActiveMonth;
return (
<div
key={i}
style={{
width: MONTHLY_BAR_WIDTH,
display: "flex",
justifyContent: "center",
fontSize: typography.size.xs,
fontWeight: isHighlighted ? typography.weight.bold : typography.weight.regular,
color: isHighlighted ? colors.accent.primary : colors.text.muted,
}}
>
{label}
</div>
);
})}
</div>
</div>
);
}

function Section({ title, marginTop = 0, children }: { title: string; marginTop?: number; children: React.ReactNode }) {
return (
<div
Expand Down
Loading