Skip to content

feat: add CSV output support with refactored shared handlers - #231

Closed
watany-dev wants to merge 1 commit into
ccusage:mainfrom
watany-dev:toCsv
Closed

watany-dev wants to merge 1 commit into
ccusage:mainfrom
watany-dev:toCsv

Conversation

@watany-dev

Copy link
Copy Markdown

Function

The --csv option allows csv output. This is an option that works well with other system integration and spreadsheets such as excel.

Caution.

You may choose not to implement this in ccusage because it is the same as this.

npx ccusage --json | jq -r ' 
 ["date", "inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens", "totalTokens", "totalCost ", "modelsUsed"], 
 (.daily[] | [ 
.date, 
.inputTokens, 
.outputTokens, 
.cacheCreationTokens, 
.cacheReadTokens, 
.totalTokens, 
.totalCost, 
 (. modelsUsed | join(";")) 
 ]) 
 | @csv 
' 

- Add --csv flag to all commands (daily, monthly, session, blocks)
- Create shared CSV utility functions with proper escaping
- Implement DRY CSV output handlers to eliminate code duplication
- Reduce 116 lines of duplicated CSV logic to 4 lines total
- Maintain full functionality while improving maintainability
- Add comprehensive tests for CSV formatting and edge cases
- Update documentation with CSV usage examples

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @watany-dev, 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!

This pull request significantly enhances the ccusage CLI tool by adding native CSV output capabilities for all usage reports. This new feature, accessible via a --csv flag, is built upon a refactored and centralized set of CSV handling utilities, improving code maintainability and providing users with a highly requested data export format for external analysis.

Highlights

  • New Feature: CSV Output: Introduced a new --csv command-line option to generate usage reports (daily, monthly, session, and blocks) in CSV format, enabling easier integration with spreadsheets and other systems.
  • Refactored Output Handlers: Implemented shared, reusable handlers (_csv-output.ts) for generating CSV output, reducing code duplication across different report commands and centralizing CSV formatting logic.
  • CSV Utility Module: Added a dedicated utility module (_csv-utils.ts) containing functions for CSV value escaping, converting arrays of objects to CSV strings, and number/date formatting, ensuring robust and consistent CSV generation.
  • Mutual Exclusivity Validation: Added validation to prevent simultaneous use of --json and --csv options, ensuring clear and unambiguous output format selection.
  • Documentation Update: Updated CLAUDE.md to reflect the new --csv option and its usage across various report types.
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.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available 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

  1. 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. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces CSV output support across various reports and refactors shared handlers. The changes are well-structured, separating CSV-specific logic into new utility files. However, there are opportunities to improve code maintainability by centralizing duplicated validation and parsing logic into shared utility files, and to enhance type safety by refining generic type usage. Additionally, some minor improvements can be made to the CSV output formatting and the semantic representation of data for 'gap' blocks.

Comment thread src/_csv-output.ts
Comment on lines +2 to +4
import { arrayToCsv, formatDecimal } from './_csv-utils.ts';
import { calculateTotals, getTotalTokens } from './calculate-cost.ts';
import { log } from './logger.ts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validateCsvJsonExclusive function is duplicated across multiple command files. To improve maintainability and adhere to the DRY principle, this function should be moved to a shared utility file (e.g., src/_utils.ts) and imported where needed. Additionally, the parseTokenLimit function (lines 200-206) is also duplicated in src/commands/blocks.ts and should be moved to the same shared utility file.

Suggested change
import { arrayToCsv, formatDecimal } from './_csv-utils.ts';
import { calculateTotals, getTotalTokens } from './calculate-cost.ts';
import { log } from './logger.ts';
import { arrayToCsv, formatDecimal } from './_csv-utils.ts';
import { calculateTotals, getTotalTokens } from './calculate-cost.ts';
import { log } from './logger.ts';
import { validateCsvJsonExclusive, parseTokenLimit } from './_utils.ts';

Comment thread src/_csv-output.ts
Comment on lines +24 to +25
/** Empty headers template when no data */
emptyHeaders: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The emptyHeaders property in CsvOutputConfig is redundant. The arrayToCsv function in _csv-utils.ts already handles generating the header row correctly even when the data array is empty, as long as the headers array is provided. Removing this property simplifies the configuration.

Suggested change
/** Empty headers template when no data */
emptyHeaders: string;
/** Headers for the CSV file */
headers: string[];
/** Function to extract row data from each item */

