Skip to content
Closed
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ This tool helps you understand the value you're getting from your subscription b
- 📁 **Custom Path**: Support for custom Claude data directory locations
- 🎨 **Beautiful Output**: Colorful table-formatted display
- 📄 **JSON Output**: Export data in structured JSON format with `--json`
- 💰 **Cost Tracking**: Shows costs in USD for each day/session
- 💰 **Cost Tracking**: Shows costs in USD or JPY for each day/session
- 💱 **Currency Support**: Switch between USD and JPY display (1 USD = 150 JPY)
- 🔄 **Cache Token Support**: Tracks and displays cache creation and cache read tokens separately

## Limitations
Expand Down Expand Up @@ -108,6 +109,9 @@ ccusage daily --since 20250525 --until 20250530
# Use custom Claude data directory
ccusage daily --path /custom/path/to/.claude

# Display costs in Japanese Yen
ccusage daily --currency JPY

# Output in JSON format
ccusage daily --json
```
Expand All @@ -128,6 +132,9 @@ ccusage session --since 20250525
# Combine filters
ccusage session --since 20250525 --until 20250530 --path /custom/path

# Display costs in Japanese Yen
ccusage session --currency JPY

# Output in JSON format
ccusage session --json
```
Expand All @@ -139,6 +146,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`)
- `-c, --currency <currency>`: Display currency - USD or JPY (default: USD, persists across sessions)
- `-j, --json`: Output results in JSON format instead of table
- `-h, --help`: Display help message
- `-v, --version`: Display version
Expand Down Expand Up @@ -183,6 +191,20 @@ All commands support the following options:
└─────────────┴────────────┴────────┴─────────┴──────────────┴────────────┴──────────────┴────────────┴───────────────┘
```

## Currency Configuration

The tool supports displaying costs in either USD (default) or JPY (Japanese Yen):

- **USD**: Default currency, shows costs with `$` symbol and 2 decimal places
- **JPY**: Japanese Yen, shows costs with `¥` symbol and no decimal places (rounded to whole numbers)
- **Conversion Rate**: Fixed at 1 USD = 150 JPY

Your currency preference is saved and persists across sessions. Once you set `--currency JPY`, all future reports will display in JPY until you explicitly change it back to USD.

Configuration is stored in:
- macOS/Linux: `~/.config/ccusage/config.json`
- Windows: `%APPDATA%\ccusage\config.json`

## Requirements
- Claude Code usage history files (`~/.claude/projects/**/*.jsonl`)

Expand Down
38 changes: 38 additions & 0 deletions bun.lock

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

8 changes: 5 additions & 3 deletions commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
createTotalsObject,
getTotalTokens,
} from "../calculate-cost.ts";
import { getCurrencyLabel } from "../currency.ts";
import { type LoadOptions, loadUsageData } from "../data-loader.ts";
import { log, logger } from "../logger.ts";
import { sharedArgs } from "../shared-args.ts";
Expand All @@ -17,6 +18,7 @@ export const dailyCommand = define({
description: "Show usage report grouped by date",
args: sharedArgs,
async run(ctx) {
const currency = ctx.values.currency;
const options: LoadOptions = {
since: ctx.values.since,
until: ctx.values.until,
Expand Down Expand Up @@ -64,7 +66,7 @@ export const dailyCommand = define({
"Cache Create",
"Cache Read",
"Total Tokens",
"Cost (USD)",
getCurrencyLabel(currency),
],
style: {
head: ["cyan"],
Expand All @@ -89,7 +91,7 @@ export const dailyCommand = define({
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatNumber(getTotalTokens(data)),
formatCurrency(data.totalCost),
formatCurrency(data.totalCost, currency),
]);
}

Expand All @@ -112,7 +114,7 @@ export const dailyCommand = define({
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatNumber(getTotalTokens(totals))),
pc.yellow(formatCurrency(totals.totalCost)),
pc.yellow(formatCurrency(totals.totalCost, currency)),
]);

// biome-ignore lint/suspicious/noConsole: <explanation>
Expand Down
8 changes: 5 additions & 3 deletions commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
createTotalsObject,
getTotalTokens,
} from "../calculate-cost.ts";
import { getCurrencyLabel } from "../currency.ts";
import { type LoadOptions, loadSessionData } from "../data-loader.ts";
import { log, logger } from "../logger.ts";
import { sharedArgs } from "../shared-args.ts";
Expand All @@ -17,6 +18,7 @@ export const sessionCommand = define({
description: "Show usage report grouped by conversation session",
args: sharedArgs,
async run(ctx) {
const currency = ctx.values.currency;
const options: LoadOptions = {
since: ctx.values.since,
until: ctx.values.until,
Expand Down Expand Up @@ -67,7 +69,7 @@ export const sessionCommand = define({
"Cache Create",
"Cache Read",
"Total Tokens",
"Cost (USD)",
getCurrencyLabel(currency),
"Last Activity",
],
style: {
Expand Down Expand Up @@ -106,7 +108,7 @@ export const sessionCommand = define({
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatNumber(getTotalTokens(data)),
formatCurrency(data.totalCost),
formatCurrency(data.totalCost, currency),
data.lastActivity,
]);
}
Expand All @@ -133,7 +135,7 @@ export const sessionCommand = define({
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatNumber(getTotalTokens(totals))),
pc.yellow(formatCurrency(totals.totalCost)),
pc.yellow(formatCurrency(totals.totalCost, currency)),
"",
]);

Expand Down
62 changes: 62 additions & 0 deletions config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { getConfigPath, getCurrency, setCurrency } from "./config";

describe("config", () => {
// Store original currency to restore after tests
let originalCurrency: "USD" | "JPY";

beforeEach(() => {
// Save the current currency setting
originalCurrency = getCurrency();
});

describe("getCurrency", () => {
test("returns current currency setting", () => {
const currency = getCurrency();
expect(["USD", "JPY"]).toContain(currency);
});
});

describe("setCurrency", () => {
test("sets currency to USD", () => {
setCurrency("USD");
expect(getCurrency()).toBe("USD");
// Restore original
setCurrency(originalCurrency);
});

test("sets currency to JPY", () => {
setCurrency("JPY");
expect(getCurrency()).toBe("JPY");
// Restore original
setCurrency(originalCurrency);
});

test("throws error for invalid currency", () => {
// biome-ignore lint/suspicious/noExplicitAny: testing invalid input
expect(() => setCurrency("EUR" as any)).toThrow(
"Invalid currency: EUR. Must be USD or JPY.",
);
});

test("persists currency setting", () => {
setCurrency("JPY");
expect(getCurrency()).toBe("JPY");

setCurrency("USD");
expect(getCurrency()).toBe("USD");

// Restore original
setCurrency(originalCurrency);
});
});

describe("getConfigPath", () => {
test("returns a valid config path", () => {
const path = getConfigPath();
expect(typeof path).toBe("string");
expect(path.length).toBeGreaterThan(0);
expect(path).toContain("ccusage");
});
});
});
39 changes: 39 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Conf from "conf";
import type { Schema } from "conf";

export type Currency = "USD" | "JPY";

interface ConfigSchema {
currency: Currency;
}

const schema: Schema<ConfigSchema> = {
currency: {
type: "string",
enum: ["USD", "JPY"],
default: "USD",
},
};

const config = new Conf<ConfigSchema>({
projectName: "ccusage",
schema,
defaults: {
currency: "USD",
},
});

export function getCurrency(): Currency {
return config.get("currency");
}

export function setCurrency(currency: Currency): void {
if (!["USD", "JPY"].includes(currency)) {
throw new Error(`Invalid currency: ${currency}. Must be USD or JPY.`);
}
config.set("currency", currency);
}

export function getConfigPath(): string {
return config.path;
}
Loading