ccwrap 0.1.0 → 0.2.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/dist/cli.js
CHANGED
|
@@ -88,13 +88,14 @@ const darkBanner = `
|
|
|
88
88
|
`;
|
|
89
89
|
program
|
|
90
90
|
.name("ccwrap")
|
|
91
|
-
.description("
|
|
92
|
-
.version("0.
|
|
93
|
-
.option("-o, --output <path>", "Output video file path", "
|
|
91
|
+
.description("ccwrap - Your AI coding stats, meme-ified into a shareable video")
|
|
92
|
+
.version("0.2.0")
|
|
93
|
+
.option("-o, --output <path>", "Output video file path", "ccwrap.mp4")
|
|
94
94
|
.option("-m, --mode <mode>", "Commentary mode: sassy or dark", "sassy")
|
|
95
95
|
.option("-p, --period <period>", "Time period: week, month, quarter, all (default: month)")
|
|
96
96
|
.option("--since <date>", "Start date (YYYY-MM-DD), mutually exclusive with --period")
|
|
97
97
|
.option("--until <date>", "End date (YYYY-MM-DD), requires --since")
|
|
98
|
+
.option("--skip-weekends", "Exclude weekend activity from stats")
|
|
98
99
|
.option("--json", "Output stats as JSON instead of video")
|
|
99
100
|
.option("--stats-only", "Print stats to console without rendering video")
|
|
100
101
|
.action(async (options) => {
|
|
@@ -113,7 +114,7 @@ program
|
|
|
113
114
|
const spinner = ora({ text: `Scanning Claude Code usage data...${periodHint}`, color: isDark ? "red" : "magenta" }).start();
|
|
114
115
|
let stats;
|
|
115
116
|
try {
|
|
116
|
-
stats = await loadAndComputeStats(range, periodLabel);
|
|
117
|
+
stats = await loadAndComputeStats(range, periodLabel, options.skipWeekends);
|
|
117
118
|
spinner.succeed(chalk.green("Usage data loaded!"));
|
|
118
119
|
}
|
|
119
120
|
catch (err) {
|
|
@@ -10,4 +10,5 @@ export interface Commentary {
|
|
|
10
10
|
summaryLine: string;
|
|
11
11
|
}
|
|
12
12
|
export type Mode = "sassy" | "dark";
|
|
13
|
+
export declare function buildPrompt(stats: WrappedStats, mode: Mode): string;
|
|
13
14
|
export declare function generateCommentary(stats: WrappedStats, mode: Mode): Promise<Commentary>;
|
package/dist/data/commentary.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
function buildPrompt(stats, mode) {
|
|
2
|
+
export function buildPrompt(stats, mode) {
|
|
3
3
|
const modeDescription = mode === "dark"
|
|
4
4
|
? `DARK MODE: Your commentary should be darkly humorous with environmental/existential undertones.
|
|
5
5
|
Think: water usage, carbon footprint, e-waste, heat death of the universe, humanity's dependence on AI.
|
|
@@ -19,7 +19,7 @@ function buildPrompt(stats, mode) {
|
|
|
19
19
|
const periodContext = stats.periodLabel.startsWith("Your ")
|
|
20
20
|
? `This recap covers ${stats.periodLabel.toLowerCase().replace("your ", "the user's last ")} of usage.`
|
|
21
21
|
: `This recap covers the period ${stats.periodLabel}.`;
|
|
22
|
-
return `You are writing short, punchy one-liner commentary for a "
|
|
22
|
+
return `You are writing short, punchy one-liner commentary for a "ccwrap" recap video — a shareable stats recap of someone's AI coding assistant usage. Each line appears as a caption on an animated slide.
|
|
23
23
|
|
|
24
24
|
${periodContext}
|
|
25
25
|
|
|
@@ -73,6 +73,7 @@ export async function generateCommentary(stats, mode) {
|
|
|
73
73
|
console.log(" No ANTHROPIC_API_KEY found, using pre-written commentary.");
|
|
74
74
|
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
75
75
|
}
|
|
76
|
+
/* v8 ignore start -- requires live API key */
|
|
76
77
|
try {
|
|
77
78
|
const client = new Anthropic({ apiKey });
|
|
78
79
|
const message = await client.messages.create({
|
|
@@ -99,4 +100,5 @@ export async function generateCommentary(stats, mode) {
|
|
|
99
100
|
console.log(` AI commentary failed (${err.message}), using pre-written commentary.`);
|
|
100
101
|
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
101
102
|
}
|
|
103
|
+
/* v8 ignore stop */
|
|
102
104
|
}
|
package/dist/data/parser.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
import type { WrappedStats, DateRange } from "./types.js";
|
|
2
|
-
export declare function
|
|
1
|
+
import type { RawJSONLEntry, WrappedStats, DateRange } from "./types.js";
|
|
2
|
+
export declare function estimateCost(model: string, inputTokens: number, outputTokens: number, cacheCreationTokens: number, cacheReadTokens: number): number;
|
|
3
|
+
export declare function extractProject(filePath: string): string;
|
|
4
|
+
export declare function loadAndComputeStats(range?: DateRange, periodLabel?: string, skipWeekends?: boolean): Promise<WrappedStats>;
|
|
5
|
+
export declare function computeStatsFromEntries(allEntries: (RawJSONLEntry & {
|
|
6
|
+
_file: string;
|
|
7
|
+
})[], opts?: {
|
|
8
|
+
range?: DateRange;
|
|
9
|
+
periodLabel?: string;
|
|
10
|
+
skipWeekends?: boolean;
|
|
11
|
+
}): WrappedStats;
|
|
12
|
+
export declare function computeArchetype(params: {
|
|
13
|
+
nightOwlScore: number;
|
|
14
|
+
weekendWarriorScore: number;
|
|
15
|
+
avgSessionMinutes: number;
|
|
16
|
+
totalSessions: number;
|
|
17
|
+
streakDays: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
favoriteModel: string;
|
|
20
|
+
}): {
|
|
21
|
+
archetype: string;
|
|
22
|
+
archetypeDescription: string;
|
|
23
|
+
};
|
package/dist/data/parser.js
CHANGED
|
@@ -13,7 +13,7 @@ const PRICING = {
|
|
|
13
13
|
"claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
14
14
|
"claude-3-5-haiku-20241022": { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
|
|
15
15
|
};
|
|
16
|
-
function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
16
|
+
export function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
17
17
|
const pricing = Object.entries(PRICING).find(([key]) => model.includes(key))?.[1]
|
|
18
18
|
?? (model.includes("opus") ? PRICING["claude-opus-4-6"]
|
|
19
19
|
: model.includes("haiku") ? PRICING["claude-haiku-4-5-20251001"]
|
|
@@ -59,7 +59,7 @@ async function parseJSONLFile(filePath) {
|
|
|
59
59
|
}
|
|
60
60
|
return entries;
|
|
61
61
|
}
|
|
62
|
-
function extractProject(filePath) {
|
|
62
|
+
export function extractProject(filePath) {
|
|
63
63
|
// Path format: ~/.claude/projects/{encoded-project-path}/{sessionId}.jsonl
|
|
64
64
|
const parts = filePath.split("/projects/");
|
|
65
65
|
if (parts.length < 2)
|
|
@@ -72,7 +72,7 @@ function extractProject(filePath) {
|
|
|
72
72
|
.replace(/^-/, "/")
|
|
73
73
|
.replace(/-/g, "/");
|
|
74
74
|
}
|
|
75
|
-
export async function loadAndComputeStats(range, periodLabel) {
|
|
75
|
+
export async function loadAndComputeStats(range, periodLabel, skipWeekends) {
|
|
76
76
|
const files = await findJSONLFiles();
|
|
77
77
|
if (files.length === 0) {
|
|
78
78
|
throw new Error("No Claude Code usage data found. Make sure you have used Claude Code before.");
|
|
@@ -88,6 +88,10 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
88
88
|
if (allEntries.length === 0) {
|
|
89
89
|
throw new Error("No usage data entries found in Claude Code logs.");
|
|
90
90
|
}
|
|
91
|
+
return computeStatsFromEntries(allEntries, { range, periodLabel, skipWeekends });
|
|
92
|
+
}
|
|
93
|
+
export function computeStatsFromEntries(allEntries, opts = {}) {
|
|
94
|
+
const { range, periodLabel, skipWeekends } = opts;
|
|
91
95
|
// Sort by timestamp
|
|
92
96
|
allEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
93
97
|
// Filter by date range if provided
|
|
@@ -105,6 +109,19 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
105
109
|
throw new Error(`No usage data found in the selected time period (${before} entries exist outside this range).`);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
112
|
+
// Filter out weekends if requested
|
|
113
|
+
if (skipWeekends) {
|
|
114
|
+
const before = allEntries.length;
|
|
115
|
+
const filtered = allEntries.filter(e => {
|
|
116
|
+
const day = new Date(e.timestamp).getDay();
|
|
117
|
+
return day !== 0 && day !== 6;
|
|
118
|
+
});
|
|
119
|
+
allEntries.length = 0;
|
|
120
|
+
allEntries.push(...filtered);
|
|
121
|
+
if (allEntries.length === 0) {
|
|
122
|
+
throw new Error(`No weekday usage data found (${before} weekend entries were excluded).`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
108
125
|
// Build session map
|
|
109
126
|
const sessionMap = new Map();
|
|
110
127
|
for (const entry of allEntries) {
|
|
@@ -307,7 +324,7 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
307
324
|
kwhUsed: Math.round(totalTokens / 1_000 * 0.001 * 100) / 100,
|
|
308
325
|
};
|
|
309
326
|
}
|
|
310
|
-
function computeArchetype(params) {
|
|
327
|
+
export function computeArchetype(params) {
|
|
311
328
|
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel } = params;
|
|
312
329
|
if (nightOwlScore > 50) {
|
|
313
330
|
return {
|
|
@@ -13,7 +13,7 @@ export const IntroSlide = ({ mode, periodLabel }) => {
|
|
|
13
13
|
const taglineScale = spring({ frame: Math.max(0, frame - 35), fps, from: 0.5, to: 1, config: { damping: 8 } });
|
|
14
14
|
const taglineOpacity = interpolate(frame, [35, 50], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
|
15
15
|
const rotation = interpolate(frame, [0, 150], [0, 360], { extrapolateRight: "extend" });
|
|
16
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: { position: "absolute", width: 180, height: 180, borderRadius: "50%", border: `2px solid ${c.primary}30`, transform: `scale(${logoScale}) rotate(${rotation}deg)` } }), _jsx("div", { style: { position: "absolute", width: 150, height: 150, borderRadius: "50%", border: `1px solid ${c.secondary}20`, transform: `scale(${logoScale}) rotate(-${rotation * 0.7}deg)` } }), _jsx("div", { style: { transform: `scale(${logoScale})`, fontSize: 72, fontWeight: 900, background: `linear-gradient(135deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", zIndex: 1 }, children: "</>" }),
|
|
16
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: { position: "absolute", width: 180, height: 180, borderRadius: "50%", border: `2px solid ${c.primary}30`, transform: `scale(${logoScale}) rotate(${rotation}deg)` } }), _jsx("div", { style: { position: "absolute", width: 150, height: 150, borderRadius: "50%", border: `1px solid ${c.secondary}20`, transform: `scale(${logoScale}) rotate(-${rotation * 0.7}deg)` } }), _jsx("div", { style: { transform: `scale(${logoScale})`, fontSize: 72, fontWeight: 900, background: `linear-gradient(135deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", zIndex: 1 }, children: "</>" }), _jsx("div", { style: { opacity: titleOpacity, fontSize: 48, fontWeight: 800, marginTop: 24, letterSpacing: "-0.02em", zIndex: 1 }, children: _jsx("span", { style: { background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }, children: "ccwrap" }) }), _jsx(FadeIn, { delay: 35, children: _jsx("div", { style: { opacity: taglineOpacity, transform: `scale(${taglineScale})`, fontSize: 22, fontWeight: 500, color: c.textMuted, marginTop: 20, zIndex: 1 }, children: periodLabel.startsWith("Your ")
|
|
17
17
|
? mode === "dark"
|
|
18
18
|
? `${periodLabel} in AI. The planet remembers.`
|
|
19
19
|
: `${periodLabel} in AI-assisted coding`
|
|
@@ -47,6 +47,6 @@ export const SummarySlide = ({ stats, commentary, mode }) => {
|
|
|
47
47
|
border: `1px solid ${c.primary}50`, borderRadius: 24, padding: "36px 44px",
|
|
48
48
|
maxWidth: 620, width: "92%",
|
|
49
49
|
boxShadow: `0 0 36px ${c.primary}20, 0 12px 36px rgba(0,0,0,0.5)`, zIndex: 1,
|
|
50
|
-
}, children: [_jsx("div", { style: { textAlign: "center", marginBottom: 20 }, children: _jsx("div", { style: { fontSize: 28, fontWeight: 900, ...glowText(c.primary), background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }, children: getSummaryTitle(stats.periodLabel, mode) }) }), _jsx(FadeIn, { delay: 15, children: _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 36px" }, children: [_jsx(Row, { label: "Tokens", value: formatNumber(stats.totalTokens), color: c.gold, textColor: c.textMuted }), _jsx(Row, { label: "Cost", value: `$${stats.totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, color: mode === "dark" ? c.accent : "#10b981", textColor: c.textMuted }), _jsx(Row, { label: "Sessions", value: stats.totalSessions.toLocaleString(), color: c.primary, textColor: c.textMuted }), _jsx(Row, { label: "Messages", value: formatNumber(stats.totalMessages), color: c.pink, textColor: c.textMuted }), _jsx(Row, { label: "Active Days", value: stats.totalDaysActive.toLocaleString(), color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "Fav Model", value: getModelShortName(stats.favoriteModel), color: c.accent, textColor: c.textMuted }), mode === "dark" && _jsxs(_Fragment, { children: [_jsx(Row, { label: "Water", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "CO\u2082", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, color: c.textMuted, textColor: c.textMuted })] })] }) }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { marginTop: 20, textAlign: "center", padding: "12px 20px", background: `linear-gradient(135deg, ${c.primary}20, ${c.accent}20)`, borderRadius: 12, border: `1px solid ${c.primary}40` }, children: [_jsx("div", { style: { fontSize: 12, color: c.textMuted, textTransform: "uppercase", letterSpacing: "0.15em" }, children: "Your Archetype" }), _jsx("div", { style: { fontSize: 26, fontWeight: 800, background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", marginTop: 3 }, children: stats.archetype })] }) }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { textAlign: "center", marginTop: 16, fontSize: 15, fontStyle: "italic", color: c.textMuted }, children: commentary }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { textAlign: "center", marginTop: 14, fontSize: 12, color: c.textDim }, children: ["
|
|
50
|
+
}, children: [_jsx("div", { style: { textAlign: "center", marginBottom: 20 }, children: _jsx("div", { style: { fontSize: 28, fontWeight: 900, ...glowText(c.primary), background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }, children: getSummaryTitle(stats.periodLabel, mode) }) }), _jsx(FadeIn, { delay: 15, children: _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 36px" }, children: [_jsx(Row, { label: "Tokens", value: formatNumber(stats.totalTokens), color: c.gold, textColor: c.textMuted }), _jsx(Row, { label: "Cost", value: `$${stats.totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, color: mode === "dark" ? c.accent : "#10b981", textColor: c.textMuted }), _jsx(Row, { label: "Sessions", value: stats.totalSessions.toLocaleString(), color: c.primary, textColor: c.textMuted }), _jsx(Row, { label: "Messages", value: formatNumber(stats.totalMessages), color: c.pink, textColor: c.textMuted }), _jsx(Row, { label: "Active Days", value: stats.totalDaysActive.toLocaleString(), color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "Fav Model", value: getModelShortName(stats.favoriteModel), color: c.accent, textColor: c.textMuted }), mode === "dark" && _jsxs(_Fragment, { children: [_jsx(Row, { label: "Water", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "CO\u2082", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, color: c.textMuted, textColor: c.textMuted })] })] }) }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { marginTop: 20, textAlign: "center", padding: "12px 20px", background: `linear-gradient(135deg, ${c.primary}20, ${c.accent}20)`, borderRadius: 12, border: `1px solid ${c.primary}40` }, children: [_jsx("div", { style: { fontSize: 12, color: c.textMuted, textTransform: "uppercase", letterSpacing: "0.15em" }, children: "Your Archetype" }), _jsx("div", { style: { fontSize: 26, fontWeight: 800, background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", marginTop: 3 }, children: stats.archetype })] }) }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { textAlign: "center", marginTop: 16, fontSize: 15, fontStyle: "italic", color: c.textMuted }, children: commentary }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { textAlign: "center", marginTop: 14, fontSize: 12, color: c.textDim }, children: ["ccwrap", mode === "dark" ? " — know your impact" : " — put meme software on the map"] }) })] })] }));
|
|
51
51
|
};
|
|
52
52
|
const Row = ({ label, value, color, textColor }) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("div", { style: { fontSize: 14, color: textColor }, children: label }), _jsx("div", { style: { fontSize: 20, fontWeight: 700, color }, children: value })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccwrap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"start": "tsx src/cli.ts",
|
|
24
24
|
"preview": "remotion preview src/video/index.ts",
|
|
25
25
|
"render": "tsx src/cli.ts",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
26
28
|
"prepublishOnly": "npm run build"
|
|
27
29
|
},
|
|
28
30
|
"bin": {
|
|
@@ -52,7 +54,9 @@
|
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@types/node": "^22.0.0",
|
|
54
56
|
"@types/react": "^18.3.0",
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
55
58
|
"tsx": "^4.0.0",
|
|
56
|
-
"typescript": "^5.6.0"
|
|
59
|
+
"typescript": "^5.6.0",
|
|
60
|
+
"vitest": "^4.1.0"
|
|
57
61
|
}
|
|
58
62
|
}
|