Skip to content
Merged
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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ Requires **Node.js 22.13+** and at least one supported tool with session data on
codeburn overview # this month, clean tables
codeburn overview --no-color # plain text, ready to paste
codeburn overview --from 2026-06-01 --to 2026-06-15 # any date range
codeburn overview -p all # all time
codeburn overview -p all # last 6 months
codeburn overview -p lifetime # full history (uncapped)
codeburn overview --provider claude # one tool only
```

Expand Down Expand Up @@ -386,7 +387,7 @@ Adding a new provider is a single file. See `src/providers/codex.ts` for an exam
<details>
<summary><strong>All commands and keyboard shortcuts</strong></summary>

Run `codeburn` for the dashboard, or use a subcommand below. Most commands also accept `--provider`, `--project` / `--exclude`, and a period flag (`-p today|week|30days|month|all`).
Run `codeburn` for the dashboard, or use a subcommand below. Most commands also accept `--provider`, `--project` / `--exclude`, and a period flag (`-p today|week|30days|month|all|lifetime`).

**Dashboard & reports**

Expand Down Expand Up @@ -472,7 +473,7 @@ Sync sends token counts, costs, models, and projects, never prompts or code. Thi
| `codeburn models --task feature` | Filter to feature-development work |
| `codeburn models --provider claude` | Filter to a single provider |

Left/right arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--from` / `--to` for an exact historical window). The main Daily Activity panel always shows scrollable full history: use up/down to move one day, Page Up/Page Down (or Shift+Space/Space) to page, and `g`/`G` to jump to either end. These keys update the panel in place instead of moving terminal scrollback. Press `q` to quit, `1` `2` `3` `4` `5` as period shortcuts, `c` to open model comparison, or `o` to open optimize. The dashboard auto-refreshes every 30 seconds by default (`--refresh 0` to disable). It also shows average cost per session and the five most expensive sessions across all projects.
Left/right arrow keys switch between Today, 7 Days, 30 Days, Month, 6 Months, and Lifetime (use `--from` / `--to` for an exact historical window). The main Daily Activity panel always shows scrollable full history: use up/down to move one day, Page Up/Page Down (or Shift+Space/Space) to page, and `g`/`G` to jump to either end. These keys update the panel in place instead of moving terminal scrollback. Press `q` to quit, `1` `2` `3` `4` `5` `6` as period shortcuts, `c` to open model comparison, or `o` to open optimize. The dashboard auto-refreshes every 30 seconds by default (`--refresh 0` to disable). It also shows average cost per session and the five most expensive sessions across all projects.

</details>

