diff --git a/bun.lock b/bun.lock index c98d42c..5dee7bc 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "@resvg/resvg-wasm": "^2.6.2", "react": "^19.2.3", "satori": "^0.18.3", - "xdg-basedir": "^5.1.0", }, "devDependencies": { "@semantic-release/exec": "^7.1.0", @@ -758,8 +757,6 @@ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], diff --git a/package.json b/package.json index e15526b..5409402 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/collector.ts b/src/collector.ts index 483143e..7ffa72b 100644 --- a/src/collector.ts +++ b/src/collector.ts @@ -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"; @@ -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; @@ -59,24 +78,24 @@ export interface ClaudeUsageSummary { } export async function checkClaudeDataExists(): Promise { - try { - await readFile(CLAUDE_STATS_CACHE_PATH); - return true; - } catch { - return false; - } + return resolveClaudeDataPath() !== null; } export async function loadClaudeStatsCache(): Promise { - 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> { const projects = new Set(); + 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 { diff --git a/src/index.ts b/src/index.ts index a9313be..c94d668 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,13 +25,15 @@ USAGE: cc-wrapped [OPTIONS] OPTIONS: - --year Generate wrapped for a specific year (default: current year) - --help, -h Show this help message - --version, -v Show version number + -y, --year Generate wrapped for a specific year (default: current year) + -c, --config-dir 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 `); } @@ -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" }, }, @@ -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(); @@ -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); }