Repository navigation
feat: add CSV output support with refactored shared handlers - #231
watany-dev wants to merge 1 commit into
Conversation
- 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]>
There was a problem hiding this comment.
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
--csvcommand-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
--jsonand--csvoptions, ensuring clear and unambiguous output format selection. - Documentation Update: Updated
CLAUDE.mdto reflect the new--csvoption 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
-
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 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.
| import { arrayToCsv, formatDecimal } from './_csv-utils.ts'; | ||
| import { calculateTotals, getTotalTokens } from './calculate-cost.ts'; | ||
| import { log } from './logger.ts'; |
There was a problem hiding this comment.
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.
| 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'; |
| /** Empty headers template when no data */ | ||
| emptyHeaders: string; |
There was a problem hiding this comment.
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.
| /** Empty headers template when no data */ | |
| emptyHeaders: string; | |
| /** Headers for the CSV file */ | |
| headers: string[]; | |
| /** Function to extract row data from each item */ |
| function validateCsvJsonExclusive(hasJson: boolean, hasCsv: boolean): void { | ||
| if (hasJson && hasCsv) { | ||
| throw new Error('Cannot use both --json and --csv options together'); | ||
| } | ||
| } |
| const emptyColumns = new Array(columnsBeforeTokens).fill('').join(','); | ||
| const totalRow = emptyColumns ? `Total,${emptyColumns.slice(1)},${totalValues.join(',')}` : `Total,${totalValues.join(',')}`; |
There was a problem hiding this comment.
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.
| 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(',')}`; |
| outputCsvData(data as any, { | ||
| headers: [timeField, 'Models', 'InputTokens', 'OutputTokens', 'CacheCreationTokens', 'CacheReadTokens', 'TotalTokens', 'Cost'], | ||
| emptyHeaders: `${timeField},Models,InputTokens,OutputTokens,CacheCreationTokens,CacheReadTokens,TotalTokens,Cost`, | ||
| includeTotals: true, |
There was a problem hiding this comment.
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 => ({| // 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); |
There was a problem hiding this comment.
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.
| // 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); |
| import process from 'node:process'; | ||
| import { define } from 'gunshi'; | ||
| import pc from 'picocolors'; | ||
| import { handleUsageDataCsv } from '../_csv-output.ts'; |
There was a problem hiding this comment.
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.
| import { handleUsageDataCsv } from '../_csv-output.ts'; | |
| import { handleUsageDataCsv } from '../_csv-output.ts'; | |
| import { validateCsvJsonExclusive } from '../_utils.ts'; |
| // 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); |
There was a problem hiding this comment.
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.
| // 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); |
| import process from 'node:process'; | ||
| import { define } from 'gunshi'; | ||
| import pc from 'picocolors'; | ||
| import { handleSessionCsv } from '../_csv-output.ts'; |
There was a problem hiding this comment.
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.
| import { handleSessionCsv } from '../_csv-output.ts'; | |
| import { handleSessionCsv } from '../_csv-output.ts'; | |
| import { validateCsvJsonExclusive } from '../_utils.ts'; |
| // 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); |
There was a problem hiding this comment.
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.
| // 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); |
|
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 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! 🙏 |
|
Thanks for the prompt review. It is important to keep the code base clean and we are happy with the decision! |
Function
The
--csvoption 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.