From 3ceebfa005071d16eee15bdb67199f33b3ff86ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Jun 2025 21:42:23 +0000 Subject: [PATCH 01/16] Implement CLAUDE_CONFIG_DIR environment variable support Co-authored-by: timrogers <116134+timrogers@users.noreply.github.com> --- src/data-loader.test.ts | 59 +++++++++++++++++++++++++++++++++++++++++ src/data-loader.ts | 3 ++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index a7277d72..4c885fee 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -3,6 +3,7 @@ import { createFixture } from 'fs-fixture'; import { calculateCostForEntry, formatDate, + getDefaultClaudePath, loadDailyUsageData, loadMonthlyUsageData, loadSessionData, @@ -29,6 +30,64 @@ describe('formatDate', () => { }); }); +describe('getDefaultClaudePath', () => { + test('returns CLAUDE_CONFIG_DIR when environment variable is set', () => { + const originalEnv = process.env.CLAUDE_CONFIG_DIR; + const testPath = '/custom/claude/path'; + + try { + process.env.CLAUDE_CONFIG_DIR = testPath; + expect(getDefaultClaudePath()).toBe(testPath); + } finally { + // Restore original value + if (originalEnv !== undefined) { + process.env.CLAUDE_CONFIG_DIR = originalEnv; + } else { + delete process.env.CLAUDE_CONFIG_DIR; + } + } + }); + + test('returns default path when CLAUDE_CONFIG_DIR is not set', () => { + const originalEnv = process.env.CLAUDE_CONFIG_DIR; + + try { + delete process.env.CLAUDE_CONFIG_DIR; + const result = getDefaultClaudePath(); + expect(result).toContain('.claude'); + expect(result).not.toBe('.claude'); // Should be full path with home directory + } finally { + // Restore original value + if (originalEnv !== undefined) { + process.env.CLAUDE_CONFIG_DIR = originalEnv; + } + } + }); + + test('returns default path when CLAUDE_CONFIG_DIR is empty or whitespace', () => { + const originalEnv = process.env.CLAUDE_CONFIG_DIR; + + try { + // Test empty string + process.env.CLAUDE_CONFIG_DIR = ''; + let result = getDefaultClaudePath(); + expect(result).toContain('.claude'); + + // Test whitespace only + process.env.CLAUDE_CONFIG_DIR = ' '; + result = getDefaultClaudePath(); + expect(result).toContain('.claude'); + } finally { + // Restore original value + if (originalEnv !== undefined) { + process.env.CLAUDE_CONFIG_DIR = originalEnv; + } else { + delete process.env.CLAUDE_CONFIG_DIR; + } + } + }); +}); + describe('loadDailyUsageData', () => { test('returns empty array when no files found', async () => { await using fixture = await createFixture({ diff --git a/src/data-loader.ts b/src/data-loader.ts index d1a82e63..ca167ffe 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -13,7 +13,8 @@ import { import { groupBy } from './utils.internal.ts'; export function getDefaultClaudePath(): string { - return path.join(homedir(), '.claude'); + const envPath = process.env.CLAUDE_CONFIG_DIR; + return envPath && envPath.trim() ? envPath : path.join(homedir(), '.claude'); } export const UsageDataSchema = v.object({ From f2d2127dc5b8bd77ac1aadfb46afa81624377c15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Jun 2025 21:45:18 +0000 Subject: [PATCH 02/16] Update documentation for CLAUDE_CONFIG_DIR environment variable Co-authored-by: timrogers <116134+timrogers@users.noreply.github.com> --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf3dfa7f..d17e70b8 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ ccusage daily --since 20250525 --until 20250530 # Use custom Claude data directory ccusage daily --path /custom/path/to/.claude +# Or set CLAUDE_CONFIG_DIR environment variable +export CLAUDE_CONFIG_DIR="/custom/path/to/.claude" +ccusage daily + # Output in JSON format ccusage daily --json @@ -164,6 +168,10 @@ ccusage monthly --since 20250101 --until 20250531 # Use custom Claude data directory ccusage monthly --path /custom/path/to/.claude +# Or set CLAUDE_CONFIG_DIR environment variable +export CLAUDE_CONFIG_DIR="/custom/path/to/.claude" +ccusage monthly + # Output in JSON format ccusage monthly --json @@ -194,6 +202,10 @@ ccusage session --since 20250525 # Combine filters ccusage session --since 20250525 --until 20250530 --path /custom/path +# Or use environment variable +export CLAUDE_CONFIG_DIR="/custom/path" +ccusage session --since 20250525 --until 20250530 + # Output in JSON format ccusage session --json @@ -216,7 +228,7 @@ All commands support the following options: - `-s, --since `: Filter from date (YYYYMMDD format) - `-u, --until `: Filter until date (YYYYMMDD format) -- `-p, --path `: Custom path to Claude data directory (default: `~/.claude`) +- `-p, --path `: Custom path to Claude data directory (default: `$CLAUDE_CONFIG_DIR` or `~/.claude`) - `-j, --json`: Output results in JSON format instead of table - `-m, --mode `: Cost calculation mode: `auto` (default), `calculate`, or `display` - `-o, --order `: Sort order: `desc` (newest first, default) or `asc` (oldest first). @@ -232,6 +244,21 @@ All commands support the following options: - **`calculate`**: Always calculates costs from token counts using model pricing, ignores any pre-calculated `costUSD` values - **`display`**: Always uses pre-calculated `costUSD` values only, shows $0.00 for entries without pre-calculated costs +#### Environment Variable Support + +The tool supports the `CLAUDE_CONFIG_DIR` environment variable to specify the Claude data directory: + +```bash +# Set the environment variable to use a custom Claude directory +export CLAUDE_CONFIG_DIR="/path/to/custom/claude/directory" +ccusage daily + +# The --path option takes precedence over the environment variable +ccusage daily --path "/different/path" +``` + +When both the environment variable and `--path` option are provided, the `--path` option takes precedence, ensuring backward compatibility. + ### MCP (Model Context Protocol) Support Exposes usage data through Model Context Protocol for integration with other tools: From 03c81b9ca020931a117e66d82d8987fa519fd301 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Sat, 14 Jun 2025 17:57:09 +0100 Subject: [PATCH 03/16] Fix lint --- src/data-loader.test.ts | 23 ++++++++++++++--------- src/data-loader.ts | 3 ++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index 4c885fee..aa68ebf2 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -34,15 +34,17 @@ describe('getDefaultClaudePath', () => { test('returns CLAUDE_CONFIG_DIR when environment variable is set', () => { const originalEnv = process.env.CLAUDE_CONFIG_DIR; const testPath = '/custom/claude/path'; - + try { process.env.CLAUDE_CONFIG_DIR = testPath; expect(getDefaultClaudePath()).toBe(testPath); - } finally { + } + finally { // Restore original value if (originalEnv !== undefined) { process.env.CLAUDE_CONFIG_DIR = originalEnv; - } else { + } + else { delete process.env.CLAUDE_CONFIG_DIR; } } @@ -50,13 +52,14 @@ describe('getDefaultClaudePath', () => { test('returns default path when CLAUDE_CONFIG_DIR is not set', () => { const originalEnv = process.env.CLAUDE_CONFIG_DIR; - + try { delete process.env.CLAUDE_CONFIG_DIR; const result = getDefaultClaudePath(); expect(result).toContain('.claude'); expect(result).not.toBe('.claude'); // Should be full path with home directory - } finally { + } + finally { // Restore original value if (originalEnv !== undefined) { process.env.CLAUDE_CONFIG_DIR = originalEnv; @@ -66,22 +69,24 @@ describe('getDefaultClaudePath', () => { test('returns default path when CLAUDE_CONFIG_DIR is empty or whitespace', () => { const originalEnv = process.env.CLAUDE_CONFIG_DIR; - + try { // Test empty string process.env.CLAUDE_CONFIG_DIR = ''; let result = getDefaultClaudePath(); expect(result).toContain('.claude'); - + // Test whitespace only process.env.CLAUDE_CONFIG_DIR = ' '; result = getDefaultClaudePath(); expect(result).toContain('.claude'); - } finally { + } + finally { // Restore original value if (originalEnv !== undefined) { process.env.CLAUDE_CONFIG_DIR = originalEnv; - } else { + } + else { delete process.env.CLAUDE_CONFIG_DIR; } } diff --git a/src/data-loader.ts b/src/data-loader.ts index ca167ffe..cbd8d7f2 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -2,6 +2,7 @@ import type { CostMode, SortOrder } from './types.internal.ts'; import { readFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import path from 'node:path'; +import process from 'node:process'; import { unreachable } from '@core/errorutil'; import { sort } from 'fast-sort'; import { glob } from 'tinyglobby'; @@ -14,7 +15,7 @@ import { groupBy } from './utils.internal.ts'; export function getDefaultClaudePath(): string { const envPath = process.env.CLAUDE_CONFIG_DIR; - return envPath && envPath.trim() ? envPath : path.join(homedir(), '.claude'); + return (envPath != null && envPath.trim() !== '') ? envPath : path.join(homedir(), '.claude'); } export const UsageDataSchema = v.object({ From 4c0cfa799322a15e935d1596a1226fcd352968cf Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:11:32 +0100 Subject: [PATCH 04/16] chore(deps): path-type --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index 833b7e1c..4b3a8202 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "fs-fixture": "^2.7.1", "gunshi": "^0.26.3", "lint-staged": "^16.1.0", + "path-type": "^6.0.0", "picocolors": "^1.1.1", "publint": "^0.3.12", "simple-git-hooks": "^2.13.0", @@ -843,6 +844,8 @@ "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], diff --git a/package.json b/package.json index cbc70c03..e86b731d 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "fs-fixture": "^2.7.1", "gunshi": "^0.26.3", "lint-staged": "^16.1.0", + "path-type": "^6.0.0", "picocolors": "^1.1.1", "publint": "^0.3.12", "simple-git-hooks": "^2.13.0", From a9a4a5a5f16cce670f30c8ee9c59da9ab69313ec Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:14:48 +0100 Subject: [PATCH 05/16] fix(data-loader): add default Claude code path --- src/data-loader.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data-loader.ts b/src/data-loader.ts index cbd8d7f2..de830c16 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -13,6 +13,8 @@ import { } from './pricing-fetcher.ts'; import { groupBy } from './utils.internal.ts'; +const DEFAULT_CLAUDE_CODE_PATH = path.join(homedir(), '.claude'); + export function getDefaultClaudePath(): string { const envPath = process.env.CLAUDE_CONFIG_DIR; return (envPath != null && envPath.trim() !== '') ? envPath : path.join(homedir(), '.claude'); From 4a9715a14d22a7b8531b05ec1eaae194d0226924 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:15:05 +0100 Subject: [PATCH 06/16] fix(data-loader): read environment variable CLAUDE_CONFIG_DIR and use isDirectorySync to determine default path --- src/data-loader.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/data-loader.ts b/src/data-loader.ts index de830c16..edb4b4fc 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import process from 'node:process'; import { unreachable } from '@core/errorutil'; import { sort } from 'fast-sort'; +import { isDirectorySync } from 'path-type'; import { glob } from 'tinyglobby'; import * as v from 'valibot'; import { logger } from './logger.ts'; @@ -15,9 +16,16 @@ import { groupBy } from './utils.internal.ts'; const DEFAULT_CLAUDE_CODE_PATH = path.join(homedir(), '.claude'); +/** + * Default path for Claude data directory + * Uses environment variable CLAUDE_CONFIG_DIR if set, otherwise defaults to ~/.claude + */ export function getDefaultClaudePath(): string { - const envPath = process.env.CLAUDE_CONFIG_DIR; - return (envPath != null && envPath.trim() !== '') ? envPath : path.join(homedir(), '.claude'); + const envClaudeCodePath = process.env.CLAUDE_CONFIG_DIR?.trim() ?? ''; + if (isDirectorySync(envClaudeCodePath)) { + return envClaudeCodePath; + } + return DEFAULT_CLAUDE_CODE_PATH; } export const UsageDataSchema = v.object({ From a8dc85affb7d12ccdd5dadca7d9b0f53ce2e9f09 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:44:33 +0100 Subject: [PATCH 07/16] fix(data-loader): Ensure valid Claude data directory and if not, throw an error --- src/data-loader.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/data-loader.ts b/src/data-loader.ts index edb4b4fc..4e956c02 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -21,11 +21,15 @@ const DEFAULT_CLAUDE_CODE_PATH = path.join(homedir(), '.claude'); * Uses environment variable CLAUDE_CONFIG_DIR if set, otherwise defaults to ~/.claude */ export function getDefaultClaudePath(): string { - const envClaudeCodePath = process.env.CLAUDE_CONFIG_DIR?.trim() ?? ''; - if (isDirectorySync(envClaudeCodePath)) { - return envClaudeCodePath; + const envClaudeCodePath = process.env.CLAUDE_CONFIG_DIR?.trim() ?? DEFAULT_CLAUDE_CODE_PATH; + if (!isDirectorySync(envClaudeCodePath)) { + throw new Error( + ` Claude data directory does not exist: ${envClaudeCodePath}. +Please set CLAUDE_CONFIG_DIR to a valid path, or ensure ${DEFAULT_CLAUDE_CODE_PATH} exists. + `.trim(), + ); } - return DEFAULT_CLAUDE_CODE_PATH; + return envClaudeCodePath; } export const UsageDataSchema = v.object({ From 689d029668b324eeecc17fdd5697fdeac0e57768 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:44:54 +0100 Subject: [PATCH 08/16] test(data-loader): improve coverage for getDefaultClaudePath Refactor and expand tests for getDefaultClaudePath to cover additional edge cases, including handling of whitespace, non-existent directories, and invalid paths. Use beforeEach and afterEach hooks for environment cleanup and restoration. This ensures more robust and reliable behavior --- src/data-loader.test.ts | 108 +++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index aa68ebf2..9af38910 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from 'bun:test'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import process from 'node:process'; +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { createFixture } from 'fs-fixture'; import { calculateCostForEntry, @@ -31,65 +34,70 @@ describe('formatDate', () => { }); describe('getDefaultClaudePath', () => { - test('returns CLAUDE_CONFIG_DIR when environment variable is set', () => { - const originalEnv = process.env.CLAUDE_CONFIG_DIR; - const testPath = '/custom/claude/path'; + const originalEnv = process.env.CLAUDE_CONFIG_DIR; - try { - process.env.CLAUDE_CONFIG_DIR = testPath; - expect(getDefaultClaudePath()).toBe(testPath); + beforeEach(() => { + // Clean up env var before each test + delete process.env.CLAUDE_CONFIG_DIR; + }); + + afterEach(() => { + // Restore original environment + if (originalEnv != null) { + process.env.CLAUDE_CONFIG_DIR = originalEnv; } - finally { - // Restore original value - if (originalEnv !== undefined) { - process.env.CLAUDE_CONFIG_DIR = originalEnv; - } - else { - delete process.env.CLAUDE_CONFIG_DIR; - } + else { + delete process.env.CLAUDE_CONFIG_DIR; } }); - test('returns default path when CLAUDE_CONFIG_DIR is not set', () => { - const originalEnv = process.env.CLAUDE_CONFIG_DIR; + test('returns CLAUDE_CONFIG_DIR when environment variable is set', async () => { + await using fixture = await createFixture({ + claude: {}, + }); + process.env.CLAUDE_CONFIG_DIR = fixture.path; - try { - delete process.env.CLAUDE_CONFIG_DIR; - const result = getDefaultClaudePath(); - expect(result).toContain('.claude'); - expect(result).not.toBe('.claude'); // Should be full path with home directory - } - finally { - // Restore original value - if (originalEnv !== undefined) { - process.env.CLAUDE_CONFIG_DIR = originalEnv; - } - } + expect(getDefaultClaudePath()).toBe(fixture.path); }); - test('returns default path when CLAUDE_CONFIG_DIR is empty or whitespace', () => { - const originalEnv = process.env.CLAUDE_CONFIG_DIR; + test('returns default path when CLAUDE_CONFIG_DIR is not set', async () => { + // Ensure CLAUDE_CONFIG_DIR is not set + delete process.env.CLAUDE_CONFIG_DIR; - try { - // Test empty string - process.env.CLAUDE_CONFIG_DIR = ''; - let result = getDefaultClaudePath(); - expect(result).toContain('.claude'); + // Test that it returns the default path (which ends with .claude) + const actualPath = getDefaultClaudePath(); + expect(actualPath).toMatch(/\.claude$/); + expect(actualPath).toContain(homedir()); + }); - // Test whitespace only - process.env.CLAUDE_CONFIG_DIR = ' '; - result = getDefaultClaudePath(); - expect(result).toContain('.claude'); - } - finally { - // Restore original value - if (originalEnv !== undefined) { - process.env.CLAUDE_CONFIG_DIR = originalEnv; - } - else { - delete process.env.CLAUDE_CONFIG_DIR; - } - } + test('returns default path with trimmed CLAUDE_CONFIG_DIR', async () => { + await using fixture = await createFixture({ + claude: {}, + }); + // Test with extra spaces + process.env.CLAUDE_CONFIG_DIR = ` ${fixture.path} `; + + expect(getDefaultClaudePath()).toBe(fixture.path); + }); + + test('throws an error when CLAUDE_CONFIG_DIR is not a directory', async () => { + await using fixture = await createFixture(); + process.env.CLAUDE_CONFIG_DIR = join(fixture.path, 'not-a-directory'); + + expect(() => getDefaultClaudePath()).toThrow(/Claude data directory does not exist/); + }); + + test('throws an error when CLAUDE_CONFIG_DIR does not exist', async () => { + process.env.CLAUDE_CONFIG_DIR = '/nonexistent/path/that/does/not/exist'; + + expect(() => getDefaultClaudePath()).toThrow(/Claude data directory does not exist/); + }); + + test('throws an error when default path does not exist', () => { + // Set to a non-existent path + process.env.CLAUDE_CONFIG_DIR = '/nonexistent/path/.claude'; + + expect(() => getDefaultClaudePath()).toThrow(/Claude data directory does not exist/); }); }); From 22dbb72022269e74d133308560e82798f7996b2d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:52:23 +0100 Subject: [PATCH 09/16] refactor(cli): remove -p short option for path argument The -p short option is no longer needed since the path can now be configured via the CLAUDE_CONFIG_DIR environment variable. Users can still use --path for the full option name when needed. This simplifies the CLI interface by reducing the number of short options and encouraging use of the environment variable for persistent configuration. --- src/shared-args.internal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared-args.internal.ts b/src/shared-args.internal.ts index eabd7c06..5a597516 100644 --- a/src/shared-args.internal.ts +++ b/src/shared-args.internal.ts @@ -27,7 +27,6 @@ export const sharedArgs = { }, path: { type: 'string', - short: 'p', description: 'Custom path to Claude data directory', default: getDefaultClaudePath(), }, From 5100773a224942d4a1b371976066b2a1675821d5 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:53:03 +0100 Subject: [PATCH 10/16] docs(readme): update CLI options to remove -p short option Update the README documentation to reflect the removal of the -p short option. The --path long option remains available for users who need to override the default Claude data directory location. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d17e70b8..535f84a7 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ All commands support the following options: - `-s, --since `: Filter from date (YYYYMMDD format) - `-u, --until `: Filter until date (YYYYMMDD format) -- `-p, --path `: Custom path to Claude data directory (default: `$CLAUDE_CONFIG_DIR` or `~/.claude`) +- `--path `: Custom path to Claude data directory (default: `$CLAUDE_CONFIG_DIR` or `~/.claude`) - `-j, --json`: Output results in JSON format instead of table - `-m, --mode `: Cost calculation mode: `auto` (default), `calculate`, or `display` - `-o, --order `: Sort order: `desc` (newest first, default) or `asc` (oldest first). From d7f4bb3b84e53b498176caee0c3936ce360bbb73 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:58:17 +0100 Subject: [PATCH 11/16] refactor(cli): remove --path option entirely and use environment variable Remove the --path CLI option completely since the Claude data directory can now be configured via the CLAUDE_CONFIG_DIR environment variable. Changes: - Remove path argument from shared-args.internal.ts - Update all commands (daily, monthly, session, mcp) to use getDefaultClaudePath() - Remove unused import from shared-args.internal.ts - Fix import order in mcp.ts command This simplifies the CLI interface and encourages users to set the CLAUDE_CONFIG_DIR environment variable for persistent configuration instead of passing the path on every command invocation. --- src/commands/daily.ts | 6 +++--- src/commands/mcp.ts | 6 +++--- src/commands/monthly.ts | 6 +++--- src/commands/session.ts | 6 +++--- src/shared-args.internal.ts | 6 ------ 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/commands/daily.ts b/src/commands/daily.ts index cfbf89bc..1ef74abf 100644 --- a/src/commands/daily.ts +++ b/src/commands/daily.ts @@ -7,7 +7,7 @@ import { createTotalsObject, getTotalTokens, } from '../calculate-cost.ts'; -import { loadDailyUsageData } from '../data-loader.ts'; +import { getDefaultClaudePath, loadDailyUsageData } from '../data-loader.ts'; import { detectMismatches, printMismatchReport } from '../debug.ts'; import { log, logger } from '../logger.ts'; import { sharedCommandConfig } from '../shared-args.internal.ts'; @@ -25,7 +25,7 @@ export const dailyCommand = define({ const dailyData = await loadDailyUsageData({ since: ctx.values.since, until: ctx.values.until, - claudePath: ctx.values.path, + claudePath: getDefaultClaudePath(), mode: ctx.values.mode, order: ctx.values.order, }); @@ -45,7 +45,7 @@ export const dailyCommand = define({ // Show debug information if requested if (ctx.values.debug && !ctx.values.json) { - const mismatchStats = await detectMismatches(ctx.values.path); + const mismatchStats = await detectMismatches(getDefaultClaudePath()); printMismatchReport(mismatchStats, ctx.values.debugSamples); } diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index 85111ac5..57502789 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -1,4 +1,5 @@ import { define } from 'gunshi'; +import { getDefaultClaudePath } from '../data-loader.ts'; import { logger } from '../logger.ts'; import { createMcpServer } from '../mcp.ts'; import { sharedArgs } from '../shared-args.internal.ts'; @@ -7,7 +8,6 @@ export const mcpCommand = define({ name: 'mcp', description: 'Show usage report for MCP', args: { - path: sharedArgs.path, mode: sharedArgs.mode, type: { type: 'enum', @@ -23,14 +23,14 @@ export const mcpCommand = define({ }, }, async run(ctx) { - const { type, mode, path, port } = ctx.values; + const { type, mode, port } = ctx.values; // disable info logging if (type === 'stdio') { logger.level = 0; } const server = createMcpServer({ - claudePath: path, + claudePath: getDefaultClaudePath(), mode, }); diff --git a/src/commands/monthly.ts b/src/commands/monthly.ts index 38919c00..20f19bfe 100644 --- a/src/commands/monthly.ts +++ b/src/commands/monthly.ts @@ -7,7 +7,7 @@ import { createTotalsObject, getTotalTokens, } from '../calculate-cost.ts'; -import { loadMonthlyUsageData } from '../data-loader.ts'; +import { getDefaultClaudePath, loadMonthlyUsageData } from '../data-loader.ts'; import { detectMismatches, printMismatchReport } from '../debug.ts'; import { log, logger } from '../logger.ts'; import { sharedCommandConfig } from '../shared-args.internal.ts'; @@ -25,7 +25,7 @@ export const monthlyCommand = define({ const monthlyData = await loadMonthlyUsageData({ since: ctx.values.since, until: ctx.values.until, - claudePath: ctx.values.path, + claudePath: getDefaultClaudePath(), mode: ctx.values.mode, order: ctx.values.order, }); @@ -56,7 +56,7 @@ export const monthlyCommand = define({ // Show debug information if requested if (ctx.values.debug && !ctx.values.json) { - const mismatchStats = await detectMismatches(ctx.values.path); + const mismatchStats = await detectMismatches(getDefaultClaudePath()); printMismatchReport(mismatchStats, ctx.values.debugSamples); } diff --git a/src/commands/session.ts b/src/commands/session.ts index a40610e2..32c72d12 100644 --- a/src/commands/session.ts +++ b/src/commands/session.ts @@ -7,7 +7,7 @@ import { createTotalsObject, getTotalTokens, } from '../calculate-cost.ts'; -import { loadSessionData } from '../data-loader.ts'; +import { getDefaultClaudePath, loadSessionData } from '../data-loader.ts'; import { detectMismatches, printMismatchReport } from '../debug.ts'; import { log, logger } from '../logger.ts'; import { sharedCommandConfig } from '../shared-args.internal.ts'; @@ -25,7 +25,7 @@ export const sessionCommand = define({ const sessionData = await loadSessionData({ since: ctx.values.since, until: ctx.values.until, - claudePath: ctx.values.path, + claudePath: getDefaultClaudePath(), mode: ctx.values.mode, order: ctx.values.order, }); @@ -45,7 +45,7 @@ export const sessionCommand = define({ // Show debug information if requested if (ctx.values.debug && !ctx.values.json) { - const mismatchStats = await detectMismatches(ctx.values.path); + const mismatchStats = await detectMismatches(getDefaultClaudePath()); printMismatchReport(mismatchStats, ctx.values.debugSamples); } diff --git a/src/shared-args.internal.ts b/src/shared-args.internal.ts index 5a597516..0f382c5c 100644 --- a/src/shared-args.internal.ts +++ b/src/shared-args.internal.ts @@ -1,7 +1,6 @@ import type { Args } from 'gunshi'; import type { CostMode, SortOrder } from './types.internal.ts'; import * as v from 'valibot'; -import { getDefaultClaudePath } from './data-loader'; import { CostModes, dateSchema, SortOrders } from './types.internal.ts'; function parseDateArg(value: string): string { @@ -25,11 +24,6 @@ export const sharedArgs = { description: 'Filter until date (YYYYMMDD format)', parse: parseDateArg, }, - path: { - type: 'string', - description: 'Custom path to Claude data directory', - default: getDefaultClaudePath(), - }, json: { type: 'boolean', short: 'j', From 66864d7efef5f283f7f6689b76cc9442b4d35b6a Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:58:51 +0100 Subject: [PATCH 12/16] docs(readme): remove --path option from CLI documentation The --path option has been completely removed from the CLI. Users should now use the CLAUDE_CONFIG_DIR environment variable to configure custom Claude data directory locations. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 535f84a7..cbf141a7 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,6 @@ All commands support the following options: - `-s, --since `: Filter from date (YYYYMMDD format) - `-u, --until `: Filter until date (YYYYMMDD format) -- `--path `: Custom path to Claude data directory (default: `$CLAUDE_CONFIG_DIR` or `~/.claude`) - `-j, --json`: Output results in JSON format instead of table - `-m, --mode `: Cost calculation mode: `auto` (default), `calculate`, or `display` - `-o, --order `: Sort order: `desc` (newest first, default) or `asc` (oldest first). From c62432656853dfd09b777c0847adbe8ee4f53808 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:00:16 +0100 Subject: [PATCH 13/16] docs(readme): remove remaining --path references from examples Update all example commands to remove --path option usage. The tool now exclusively uses the CLAUDE_CONFIG_DIR environment variable for custom data directory configuration. Updated sections: - Daily report examples - Session report examples - Environment variable documentation --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cbf141a7..4edd8429 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,7 @@ ccusage daily # Filter by date range ccusage daily --since 20250525 --until 20250530 -# Use custom Claude data directory -ccusage daily --path /custom/path/to/.claude - -# Or set CLAUDE_CONFIG_DIR environment variable +# Set CLAUDE_CONFIG_DIR environment variable for custom data directory export CLAUDE_CONFIG_DIR="/custom/path/to/.claude" ccusage daily @@ -199,10 +196,7 @@ ccusage session # Filter sessions by last activity date ccusage session --since 20250525 -# Combine filters -ccusage session --since 20250525 --until 20250530 --path /custom/path - -# Or use environment variable +# Combine filters with environment variable export CLAUDE_CONFIG_DIR="/custom/path" ccusage session --since 20250525 --until 20250530 @@ -252,11 +246,11 @@ The tool supports the `CLAUDE_CONFIG_DIR` environment variable to specify the Cl export CLAUDE_CONFIG_DIR="/path/to/custom/claude/directory" ccusage daily -# The --path option takes precedence over the environment variable -ccusage daily --path "/different/path" +# The environment variable determines the Claude data directory +ccusage daily ``` -When both the environment variable and `--path` option are provided, the `--path` option takes precedence, ensuring backward compatibility. +The tool will use the path specified in the `CLAUDE_CONFIG_DIR` environment variable, or fall back to the default `~/.claude` directory if not set. ### MCP (Model Context Protocol) Support From 764ed25cadeed399e4ddf5d288c944ae74cd5bff Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:07:55 +0100 Subject: [PATCH 14/16] trigger ci: force new CI run to resolve test environment issue From 844308e7591a991b3b10103049f9891b2339642c Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:10:18 +0100 Subject: [PATCH 15/16] fix(test): handle CI environment where ~/.claude does not exist Update test to properly handle both local and CI environments: - In local dev: ~/.claude might exist, test passes - In CI: ~/.claude does not exist, test expects error This makes the test work correctly in both environments while maintaining proper validation behavior. --- src/data-loader.test.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index 9af38910..92156df1 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -60,14 +60,26 @@ describe('getDefaultClaudePath', () => { expect(getDefaultClaudePath()).toBe(fixture.path); }); - test('returns default path when CLAUDE_CONFIG_DIR is not set', async () => { + test('throws error when CLAUDE_CONFIG_DIR is not set and default path does not exist', () => { // Ensure CLAUDE_CONFIG_DIR is not set delete process.env.CLAUDE_CONFIG_DIR; - // Test that it returns the default path (which ends with .claude) - const actualPath = getDefaultClaudePath(); - expect(actualPath).toMatch(/\.claude$/); - expect(actualPath).toContain(homedir()); + // Test that it throws an error when default path doesn't exist + // This test only works in environments where ~/.claude doesn't exist (like CI) + // In local development, ~/.claude might exist, so we'll check both cases + const homeClaudePath = join(homedir(), '.claude'); + + try { + const actualPath = getDefaultClaudePath(); + // If we get here, the directory exists + expect(actualPath).toBe(homeClaudePath); + } + catch (error) { + // If an error is thrown, it should be about the directory not existing + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain('Claude data directory does not exist'); + expect((error as Error).message).toContain(homeClaudePath); + } }); test('returns default path with trimmed CLAUDE_CONFIG_DIR', async () => { From 84441d67e14675ee481227e22f97f658b80d822d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:14:11 +0100 Subject: [PATCH 16/16] fix(ci): create ~/.claude directory and clean up test - Add step to create default Claude directory in CI environment - Remove try-catch block from test, making it clean and simple - Test now works consistently in both local and CI environments --- .github/workflows/ci.yaml | 2 ++ src/data-loader.test.ts | 22 +++++----------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f2ca6271..217659aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,6 +16,8 @@ jobs: - run: bun install --frozen-lockfile - run: bun lint - run: bun typecheck + - name: Create default Claude directory for tests + run: mkdir -p $HOME/.claude - run: bun test npm-publish-dry-run-and-upload-pkg-pr-now: diff --git a/src/data-loader.test.ts b/src/data-loader.test.ts index 92156df1..cf7586f0 100644 --- a/src/data-loader.test.ts +++ b/src/data-loader.test.ts @@ -60,26 +60,14 @@ describe('getDefaultClaudePath', () => { expect(getDefaultClaudePath()).toBe(fixture.path); }); - test('throws error when CLAUDE_CONFIG_DIR is not set and default path does not exist', () => { + test('returns default path when CLAUDE_CONFIG_DIR is not set', () => { // Ensure CLAUDE_CONFIG_DIR is not set delete process.env.CLAUDE_CONFIG_DIR; - // Test that it throws an error when default path doesn't exist - // This test only works in environments where ~/.claude doesn't exist (like CI) - // In local development, ~/.claude might exist, so we'll check both cases - const homeClaudePath = join(homedir(), '.claude'); - - try { - const actualPath = getDefaultClaudePath(); - // If we get here, the directory exists - expect(actualPath).toBe(homeClaudePath); - } - catch (error) { - // If an error is thrown, it should be about the directory not existing - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain('Claude data directory does not exist'); - expect((error as Error).message).toContain(homeClaudePath); - } + // Test that it returns the default path (which ends with .claude) + const actualPath = getDefaultClaudePath(); + expect(actualPath).toMatch(/\.claude$/); + expect(actualPath).toContain(homedir()); }); test('returns default path with trimmed CLAUDE_CONFIG_DIR', async () => {