From 8d3fd62830d594f198992c77e27949c6fbdcb375 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Thu, 25 Dec 2025 11:08:43 +0800 Subject: [PATCH] feat(ccusage): add --no-cost flag to hide cost column Add --no-cost option to all commands (daily, monthly, session, blocks) to hide cost information from table output. This is useful for Claude Pro subscribers who pay a flat rate and don't need cost tracking. - Add noCost flag to shared args - Update table utilities to support hideCost option - Apply hideCost to formatUsageDataRow, formatTotalsRow, pushBreakdownRows - Update all commands to conditionally hide cost column Closes #625 --- apps/ccusage/src/_shared-args.ts | 5 +++ apps/ccusage/src/commands/blocks.ts | 49 +++++++++++++++++------ apps/ccusage/src/commands/daily.ts | 28 ++++++------- apps/ccusage/src/commands/monthly.ts | 14 +++++-- apps/ccusage/src/commands/session.ts | 14 +++++-- packages/terminal/src/table.ts | 59 +++++++++++++++++++++------- 6 files changed, 122 insertions(+), 47 deletions(-) diff --git a/apps/ccusage/src/_shared-args.ts b/apps/ccusage/src/_shared-args.ts index e6ec5f27..727da225 100644 --- a/apps/ccusage/src/_shared-args.ts +++ b/apps/ccusage/src/_shared-args.ts @@ -108,6 +108,11 @@ export const sharedArgs = { description: 'Force compact mode for narrow displays (better for screenshots)', default: false, }, + noCost: { + type: 'boolean', + description: 'Hide cost column from output (useful for Pro subscribers)', + default: false, + }, } as const satisfies Args; /** diff --git a/apps/ccusage/src/commands/blocks.ts b/apps/ccusage/src/commands/blocks.ts index 57ef1520..75339399 100644 --- a/apps/ccusage/src/commands/blocks.ts +++ b/apps/ccusage/src/commands/blocks.ts @@ -323,6 +323,7 @@ export const blocksCommand = define({ } const burnRate = calculateBurnRate(block); const projection = projectBlockUsage(block); + const hideCost = Boolean(ctx.values.noCost); logger.box('Current Session Block Status'); @@ -340,18 +341,33 @@ export const blocksCommand = define({ log(pc.bold('Current Usage:')); log(` Input Tokens: ${formatNumber(block.tokenCounts.inputTokens)}`); log(` Output Tokens: ${formatNumber(block.tokenCounts.outputTokens)}`); - log(` Total Cost: ${formatCurrency(block.costUSD)}\n`); + if (!hideCost) { + log(` Total Cost: ${formatCurrency(block.costUSD)}\n`); + } + else { + log(''); + } if (burnRate != null) { log(pc.bold('Burn Rate:')); log(` Tokens/minute: ${formatNumber(burnRate.tokensPerMinute)}`); - log(` Cost/hour: ${formatCurrency(burnRate.costPerHour)}\n`); + if (!hideCost) { + log(` Cost/hour: ${formatCurrency(burnRate.costPerHour)}\n`); + } + else { + log(''); + } } if (projection != null) { log(pc.bold('Projected Usage (if current rate continues):')); log(` Total Tokens: ${formatNumber(projection.totalTokens)}`); - log(` Total Cost: ${formatCurrency(projection.totalCost)}\n`); + if (!hideCost) { + log(` Total Cost: ${formatCurrency(projection.totalCost)}\n`); + } + else { + log(''); + } if (ctx.values.tokenLimit != null) { // Parse token limit @@ -381,6 +397,7 @@ export const blocksCommand = define({ // Calculate token limit if "max" is specified const actualTokenLimit = parseTokenLimit(ctx.values.tokenLimit, maxTokensFromAll); + const hideCost = Boolean(ctx.values.noCost); const tableHeaders = ['Block Start', 'Duration/Status', 'Models', 'Tokens']; const tableAligns: ('left' | 'right' | 'center')[] = ['left', 'left', 'left', 'right']; @@ -391,8 +408,10 @@ export const blocksCommand = define({ tableAligns.push('right'); } - tableHeaders.push('Cost'); - tableAligns.push('right'); + if (!hideCost) { + tableHeaders.push('Cost'); + tableAligns.push('right'); + } const table = new ResponsiveTable({ head: tableHeaders, @@ -420,7 +439,9 @@ export const blocksCommand = define({ if (actualTokenLimit != null && actualTokenLimit > 0) { gapRow.push(pc.gray('-')); } - gapRow.push(pc.gray('-')); + if (!hideCost) { + gapRow.push(pc.gray('-')); + } table.push(gapRow); } else { @@ -442,7 +463,9 @@ export const blocksCommand = define({ row.push(percentage > 100 ? pc.red(percentText) : percentText); } - row.push(formatCurrency(block.costUSD)); + if (!hideCost) { + row.push(formatCurrency(block.costUSD)); + } table.push(row); // Add REMAINING and PROJECTED rows for active blocks @@ -461,14 +484,16 @@ export const blocksCommand = define({ ? `${remainingPercent.toFixed(1)}%` : pc.red('0.0%'); - const remainingRow = [ + const remainingRow: (string | { content: string; hAlign: 'right' })[] = [ { content: pc.gray(`(assuming ${formatNumber(actualTokenLimit)} token limit)`), hAlign: 'right' as const }, pc.blue('REMAINING'), '', remainingText, remainingPercentText, - '', // No cost for remaining - it's about token limit, not cost ]; + if (!hideCost) { + remainingRow.push(''); // No cost for remaining - it's about token limit, not cost + } table.push(remainingRow); } @@ -480,7 +505,7 @@ export const blocksCommand = define({ ? pc.red(projectedTokens) : projectedTokens; - const projectedRow = [ + const projectedRow: (string | { content: string; hAlign: 'right' })[] = [ { content: pc.gray('(assuming current burn rate)'), hAlign: 'right' as const }, pc.yellow('PROJECTED'), '', @@ -494,7 +519,9 @@ export const blocksCommand = define({ projectedRow.push(percentText); } - projectedRow.push(formatCurrency(projection.totalCost)); + if (!hideCost) { + projectedRow.push(formatCurrency(projection.totalCost)); + } table.push(projectedRow); } } diff --git a/apps/ccusage/src/commands/daily.ts b/apps/ccusage/src/commands/daily.ts index 03dafc89..d595c972 100644 --- a/apps/ccusage/src/commands/daily.ts +++ b/apps/ccusage/src/commands/daily.ts @@ -133,14 +133,20 @@ export const dailyCommand = define({ // Print header logger.box('Claude Code Token Usage Report - Daily'); + const hideCost = Boolean(mergedOptions.noCost); + // Create table with compact mode support const tableConfig: UsageReportConfig = { firstColumnName: 'Date', dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined), forceCompact: ctx.values.compact, + hideCost, }; const table = createUsageReportTable(tableConfig); + // Calculate column count based on hideCost + const columnCount = hideCost ? 7 : 8; + // Add daily data - group by project if instances flag is used if (Boolean(mergedOptions.instances) && dailyData.some(d => d.project != null)) { // Group data by project for visual separation @@ -151,19 +157,13 @@ export const dailyCommand = define({ // Add project section header if (!isFirstProject) { // Add empty row for visual separation between projects - table.push(['', '', '', '', '', '', '', '']); + table.push(Array.from({ length: columnCount }, () => '')); } // Add project header row table.push([ pc.cyan(`Project: ${formatProjectName(projectName, projectAliases)}`), - '', - '', - '', - '', - '', - '', - '', + ...Array.from({ length: columnCount - 1 }, () => ''), ]); // Add data rows for this project @@ -175,12 +175,12 @@ export const dailyCommand = define({ cacheReadTokens: data.cacheReadTokens, totalCost: data.totalCost, modelsUsed: data.modelsUsed, - }); + }, { hideCost }); table.push(row); // Add model breakdown rows if flag is set if (mergedOptions.breakdown) { - pushBreakdownRows(table, data.modelBreakdowns); + pushBreakdownRows(table, data.modelBreakdowns, 1, 0, hideCost); } } @@ -198,18 +198,18 @@ export const dailyCommand = define({ cacheReadTokens: data.cacheReadTokens, totalCost: data.totalCost, modelsUsed: data.modelsUsed, - }); + }, { hideCost }); table.push(row); // Add model breakdown rows if flag is set if (mergedOptions.breakdown) { - pushBreakdownRows(table, data.modelBreakdowns); + pushBreakdownRows(table, data.modelBreakdowns, 1, 0, hideCost); } } } // Add empty row for visual separation before totals - addEmptySeparatorRow(table, 8); + addEmptySeparatorRow(table, columnCount); // Add totals const totalsRow = formatTotalsRow({ @@ -218,7 +218,7 @@ export const dailyCommand = define({ cacheCreationTokens: totals.cacheCreationTokens, cacheReadTokens: totals.cacheReadTokens, totalCost: totals.totalCost, - }); + }, { hideCost }); table.push(totalsRow); log(table.toString()); diff --git a/apps/ccusage/src/commands/monthly.ts b/apps/ccusage/src/commands/monthly.ts index 1d7b4410..f777a1d4 100644 --- a/apps/ccusage/src/commands/monthly.ts +++ b/apps/ccusage/src/commands/monthly.ts @@ -98,14 +98,20 @@ export const monthlyCommand = define({ // Print header logger.box('Claude Code Token Usage Report - Monthly'); + const hideCost = Boolean(mergedOptions.noCost); + // Create table with compact mode support const tableConfig: UsageReportConfig = { firstColumnName: 'Month', dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? DEFAULT_LOCALE), forceCompact: ctx.values.compact, + hideCost, }; const table = createUsageReportTable(tableConfig); + // Calculate column count based on hideCost + const columnCount = hideCost ? 7 : 8; + // Add monthly data for (const data of monthlyData) { // Main row @@ -116,17 +122,17 @@ export const monthlyCommand = define({ cacheReadTokens: data.cacheReadTokens, totalCost: data.totalCost, modelsUsed: data.modelsUsed, - }); + }, { hideCost }); table.push(row); // Add model breakdown rows if flag is set if (mergedOptions.breakdown) { - pushBreakdownRows(table, data.modelBreakdowns); + pushBreakdownRows(table, data.modelBreakdowns, 1, 0, hideCost); } } // Add empty row for visual separation before totals - addEmptySeparatorRow(table, 8); + addEmptySeparatorRow(table, columnCount); // Add totals const totalsRow = formatTotalsRow({ @@ -135,7 +141,7 @@ export const monthlyCommand = define({ cacheCreationTokens: totals.cacheCreationTokens, cacheReadTokens: totals.cacheReadTokens, totalCost: totals.totalCost, - }); + }, { hideCost }); table.push(totalsRow); log(table.toString()); diff --git a/apps/ccusage/src/commands/session.ts b/apps/ccusage/src/commands/session.ts index 0b19f98e..dc7b511d 100644 --- a/apps/ccusage/src/commands/session.ts +++ b/apps/ccusage/src/commands/session.ts @@ -124,15 +124,21 @@ export const sessionCommand = define({ // Print header logger.box('Claude Code Token Usage Report - By Session'); + const hideCost = Boolean(ctx.values.noCost); + // Create table with compact mode support const tableConfig: UsageReportConfig = { firstColumnName: 'Session', includeLastActivity: true, dateFormatter: (dateStr: string) => formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale), forceCompact: ctx.values.compact, + hideCost, }; const table = createUsageReportTable(tableConfig); + // Calculate column count based on hideCost (session has Last Activity column) + const columnCount = hideCost ? 8 : 9; + // Add session data let maxSessionLength = 0; for (const data of sessionData) { @@ -148,18 +154,18 @@ export const sessionCommand = define({ cacheReadTokens: data.cacheReadTokens, totalCost: data.totalCost, modelsUsed: data.modelsUsed, - }, data.lastActivity); + }, { lastActivity: data.lastActivity, hideCost }); table.push(row); // Add model breakdown rows if flag is set if (ctx.values.breakdown) { // Session has 1 extra column before data and 1 trailing column - pushBreakdownRows(table, data.modelBreakdowns, 1, 1); + pushBreakdownRows(table, data.modelBreakdowns, 1, 1, hideCost); } } // Add empty row for visual separation before totals - addEmptySeparatorRow(table, 9); + addEmptySeparatorRow(table, columnCount); // Add totals const totalsRow = formatTotalsRow({ @@ -168,7 +174,7 @@ export const sessionCommand = define({ cacheCreationTokens: totals.cacheCreationTokens, cacheReadTokens: totals.cacheReadTokens, totalCost: totals.totalCost, - }, true); // Include Last Activity column + }, { includeLastActivity: true, hideCost }); table.push(totalsRow); log(table.toString()); diff --git a/packages/terminal/src/table.ts b/packages/terminal/src/table.ts index 1874e76b..154cbdb1 100644 --- a/packages/terminal/src/table.ts +++ b/packages/terminal/src/table.ts @@ -365,6 +365,7 @@ export function formatModelsDisplayMultiline(models: string[]): string { * @param breakdowns - Array of model breakdowns * @param extraColumns - Number of extra empty columns before the data (default: 1 for models column) * @param trailingColumns - Number of extra empty columns after the data (default: 0) + * @param hideCost - Whether to hide the cost column (default: false) */ export function pushBreakdownRows( table: { push: (row: (string | number)[]) => void }, @@ -378,6 +379,7 @@ export function pushBreakdownRows( }>, extraColumns = 1, trailingColumns = 0, + hideCost = false, ): void { for (const breakdown of breakdowns) { const row: (string | number)[] = [` └─ ${formatModelName(breakdown.modelName)}`]; @@ -397,9 +399,12 @@ export function pushBreakdownRows( pc.gray(formatNumber(breakdown.cacheCreationTokens)), pc.gray(formatNumber(breakdown.cacheReadTokens)), pc.gray(formatNumber(totalTokens)), - pc.gray(formatCurrency(breakdown.cost)), ); + if (!hideCost) { + row.push(pc.gray(formatCurrency(breakdown.cost))); + } + // Add trailing empty columns for (let i = 0; i < trailingColumns; i++) { row.push(''); @@ -421,6 +426,8 @@ export type UsageReportConfig = { dateFormatter?: (dateStr: string) => string; /** Force compact mode regardless of terminal width */ forceCompact?: boolean; + /** Hide cost column from output */ + hideCost?: boolean; }; /** @@ -441,6 +448,8 @@ export type UsageData = { * @returns Configured ResponsiveTable instance */ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTable { + const hideCost = config.hideCost ?? false; + const baseHeaders = [ config.firstColumnName, 'Models', @@ -449,7 +458,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab 'Cache Create', 'Cache Read', 'Total Tokens', - 'Cost (USD)', + ...(hideCost ? [] : ['Cost (USD)']), ]; const baseAligns: TableCellAlign[] = [ @@ -460,7 +469,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab 'right', 'right', 'right', - 'right', + ...(hideCost ? [] : ['right' as TableCellAlign]), ]; const compactHeaders = [ @@ -468,7 +477,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab 'Models', 'Input', 'Output', - 'Cost (USD)', + ...(hideCost ? [] : ['Cost (USD)']), ]; const compactAligns: TableCellAlign[] = [ @@ -476,7 +485,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab 'left', 'right', 'right', - 'right', + ...(hideCost ? [] : ['right' as TableCellAlign]), ]; // Add Last Activity column for session reports @@ -499,19 +508,30 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab }); } +/** + * Options for formatting usage data rows + */ +export type FormatRowOptions = { + /** Optional last activity value (for session reports) */ + lastActivity?: string; + /** Hide cost from the row */ + hideCost?: boolean; +}; + /** * Formats a usage data row for display in the table * @param firstColumnValue - Value for the first column (date, month, etc.) * @param data - Usage data containing tokens and cost information - * @param lastActivity - Optional last activity value (for session reports) + * @param options - Optional formatting options * @returns Formatted table row */ export function formatUsageDataRow( firstColumnValue: string, data: UsageData, - lastActivity?: string, + options?: FormatRowOptions, ): (string | number)[] { const totalTokens = data.inputTokens + data.outputTokens + data.cacheCreationTokens + data.cacheReadTokens; + const hideCost = options?.hideCost ?? false; const row: (string | number)[] = [ firstColumnValue, @@ -521,24 +541,35 @@ export function formatUsageDataRow( formatNumber(data.cacheCreationTokens), formatNumber(data.cacheReadTokens), formatNumber(totalTokens), - formatCurrency(data.totalCost), + ...(hideCost ? [] : [formatCurrency(data.totalCost)]), ]; - if (lastActivity !== undefined) { - row.push(lastActivity); + if (options?.lastActivity !== undefined) { + row.push(options.lastActivity); } return row; } +/** + * Options for formatting totals row + */ +export type FormatTotalsOptions = { + /** Whether to include an empty last activity column */ + includeLastActivity?: boolean; + /** Hide cost from the row */ + hideCost?: boolean; +}; + /** * Creates a totals row with yellow highlighting * @param totals - Totals data to display - * @param includeLastActivity - Whether to include an empty last activity column + * @param options - Optional formatting options * @returns Formatted totals row */ -export function formatTotalsRow(totals: UsageData, includeLastActivity = false): (string | number)[] { +export function formatTotalsRow(totals: UsageData, options?: FormatTotalsOptions): (string | number)[] { const totalTokens = totals.inputTokens + totals.outputTokens + totals.cacheCreationTokens + totals.cacheReadTokens; + const hideCost = options?.hideCost ?? false; const row: (string | number)[] = [ pc.yellow('Total'), @@ -548,10 +579,10 @@ export function formatTotalsRow(totals: UsageData, includeLastActivity = false): pc.yellow(formatNumber(totals.cacheCreationTokens)), pc.yellow(formatNumber(totals.cacheReadTokens)), pc.yellow(formatNumber(totalTokens)), - pc.yellow(formatCurrency(totals.totalCost)), + ...(hideCost ? [] : [pc.yellow(formatCurrency(totals.totalCost))]), ]; - if (includeLastActivity) { + if (options?.includeLastActivity ?? false) { row.push(''); // Empty for Last Activity column in totals }