Skip to content
Merged
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
2 changes: 0 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ 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:
Expand Down
30 changes: 5 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ ccusage daily
# Filter by date range
ccusage daily --since 20250525 --until 20250530

# Set CLAUDE_CONFIG_DIR environment variable for custom data directory
export CLAUDE_CONFIG_DIR="/custom/path/to/.claude"
ccusage daily
# Use custom Claude data directory
ccusage daily --path /custom/path/to/.claude

# Output in JSON format
ccusage daily --json
Expand Down Expand Up @@ -165,10 +164,6 @@ 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

Expand Down Expand Up @@ -196,9 +191,8 @@ ccusage session
# Filter sessions by last activity date
ccusage session --since 20250525

# Combine filters with environment variable
export CLAUDE_CONFIG_DIR="/custom/path"
ccusage session --since 20250525 --until 20250530
# Combine filters
ccusage session --since 20250525 --until 20250530 --path /custom/path

# Output in JSON format
ccusage session --json
Expand All @@ -222,6 +216,7 @@ All commands support the following options:

- `-s, --since <date>`: Filter from date (YYYYMMDD format)
- `-u, --until <date>`: Filter until date (YYYYMMDD format)
- `-p, --path <path>`: Custom path to Claude data directory (default: `~/.claude`)
- `-j, --json`: Output results in JSON format instead of table
- `-m, --mode <mode>`: Cost calculation mode: `auto` (default), `calculate`, or `display`
- `-o, --order <order>`: Sort order: `desc` (newest first, default) or `asc` (oldest first).
Expand All @@ -237,21 +232,6 @@ 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 environment variable determines the Claude data directory
ccusage daily
```

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

Exposes usage data through Model Context Protocol for integration with other tools:
Expand Down
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.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"fs-fixture": "^2.8.0",
"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",
Expand Down
6 changes: 3 additions & 3 deletions src/commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
createTotalsObject,
getTotalTokens,
} from '../calculate-cost.ts';
import { getDefaultClaudePath, loadDailyUsageData } from '../data-loader.ts';
import { loadDailyUsageData } from '../data-loader.ts';
import { detectMismatches, printMismatchReport } from '../debug.ts';
import { log, logger } from '../logger.ts';
import { sharedCommandConfig } from '../shared-args.internal.ts';
Expand All @@ -25,7 +25,7 @@ export const dailyCommand = define({
const dailyData = await loadDailyUsageData({
since: ctx.values.since,
until: ctx.values.until,
claudePath: getDefaultClaudePath(),
claudePath: ctx.values.path,
mode: ctx.values.mode,
order: ctx.values.order,
});
Expand All @@ -45,7 +45,7 @@ export const dailyCommand = define({

// Show debug information if requested
if (ctx.values.debug && !ctx.values.json) {
const mismatchStats = await detectMismatches(getDefaultClaudePath());
const mismatchStats = await detectMismatches(ctx.values.path);
printMismatchReport(mismatchStats, ctx.values.debugSamples);
}

Expand Down
6 changes: 3 additions & 3 deletions src/commands/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
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';
Expand All @@ -8,6 +7,7 @@ export const mcpCommand = define({
name: 'mcp',
description: 'Show usage report for MCP',
args: {
path: sharedArgs.path,
mode: sharedArgs.mode,
type: {
type: 'enum',
Expand All @@ -23,14 +23,14 @@ export const mcpCommand = define({
},
},
async run(ctx) {
const { type, mode, port } = ctx.values;
const { type, mode, path, port } = ctx.values;
// disable info logging
if (type === 'stdio') {
logger.level = 0;
}

const server = createMcpServer({
claudePath: getDefaultClaudePath(),
claudePath: path,
mode,
});

Expand Down
6 changes: 3 additions & 3 deletions src/commands/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
createTotalsObject,
getTotalTokens,
} from '../calculate-cost.ts';
import { getDefaultClaudePath, loadMonthlyUsageData } from '../data-loader.ts';
import { loadMonthlyUsageData } from '../data-loader.ts';
import { detectMismatches, printMismatchReport } from '../debug.ts';
import { log, logger } from '../logger.ts';
import { sharedCommandConfig } from '../shared-args.internal.ts';
Expand All @@ -25,7 +25,7 @@ export const monthlyCommand = define({
const monthlyData = await loadMonthlyUsageData({
since: ctx.values.since,
until: ctx.values.until,
claudePath: getDefaultClaudePath(),
claudePath: ctx.values.path,
mode: ctx.values.mode,
order: ctx.values.order,
});
Expand Down Expand Up @@ -56,7 +56,7 @@ export const monthlyCommand = define({

// Show debug information if requested
if (ctx.values.debug && !ctx.values.json) {
const mismatchStats = await detectMismatches(getDefaultClaudePath());
const mismatchStats = await detectMismatches(ctx.values.path);
printMismatchReport(mismatchStats, ctx.values.debugSamples);
}

Expand Down
6 changes: 3 additions & 3 deletions src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
createTotalsObject,
getTotalTokens,
} from '../calculate-cost.ts';
import { getDefaultClaudePath, loadSessionData } from '../data-loader.ts';
import { loadSessionData } from '../data-loader.ts';
import { detectMismatches, printMismatchReport } from '../debug.ts';
import { log, logger } from '../logger.ts';
import { sharedCommandConfig } from '../shared-args.internal.ts';
Expand All @@ -25,7 +25,7 @@ export const sessionCommand = define({
const sessionData = await loadSessionData({
since: ctx.values.since,
until: ctx.values.until,
claudePath: getDefaultClaudePath(),
claudePath: ctx.values.path,
mode: ctx.values.mode,
order: ctx.values.order,
});
Expand All @@ -45,7 +45,7 @@ export const sessionCommand = define({

// Show debug information if requested
if (ctx.values.debug && !ctx.values.json) {
const mismatchStats = await detectMismatches(getDefaultClaudePath());
const mismatchStats = await detectMismatches(ctx.values.path);
printMismatchReport(mismatchStats, ctx.values.debugSamples);
}

Expand Down
74 changes: 1 addition & 73 deletions src/data-loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
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 { describe, expect, test } from 'bun:test';
import { createFixture } from 'fs-fixture';
import {
calculateCostForEntry,
formatDate,
getDefaultClaudePath,
loadDailyUsageData,
loadMonthlyUsageData,
loadSessionData,
Expand All @@ -33,74 +29,6 @@ describe('formatDate', () => {
});
});

describe('getDefaultClaudePath', () => {
const originalEnv = process.env.CLAUDE_CONFIG_DIR;

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;
}
else {
delete 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;

expect(getDefaultClaudePath()).toBe(fixture.path);
});

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 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 () => {
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/);
});
});

describe('loadDailyUsageData', () => {
test('returns empty array when no files found', async () => {
await using fixture = await createFixture({
Expand Down
18 changes: 1 addition & 17 deletions src/data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,18 @@ 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 { groupBy } from 'es-toolkit'; // TODO: after node20 is deprecated, switch to native Object.groupBy
import { sort } from 'fast-sort';
import { isDirectorySync } from 'path-type';
import { glob } from 'tinyglobby';
import * as v from 'valibot';
import { logger } from './logger.ts';
import {
PricingFetcher,
} from './pricing-fetcher.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 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 envClaudeCodePath;
return path.join(homedir(), '.claude');
}

export const UsageDataSchema = v.object({
Expand Down
7 changes: 7 additions & 0 deletions src/shared-args.internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Args } from 'gunshi';
import type { CostMode, SortOrder } from './types.internal.ts';
import * as v from 'valibot';
import { getDefaultClaudePath } from './data-loader.ts';
import { CostModes, dateSchema, SortOrders } from './types.internal.ts';

function parseDateArg(value: string): string {
Expand All @@ -24,6 +25,12 @@ export const sharedArgs = {
description: 'Filter until date (YYYYMMDD format)',
parse: parseDateArg,
},
path: {
type: 'string',
short: 'p',
description: 'Custom path to Claude data directory',
default: getDefaultClaudePath(),
},
json: {
type: 'boolean',
short: 'j',
Expand Down