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
3 changes: 0 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
"@clack/prompts": "^0.11.0",
"@resvg/resvg-wasm": "^2.6.2",
"react": "^19.2.3",
"satori": "^0.18.3",
"xdg-basedir": "^5.1.0"
"satori": "^0.18.3"
},
"devDependencies": {
"@semantic-release/exec": "^7.1.0",
Expand Down
43 changes: 31 additions & 12 deletions src/collector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Data collector - reads Claude Code storage and returns raw data

import { createReadStream } from "node:fs";
import { createReadStream, existsSync } from "node:fs";
import { readFile, readdir, realpath, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import os from "node:os";
Expand Down Expand Up @@ -38,12 +38,31 @@ export interface ClaudeStatsCache {
}

const CLAUDE_DATA_PATH = join(os.homedir(), ".claude");
const CLAUDE_STATS_CACHE_PATH = join(CLAUDE_DATA_PATH, "stats-cache.json");
const CLAUDE_HISTORY_PATH = join(CLAUDE_DATA_PATH, "history.jsonl");
const CLAUDE_PROJECTS_DIR = "projects";
const CLAUDE_CONFIG_PATH = join(os.homedir(), ".config", "claude");
const CLAUDE_PROJECTS_DIR = "projects";
const CLAUDE_CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";

// Resolve Claude data path
// Priority: 1. CLAUDE_CONFIG_DIR env var, 2. ~/.config/claude (XDG), 3. ~/.claude (legacy)
function resolveClaudeDataPath(): string | null {
const envPath = process.env[CLAUDE_CONFIG_DIR_ENV]?.trim();
if (envPath && existsSync(join(envPath, "stats-cache.json"))) {
return envPath;
}

const candidates = [
CLAUDE_CONFIG_PATH, // XDG standard (~/.config/claude)
CLAUDE_DATA_PATH, // Legacy (~/.claude)
];

for (const path of candidates) {
if (existsSync(join(path, "stats-cache.json"))) {
return path;
}
}
return null;
}

export interface ClaudeUsageSummary {
totalInputTokens: number;
totalOutputTokens: number;
Expand All @@ -59,24 +78,24 @@ export interface ClaudeUsageSummary {
}

export async function checkClaudeDataExists(): Promise<boolean> {
try {
await readFile(CLAUDE_STATS_CACHE_PATH);
return true;
} catch {
return false;
}
return resolveClaudeDataPath() !== null;
}

export async function loadClaudeStatsCache(): Promise<ClaudeStatsCache> {
const raw = await readFile(CLAUDE_STATS_CACHE_PATH, "utf8");
const dataPath = resolveClaudeDataPath();
if (!dataPath) throw new Error("Claude data not found");
const raw = await readFile(join(dataPath, "stats-cache.json"), "utf8");
return JSON.parse(raw) as ClaudeStatsCache;
}

export async function collectClaudeProjects(year: number): Promise<Set<string>> {
const projects = new Set<string>();
const dataPath = resolveClaudeDataPath();
if (!dataPath) return projects;

try {
const raw = await readFile(CLAUDE_HISTORY_PATH, "utf8");
const historyPath = join(dataPath, "history.jsonl");
const raw = await readFile(historyPath, "utf8");
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
try {
Expand Down
20 changes: 14 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ USAGE:
cc-wrapped [OPTIONS]

OPTIONS:
--year <YYYY> Generate wrapped for a specific year (default: current year)
--help, -h Show this help message
--version, -v Show version number
-y, --year <YYYY> Generate wrapped for a specific year (default: current year)
-c, --config-dir <PATH> Path to Claude Code config directory (default: auto-detect)
-h, --help Show this help message
-v, --version Show version number

EXAMPLES:
cc-wrapped # Generate current year wrapped
cc-wrapped --year 2025 # Generate 2025 wrapped
cc-wrapped # Generate current year wrapped
cc-wrapped --year 2025 # Generate 2025 wrapped
cc-wrapped -c ~/.config/claude # Use specific config directory
`);
}

Expand All @@ -41,6 +43,7 @@ async function main() {
args: process.argv.slice(2),
options: {
year: { type: "string", short: "y" },
"config-dir": { type: "string", short: "c" },
help: { type: "boolean", short: "h" },
version: { type: "boolean", short: "v" },
},
Expand All @@ -58,6 +61,11 @@ async function main() {
process.exit(0);
}

// Set config dir from CLI arg (takes priority over auto-detection)
if (values["config-dir"]) {
process.env.CLAUDE_CONFIG_DIR = values["config-dir"];
}

p.intro("claude code wrapped");

const requestedYear = values.year ? parseInt(values.year, 10) : new Date().getFullYear();
Expand All @@ -75,7 +83,7 @@ async function main() {

const dataExists = await checkClaudeDataExists();
if (!dataExists) {
p.cancel("Claude Code data not found in ~/.claude\n\nMake sure you have used Claude Code at least once.");
p.cancel("Claude Code data not found in ~/.config/claude or ~/.claude\n\nMake sure you have used Claude Code at least once.");
process.exit(0);
}

Expand Down