Comment thread src/_csv-output.ts
Comment on lines +35 to +39
function validateCsvJsonExclusive(hasJson: boolean, hasCsv: boolean): void {
if (hasJson && hasCsv) {
throw new Error('Cannot use both --json and --csv options together');
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This validateCsvJsonExclusive function is duplicated. As mentioned in the previous comment, it should be moved to a shared utility file (e.g., src/_utils.ts) and imported here.

Comment thread src/_csv-output.ts
Comment on lines +74 to +75
const emptyColumns = new Array(columnsBeforeTokens).fill('').join(',');
const totalRow = emptyColumns ? `Total,${emptyColumns.slice(1)},${totalValues.join(',')}` : `Total,${totalValues.join(',')}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for constructing totalRow is a bit convoluted, especially the emptyColumns.slice(1) part. This can be simplified by directly constructing the array of values for the total row and then joining them.

Suggested change
const emptyColumns = new Array(columnsBeforeTokens).fill('').join(',');
const totalRow = emptyColumns ? `Total,${emptyColumns.slice(1)},${totalValues.join(',')}` : `Total,${totalValues.join(',')}`;
const emptyColumns = new Array(columnsBeforeTokens).fill('');
const totalRow = `Total,${emptyColumns.join(',')}${emptyColumns.length > 0 ? ',' : ''}${totalValues.join(',')}`;

Comment thread src/_csv-output.ts
Comment on lines +97 to +100
outputCsvData(data as any, {
headers: [timeField, 'Models', 'InputTokens', 'OutputTokens', 'CacheCreationTokens', 'CacheReadTokens', 'TotalTokens', 'Cost'],
emptyHeaders: `${timeField},Models,InputTokens,OutputTokens,CacheCreationTokens,CacheReadTokens,TotalTokens,Cost`,
includeTotals: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using data as any bypasses TypeScript's type checking. While it might work, it's generally a good practice to ensure type safety. The rowMapper expects a specific structure from item, which might not be fully captured by CsvRowData. Consider refining the generic type T in outputCsvData or the rowMapper signature to accurately reflect the expected input type, avoiding as any.

	outputCsvData(data, {
		headers: [timeField, 'Models', 'InputTokens', 'OutputTokens', 'CacheCreationTokens', 'CacheReadTokens', 'TotalTokens', 'Cost'],
		includeTotals: true,
		rowMapper: item => ({

Comment thread src/commands/daily.ts
Comment on lines +25 to +33
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This validateCsvJsonExclusive logic is duplicated. It should be removed from here and imported from a shared utility file (e.g., src/_utils.ts) as suggested in the import comment.

Suggested change
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);
validateCsvJsonExclusive(ctx.values.json, ctx.values.csv);

Comment thread src/commands/monthly.ts
import process from 'node:process';
import { define } from 'gunshi';
import pc from 'picocolors';
import { handleUsageDataCsv } from '../_csv-output.ts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validateCsvJsonExclusive function is duplicated across multiple command files. To improve maintainability and adhere to the DRY principle, this function should be moved to a shared utility file (e.g., src/_utils.ts) and imported where needed.

Suggested change
import { handleUsageDataCsv } from '../_csv-output.ts';
import { handleUsageDataCsv } from '../_csv-output.ts';
import { validateCsvJsonExclusive } from '../_utils.ts';

Comment thread src/commands/monthly.ts
Comment on lines +25 to +33
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This validateCsvJsonExclusive logic is duplicated. It should be removed from here and imported from a shared utility file (e.g., src/_utils.ts) as suggested in the import comment.

Suggested change
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);
validateCsvJsonExclusive(ctx.values.json, ctx.values.csv);

Comment thread src/commands/session.ts
import process from 'node:process';
import { define } from 'gunshi';
import pc from 'picocolors';
import { handleSessionCsv } from '../_csv-output.ts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validateCsvJsonExclusive function is duplicated across multiple command files. To improve maintainability and adhere to the DRY principle, this function should be moved to a shared utility file (e.g., src/_utils.ts) and imported where needed.

Suggested change
import { handleSessionCsv } from '../_csv-output.ts';
import { handleSessionCsv } from '../_csv-output.ts';
import { validateCsvJsonExclusive } from '../_utils.ts';

Comment thread src/commands/session.ts
Comment on lines +25 to +33
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This validateCsvJsonExclusive logic is duplicated. It should be removed from here and imported from a shared utility file (e.g., src/_utils.ts) as suggested in the import comment.

Suggested change
// Validate CSV/JSON options early
try {
if (ctx.values.json && ctx.values.csv) {
throw new Error('Cannot use both --json and --csv options together');
}
}
catch (error: any) {
logger.error(error.message);
process.exit(1);
validateCsvJsonExclusive(ctx.values.json, ctx.values.csv);

@ryoppippi

ryoppippi commented Jul 2, 2025 •

Copy link
Copy Markdown
Member

Thank you very much for your contribution and the time you spent working on this!

That said, we’re currently prioritising JSON output as the main format, as it’s more versatile and widely used in modern tooling. Maintaining support for both CSV and JSON would increase the complexity of the codebase and maintenance cost, so we’ve decided to stick with JSON only for now.

If you’d like to use CSV, you can easily convert the JSON output using jq. For example:

bunx ccusage --json | jq -r '.daily | .[] | [.date, .totalCost] | @csv'

We really appreciate your interest in improving ccusage, and hope you’ll continue contributing in the future! 🙏

@ryoppippi ryoppippi closed this Jul 2, 2025
@watany-dev

Copy link
Copy Markdown
Author

Thanks for the prompt review. It is important to keep the code base clean and we are happy with the decision!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants