feat: add multi-currency support with live exchange rates#15
feat: add multi-currency support with live exchange rates#15IrumiDesu7 wants to merge 2 commits into
Conversation
- Add support for 20 currencies (IDR, EUR, GBP, JPY, etc.) - Implement CurrencyService with exchange rate API integration - Add local caching with 2-hour TTL for exchange rates - Update CLI with --currency/-c flag for all commands - Convert costs from USD to target currency at display time - Add proper currency formatting and symbols - Include comprehensive test coverage - Graceful fallback to USD on API failures
There was a problem hiding this comment.
Hello @IrumiDesu7, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
Summary of Changes
Hello! Gemini here, providing a summary of this pull request. This PR introduces comprehensive multi-currency support to the ccusage CLI tool. The primary goal is to allow users to view their Claude usage costs in their preferred currency rather than being limited to USD. This is achieved by fetching live exchange rates from an external API, caching them locally for performance and offline access, and implementing robust fallback mechanisms in case of API or cache issues. The changes integrate this new functionality into the existing daily and session commands, adding a new --currency flag and ensuring proper currency formatting in both table and JSON outputs. The implementation keeps internal calculations in USD for consistency and performs conversion only at the display layer.
Highlights
- Multi-Currency Support: Adds a new
--currency(or-c) CLI flag to specify the desired output currency. Supports 20 different currencies. - Live Exchange Rates: Integrates with the exchangerate-api.com service to fetch current exchange rates relative to USD.
- Local Caching: Implements a local cache for exchange rates in
~/.claude/exchange-rates.jsonwith a 2-hour TTL to improve performance and provide offline capability. - Robust Fallbacks: Includes logic to gracefully handle API failures by falling back to stale cached rates, and ultimately falling back to USD if no valid rate can be obtained.
- Currency Formatting: Adds currency-specific formatting, including symbols and decimal place handling (e.g., no decimals for JPY/KRW).
- CLI Integration: Modifies the
dailyandsessioncommands to accept the new--currencyflag and display costs in the selected currency in both table and JSON output formats. - New Currency Service: Introduces a dedicated
CurrencyServiceclass to encapsulate the logic for fetching, caching, converting, and formatting currency.
Changelog
Click here to see the changelog
- src/calculate-cost.ts
- Imports
Currencytype andcurrencyServiceinstance. - Adds
convertCostToCurrencyasync function to convert a USD amount to a target currency using thecurrencyService. - Adds
createTotalsObjectWithCurrencyasync function to create a totals object including the converted cost and the target currency.
- Imports
- src/commands/daily.ts
- Imports currency-related functions (
createTotalsObjectWithCurrency,convertCostToCurrency) and types (Currency,CurrencyService). - Modifies the JSON output logic to use
createTotalsObjectWithCurrencyfor the totals and iterate through daily data to convert each item's cost usingconvertCostToCurrency, adding thecurrencyfield to each daily entry. - Updates the table header to dynamically display the cost column label based on the selected currency (e.g., 'Cost (EUR)').
- Modifies the loop adding daily data rows to the table to convert the cost using
convertCostToCurrencyand format it usingCurrencyService.formatAmount. - Modifies the logic for adding the total row to the table to convert the total cost using
convertCostToCurrencyand format it usingCurrencyService.formatAmount.
- Imports currency-related functions (
- src/commands/session.ts
- Imports currency-related functions (
createTotalsObjectWithCurrency,convertCostToCurrency) and types (Currency,CurrencyService). - Modifies the JSON output logic to use
createTotalsObjectWithCurrencyfor the totals and iterate through session data to convert each item's cost usingconvertCostToCurrency, adding thecurrencyfield to each session entry. - Updates the table header to dynamically display the cost column label based on the selected currency (e.g., 'Cost (JPY)').
- Modifies the loop adding session data rows to the table to convert the cost using
convertCostToCurrencyand format it usingCurrencyService.formatAmount. - Modifies the logic for adding the total row to the table to convert the total cost using
convertCostToCurrencyand format it usingCurrencyService.formatAmount.
- Imports currency-related functions (
- src/currency.test.ts
- New file containing comprehensive unit tests for the
CurrencyServiceclass. - Tests cover fetching exchange rates (including USD=1, successful fetch, currency not found fallback, API error fallback), converting amounts from USD, getting currency symbols, and formatting amounts according to currency conventions.
- New file containing comprehensive unit tests for the
- src/currency.ts
- New file defining the
CurrencyServiceclass. - Implements methods for fetching exchange rates from
exchangerate-api.com, handling API responses, and managing a local cache (~/.claude/exchange-rates.json) with a 2-hour TTL. - Includes logic for loading and saving the cache using Node.js file system promises.
- Provides the
convertFromUSDmethod to perform the actual currency conversion. - Includes static methods
getCurrencySymbolandformatAmountfor currency-specific display formatting. - Implements fallback logic for API errors, missing currencies, and invalid cache files.
- Exports a singleton instance of
CurrencyService.
- New file defining the
- src/shared-args.ts
- Imports
SupportedCurrenciesandCurrencytypes. - Adds a new command-line argument
--currency(short-c) to thesharedArgsobject. - Configures the
--currencyargument as an enum with choices fromSupportedCurrenciesand a default value of 'USD'.
- Imports
- src/types.ts
- Defines the
SupportedCurrenciesarray containing the list of 20 supported currency codes. - Defines the
Currencytype as a union of the strings inSupportedCurrencies. - Defines the
ExchangeRateinterface for the structure of cached exchange rate data (currency code, rate, timestamp).
- Defines the
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Code Review
This is a well-executed pull request that adds significant value by introducing multi-currency support. The implementation is thoughtful, with robust caching, good fallback mechanisms, and comprehensive test coverage for the new CurrencyService.
The code is generally clear and follows good practices. The PR description is excellent and provides great context.
I have one main point of feedback regarding currency formatting consistency, detailed below. Addressing this will ensure the feature behaves as consistently as documented and tested.
Summary of Findings
- Currency Formatting Consistency (IDR): The test case for IDR formatting in
src/currency.test.tsexpects thousand separators, while the implementation and PR description examples for IDR do not use them. This should be aligned for consistency. - Explicit Return Types (Low Severity - Not Commented): In
src/calculate-cost.ts, the new functioncreateTotalsObjectWithCurrencycould benefit from an explicit return type (e.g.,Promise<TotalsWithCurrency>) for improved type safety and code clarity. This was not commented on due to review settings. - Minor Code Style (Low Severity - Not Commented): There are a few minor style issues such as extra blank lines (e.g.,
src/calculate-cost.ts:58,src/currency.ts:37) and missing newlines at the end of files (src/currency.test.ts,src/currency.ts). These were not commented on due to review settings.
Merge Readiness
This pull request introduces a valuable multi-currency feature and is largely in good shape. However, there is a medium-severity issue regarding currency formatting consistency for IDR that should be addressed to ensure the tests accurately reflect the intended behavior and documentation.
Once this point is clarified and potentially adjusted, the PR should be ready for merging. As a reviewer, I am not authorized to approve pull requests, so please ensure further review and approval from authorized team members before merging.
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ead ref External review ccusage#15 (nit a) flagged three remaining multi-paragraph comment blocks as obvious targets for the comment-trim pattern this branch has been applying (b529041, 0cbfeac). Nit d flagged the test-internal `ccusage#1209-r1q` review-thread reference as meaningless post-squash. Trims (load-bearing rationale retained; narrative prose dropped): * adapter/all/loader.rs `summary_rows` filter rationale: 16->8 lines. Kept the relaxed-predicate invariant + exact-zero safety claim; dropped the `session.shutdown` example narrative and the `unwrap_or(0.0)/total_cost = 0.0` source trace. * adapter/all/types.rs `AllRow.credits` doc: 19->9 lines. Kept the byte-parity claim (Option::is_some per-row, SUM > 0.0 totals) and the `None` vs `Some(0.0)` distinction; dropped the metadata side-channel essay. * adapter/copilot/loader.rs mode-dispatch table narrative: 25->17 lines. Kept the load-bearing table verbatim, the field-presence rationale, the README pointer, the `Some(0.0)` semantics, and the synthetic-row bypass pointer; dropped the duplicated "Auto/Display bill those as a genuine $0" rephrasing and the `parse_session_state_file` provenance prose. Scrubbed: * adapter/all/loader.rs `summary_rows_relaxed_filter_applies_uniformly_across_non_copilot_agents` test comment dropped the `External review ccusage#1209-r1q` / `r1q-r1 follow-up` references. The behavior they document is identical; only the transient review-thread provenance is gone. Validation: * `cargo fmt --check` clean. * `cargo clippy --workspace --all-targets -- -D warnings` clean. * `cargo test --workspace`: 364 tests pass, 0 failed (unchanged). Diff: 3 files changed, +23 / -50 lines (-27 net). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Summary
This PR adds comprehensive multi-currency support to ccusage, allowing users to view Claude usage costs in their preferred currency with live exchange rates.
Key Features
~/.claude/exchange-rates.jsonfor performance--currency/-cflag works with all existing commandsUsage Examples
Technical Implementation
Core Components:
CurrencyService: Handles exchange rate fetching, caching, and conversionExchangeRateinterface: Type-safe currency data structureconvertCostToCurrency(): Utility function for USD→target conversionArchitecture Decisions:
Error Handling:
Files Added/Modified
New Files:
src/currency.ts- Core currency service implementationsrc/currency.test.ts- Comprehensive test coverageModified Files:
src/types.ts- Currency types and validation schemassrc/shared-args.ts- CLI argument definitionsrc/calculate-cost.ts- Currency conversion utilitiessrc/commands/daily.ts- Multi-currency table/JSON outputsrc/commands/session.ts- Multi-currency table/JSON outputTesting
✅ Comprehensive test coverage for CurrencyService including:
✅ Manual testing verified:
Example Output
Table Format (IDR):
JSON Format (EUR):
{ "daily": [ { "date": "2025-06-06", "totalCost": 59.01, "currency": "EUR" } ], "totals": { "totalCost": 76.77, "currency": "EUR" } }Backward Compatibility
✅ Fully backward compatible - defaults to USD if no currency specified
✅ No breaking changes to existing CLI interface or JSON structure
✅ Maintains data integrity - all internal calculations still in USD
Test Plan
Implementation Notes
This feature enables international users to better understand their Claude Code usage costs in familiar currencies. The implementation prioritizes reliability with multiple fallback layers and maintains full backward compatibility.
The exchange rate API (exchangerate-api.com) is free, reliable, and doesn't require registration, making it ideal for this use case. Local caching ensures good performance and offline functionality.