Expand Down Expand Up @@ -582,7 +583,7 @@ codeburn report --from 2026-04-01 # this date through today
codeburn report --to 2026-04-10 # earliest data through this date
```

Either flag alone is valid. Inverted or malformed dates exit with a clear error. In the TUI, the custom range sets the initial load only; pressing `1` through `5` switches back to predefined periods.
Either flag alone is valid. Inverted or malformed dates exit with a clear error. In the TUI, the custom range sets the initial load only; pressing `1` through `6` switches back to predefined periods.

### Diagnosing detection

Expand Down
27 changes: 16 additions & 11 deletions src/cli-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ const END_OF_DAY_MINUTES = 59
const END_OF_DAY_SECONDS = 59
const END_OF_DAY_MS = 999

// "All Time" is intentionally bounded to the last 6 months. Older data is
// rarely actionable for a cost tracker, and capping the range keeps the parse
// path bounded so providers like Codex/Cursor with sparse multi-year history
// still load in seconds. Users who need an unbounded window can use
// `--from` / `--to`.
// The "all" period is intentionally bounded to the last 6 months. Older data
// is rarely actionable for a cost tracker, and capping the range keeps the
// parse path bounded so providers like Codex/Cursor with sparse multi-year
// history still load in seconds. Users who need an unbounded window can use
// the explicit `lifetime` period or `--from` / `--to`.
const ALL_TIME_MONTHS = 6

export type Period = 'today' | 'week' | '30days' | 'month' | 'all'
export type Period = 'today' | 'week' | '30days' | 'month' | 'all' | 'lifetime'

export const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
export const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all', 'lifetime']

// Short labels suitable for the dashboard tab strip. Long-form labels for
// header text come from `getDateRange().label`.
Expand All @@ -27,9 +27,10 @@ export const PERIOD_LABELS: Record<Period, string> = {
'30days': '30 Days',
month: 'This Month',
all: '6 Months',
lifetime: 'Lifetime',
}

const VALID_PERIODS: ReadonlyArray<Period> = ['today', 'week', '30days', 'month', 'all']
const VALID_PERIODS: ReadonlyArray<Period> = ['today', 'week', '30days', 'month', 'all', 'lifetime']

export class UsageQueryError extends Error {
constructor(message: string) {
Expand Down Expand Up @@ -145,8 +146,8 @@ export function parseDateRangeFlags(from: string | undefined, to: string | undef
* surfaces a few extra inputs not exposed in the dashboard tab strip
* (e.g. `'yesterday'`). Unknown values fall back to `'week'`.
*
* Note: `'all'` is bounded to the last 6 months. Use `--from`/`--to` for
* an unbounded historical window.
* Note: `'all'` is bounded to the last 6 months. Use `'lifetime'` or
* `--from`/`--to` for an unbounded historical window.
*/
export function getDateRange(period: string): { range: DateRange; label: string } {
const now = new Date()
Expand Down Expand Up @@ -185,9 +186,13 @@ export function getDateRange(period: string): { range: DateRange; label: string
const start = new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, 1)
return { range: { start, end }, label: 'Last 6 months' }
}
case 'lifetime': {
const start = new Date(1970, 0, 1)
return { range: { start, end }, label: 'Lifetime' }
}
default: {
process.stderr.write(
`codeburn: unknown period "${period}". Valid values: today, week, 30days, month, all.\n`
`codeburn: unknown period "${period}". Valid values: today, week, 30days, month, all, lifetime.\n`
)
process.exit(1)
}
Expand Down
6 changes: 3 additions & 3 deletions src/daily-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ export function withDailyCacheLock<T>(fn: () => Promise<T>): Promise<T> {

export const MS_PER_DAY = 24 * 60 * 60 * 1000
export const BACKFILL_DAYS = 365
// Keep 2 years of history so the longest UI-exposed period (6 months
// today, with headroom for future longer windows) always reads from
// cache while old entries get pruned.
// Keep 2 years of history so the longest bounded UI period (6 months
// via `all`) and the uncapped `lifetime` period both have headroom to
// read from cache while old entries get pruned.
export const DAILY_CACHE_RETENTION_DAYS = 730

export function toDateString(date: Date): string {
Expand Down
10 changes: 6 additions & 4 deletions src/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const ORANGE = '#FF8C42'
const DIM = '#555555'
const GOLD = '#FFD700'
const PLAN_BAR_WIDTH = 10
const HEAVY_PERIODS = new Set<Period>(['30days', 'month', 'all'])
const HEAVY_PERIODS = new Set<Period>(['30days', 'month', 'all', 'lifetime'])

const LANG_DISPLAY_NAMES: Record<string, string> = {
javascript: 'JavaScript', typescript: 'TypeScript', python: 'Python',
Expand Down Expand Up @@ -760,7 +760,8 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable,
<Text color={ORANGE} bold>2</Text><Text dimColor> week </Text>
<Text color={ORANGE} bold>3</Text><Text dimColor> 30 days </Text>
<Text color={ORANGE} bold>4</Text><Text dimColor> month </Text>
<Text color={ORANGE} bold>5</Text><Text dimColor> 6 months</Text>
<Text color={ORANGE} bold>5</Text><Text dimColor> 6 months </Text>
<Text color={ORANGE} bold>6</Text><Text dimColor> lifetime</Text>
</>
)}
{!customRange && !isOptimize && (
Expand Down Expand Up @@ -806,7 +807,7 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets,
return (
<Box flexDirection="column" width={dashWidth}>
<Overview projects={projects} label={activeLabel} width={dashWidth} planUsages={visiblePlanUsages} />
<Row wide={wide} width={dashWidth}><DailyActivity projects={scrollableDailyHistory ? (dailyHistoryProjects ?? []) : projects} days={days} pw={pw} bw={barWidth} scrollable={scrollableDailyHistory} cursor={dailyHistoryCursor} loading={dailyHistoryLoading} /><ProjectBreakdown projects={projects} pw={pw} bw={barWidth} budgets={budgets} rows={dayMode ? 8 : period === 'all' ? 14 : period === 'month' || period === '30days' ? 14 : 8} /></Row>
<Row wide={wide} width={dashWidth}><DailyActivity projects={scrollableDailyHistory ? (dailyHistoryProjects ?? []) : projects} days={days} pw={pw} bw={barWidth} scrollable={scrollableDailyHistory} cursor={dailyHistoryCursor} loading={dailyHistoryLoading} /><ProjectBreakdown projects={projects} pw={pw} bw={barWidth} budgets={budgets} rows={dayMode ? 8 : period === 'all' || period === 'lifetime' ? 14 : period === 'month' || period === '30days' ? 14 : 8} /></Row>
<Row wide={wide} width={dashWidth}><ActivityBreakdown projects={projects} pw={pw} bw={barWidth} /><ModelBreakdown projects={projects} pw={pw} bw={barWidth} /></Row>
{isCursor ? (
<ToolBreakdown projects={projects} pw={dashWidth} bw={barWidth} title="Languages" filterPrefix="lang:" />
Expand Down Expand Up @@ -1094,7 +1095,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
// Also disable while a custom --from/--to range is in effect. Switching
// period would silently abandon the user's explicit range and reload
// standard period data; the period tab strip is hidden in this mode so
// users have no expectation that 1-5 should do anything.
// users have no expectation that 1-6 should do anything.
if (isCustomRange) return
if (dayDate) {
if (key.leftArrow) { void switchDay(shiftDay(dayDate, -1)); return }
Expand All @@ -1115,6 +1116,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
else if (input === '3') switchPeriodImmediate('30days')
else if (input === '4') switchPeriodImmediate('month')
else if (input === '5') switchPeriodImmediate('all')
else if (input === '6') switchPeriodImmediate('lifetime')
})

const headerLabel = dayDate ? formatDayRangeLabel(dayDate) : customRangeLabel ?? PERIOD_LABELS[period]
Expand Down
24 changes: 12 additions & 12 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ function buildJsonReport(projects: ProjectSummary[], period: string, periodKey:
program
.command('report', { isDefault: true })
.description('Interactive usage dashboard')
.option('-p, --period <period>', 'Starting period: today, week, 30days, month, all', 'week')
.option('-p, --period <period>', 'Starting period: today, week, 30days, month, all, lifetime', 'week')
.option('--day <date>', 'Single day to review (YYYY-MM-DD, today, or yesterday). Overrides --period when set')
.option('--from <date>', 'Start date (YYYY-MM-DD). Overrides --period when set')
.option('--to <date>', 'End date (YYYY-MM-DD). Overrides --period when set')
Expand Down Expand Up @@ -797,7 +797,7 @@ program
.command('devices [action] [target]')
.description('Combined usage across your devices. Actions: scan | add (find nearby & pair) | add <host> --pin <pin> (manual) | rm <name>. Supports --format json for read-only output and scan.')
.option('--pin <pin>', 'Pairing PIN shown on the device you are adding')
.option('-p, --period <period>', 'Period: today, week, 30days, month, all', 'month')
.option('-p, --period <period>', 'Period: today, week, 30days, month, all, lifetime', 'month')
.option('--port <number>', 'Default port when adding a device', parseInteger, 7777)
.option('--format <format>', 'Output format: text, json', 'text')
.action(async (action: string | undefined, target: string | undefined, opts) => {
Expand Down Expand Up @@ -905,7 +905,7 @@ program
program
.command('overview')
.description('Plain-text usage overview, copy-pasteable (defaults to this month)')
.option('-p, --period <period>', 'Period: today, week, 30days, month, all', 'month')
.option('-p, --period <period>', 'Period: today, week, 30days, month, all, lifetime', 'month')
.option('--from <date>', 'Start date (YYYY-MM-DD). Overrides --period when set')
.option('--to <date>', 'End date (YYYY-MM-DD). Overrides --period when set')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, copilot)', 'all')
Expand Down Expand Up @@ -975,7 +975,7 @@ program
program
.command('web')
.description('Open the local web dashboard in your browser')
.option('-p, --period <period>', 'Initial period: today, week, 30days, month, all', 'today')
.option('-p, --period <period>', 'Initial period: today, week, 30days, month, all, lifetime', 'today')
.option('--from <date>', 'Start date (YYYY-MM-DD)')
.option('--to <date>', 'End date (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, copilot)', 'all')
Expand Down Expand Up @@ -1005,7 +1005,7 @@ program
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.option('--period <period>', 'Primary period for menubar-json: today, week, 30days, month, all', 'today')
.option('--period <period>', 'Primary period for menubar-json: today, week, 30days, month, all, lifetime', 'today')
.option('--day <date>', 'Single day for menubar-json (YYYY-MM-DD, today, or yesterday). Overrides --period when set')
.option('--from <date>', 'Start date (YYYY-MM-DD) for custom range')
.option('--to <date>', 'End date (YYYY-MM-DD) for custom range')
Expand Down Expand Up @@ -1723,7 +1723,7 @@ program
program
.command('optimize')
.description('Find token waste and get exact fixes')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from <date>', 'Custom range start (YYYY-MM-DD)')
.option('--to <date>', 'Custom range end (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
Expand Down Expand Up @@ -1806,7 +1806,7 @@ program
program
.command('compare')
.description('Compare two AI models side-by-side')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', 'all')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--model-a <model>', 'First model to compare')
Expand Down Expand Up @@ -1859,7 +1859,7 @@ program
program
.command('audit')
.description("Token audit: raw provider token fields vs codeburn's displayed totals and cost derivation")
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from <date>', 'Custom range start (YYYY-MM-DD)')
.option('--to <date>', 'Custom range end (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
Expand Down Expand Up @@ -1899,7 +1899,7 @@ program
program
.command('models')
.description('Per-model token + cost table, optionally exploded by task type or agent')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from <date>', 'Custom range start (YYYY-MM-DD)')
.option('--to <date>', 'Custom range end (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
Expand Down Expand Up @@ -1962,7 +1962,7 @@ program
program
.command('sessions')
.description('Full per-session usage report')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from <date>', 'Custom range start (YYYY-MM-DD)')
.option('--to <date>', 'Custom range end (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
Expand Down Expand Up @@ -1993,7 +1993,7 @@ program
program
.command('yield')
.description('Track which AI spend shipped to main vs reverted/abandoned (experimental)')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', 'week')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', 'week')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
.option('--format <format>', 'Output format: text, json', 'text')
.action(async (opts) => {
Expand All @@ -2016,7 +2016,7 @@ program
program
.command('spend')
.description('Emit model x project spend flow data')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from <date>', 'Custom range start (YYYY-MM-DD)')
.option('--to <date>', 'Custom range end (YYYY-MM-DD)')
.option('--provider <provider>', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
Expand Down
21 changes: 18 additions & 3 deletions tests/cli-date.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ describe('getDateRange', () => {
expect(range.start.getDate()).toBe(1)
})

it('"lifetime" starts at local epoch and stays open-ended through today', () => {
const { range, label } = getDateRange('lifetime')

expect(label).toBe('Lifetime')
expect(range.start.getFullYear()).toBe(1970)
expect(range.start.getMonth()).toBe(0)
expect(range.start.getDate()).toBe(1)
expect(range.end.getHours()).toBe(23)
expect(range.end.getMinutes()).toBe(59)
})

it('"week" returns the last 7 days', () => {
const { range, label } = getDateRange('week')
expect(label).toBe('Last 7 Days')
Expand Down Expand Up @@ -90,7 +101,7 @@ describe('getDateRange', () => {

describe('PERIODS / PERIOD_LABELS', () => {
it('exposes the expected period set', () => {
expect(PERIODS).toEqual(['today', 'week', '30days', 'month', 'all'])
expect(PERIODS).toEqual(['today', 'week', '30days', 'month', 'all', 'lifetime'])
})

it('has a label for every period', () => {
Expand All @@ -104,11 +115,15 @@ describe('PERIODS / PERIOD_LABELS', () => {
// ("Last 6 months") comes from getDateRange().label.
expect(PERIOD_LABELS.all).toBe('6 Months')
})

it('"lifetime" tab label is explicit about the unbounded range', () => {
expect(PERIOD_LABELS.lifetime).toBe('Lifetime')
})
})

describe('parsePeriodOrThrow', () => {
it('round-trips known periods', () => {
const known: Period[] = ['today', 'week', '30days', 'month', 'all']
const known: Period[] = ['today', 'week', '30days', 'month', 'all', 'lifetime']
for (const p of known) {
expect(parsePeriodOrThrow(p)).toBe(p)
}
Expand Down Expand Up @@ -144,7 +159,7 @@ describe('periodInfoFromQuery', () => {

describe('toPeriod', () => {
it('round-trips known periods', () => {
const known: Period[] = ['today', 'week', '30days', 'month', 'all']
const known: Period[] = ['today', 'week', '30days', 'month', 'all', 'lifetime']
for (const p of known) {
expect(toPeriod(p)).toBe(p)
}
Expand Down
Loading
Loading