diff --git a/README.md b/README.md
index ee71e3ba..4c72c36f 100644
--- a/README.md
+++ b/README.md
@@ -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
```
@@ -386,7 +387,7 @@ Adding a new provider is a single file. See `src/providers/codex.ts` for an exam
All commands and keyboard shortcuts
-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**
@@ -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.
@@ -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
diff --git a/src/cli-date.ts b/src/cli-date.ts
index d1daceb6..3b4b5fff 100644
--- a/src/cli-date.ts
+++ b/src/cli-date.ts
@@ -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`.
@@ -27,9 +27,10 @@ export const PERIOD_LABELS: Record = {
'30days': '30 Days',
month: 'This Month',
all: '6 Months',
+ lifetime: 'Lifetime',
}
-const VALID_PERIODS: ReadonlyArray = ['today', 'week', '30days', 'month', 'all']
+const VALID_PERIODS: ReadonlyArray = ['today', 'week', '30days', 'month', 'all', 'lifetime']
export class UsageQueryError extends Error {
constructor(message: string) {
@@ -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()
@@ -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)
}
diff --git a/src/daily-cache.ts b/src/daily-cache.ts
index 502bb1ed..f8543d98 100644
--- a/src/daily-cache.ts
+++ b/src/daily-cache.ts
@@ -263,9 +263,9 @@ export function withDailyCacheLock(fn: () => Promise): Promise {
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 {
diff --git a/src/dashboard.tsx b/src/dashboard.tsx
index 9f3cd6d9..c0c80624 100644
--- a/src/dashboard.tsx
+++ b/src/dashboard.tsx
@@ -49,7 +49,7 @@ const ORANGE = '#FF8C42'
const DIM = '#555555'
const GOLD = '#FFD700'
const PLAN_BAR_WIDTH = 10
-const HEAVY_PERIODS = new Set(['30days', 'month', 'all'])
+const HEAVY_PERIODS = new Set(['30days', 'month', 'all', 'lifetime'])
const LANG_DISPLAY_NAMES: Record = {
javascript: 'JavaScript', typescript: 'TypeScript', python: 'Python',
@@ -760,7 +760,8 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable,
2 week
3 30 days
4 month
- 5 6 months
+ 5 6 months
+ 6 lifetime
>
)}
{!customRange && !isOptimize && (
@@ -806,7 +807,7 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets,
return (
-
+
{isCursor ? (
@@ -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 }
@@ -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]
diff --git a/src/main.ts b/src/main.ts
index 6cccdc7e..c30c5fa2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -715,7 +715,7 @@ function buildJsonReport(projects: ProjectSummary[], period: string, periodKey:
program
.command('report', { isDefault: true })
.description('Interactive usage dashboard')
- .option('-p, --period ', 'Starting period: today, week, 30days, month, all', 'week')
+ .option('-p, --period ', 'Starting period: today, week, 30days, month, all, lifetime', 'week')
.option('--day ', 'Single day to review (YYYY-MM-DD, today, or yesterday). Overrides --period when set')
.option('--from ', 'Start date (YYYY-MM-DD). Overrides --period when set')
.option('--to ', 'End date (YYYY-MM-DD). Overrides --period when set')
@@ -797,7 +797,7 @@ program
.command('devices [action] [target]')
.description('Combined usage across your devices. Actions: scan | add (find nearby & pair) | add --pin (manual) | rm . Supports --format json for read-only output and scan.')
.option('--pin ', 'Pairing PIN shown on the device you are adding')
- .option('-p, --period ', 'Period: today, week, 30days, month, all', 'month')
+ .option('-p, --period ', 'Period: today, week, 30days, month, all, lifetime', 'month')
.option('--port ', 'Default port when adding a device', parseInteger, 7777)
.option('--format ', 'Output format: text, json', 'text')
.action(async (action: string | undefined, target: string | undefined, opts) => {
@@ -905,7 +905,7 @@ program
program
.command('overview')
.description('Plain-text usage overview, copy-pasteable (defaults to this month)')
- .option('-p, --period ', 'Period: today, week, 30days, month, all', 'month')
+ .option('-p, --period ', 'Period: today, week, 30days, month, all, lifetime', 'month')
.option('--from ', 'Start date (YYYY-MM-DD). Overrides --period when set')
.option('--to ', 'End date (YYYY-MM-DD). Overrides --period when set')
.option('--provider ', 'Filter by provider (e.g. claude, codex, copilot)', 'all')
@@ -975,7 +975,7 @@ program
program
.command('web')
.description('Open the local web dashboard in your browser')
- .option('-p, --period ', 'Initial period: today, week, 30days, month, all', 'today')
+ .option('-p, --period ', 'Initial period: today, week, 30days, month, all, lifetime', 'today')
.option('--from ', 'Start date (YYYY-MM-DD)')
.option('--to ', 'End date (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, codex, copilot)', 'all')
@@ -1005,7 +1005,7 @@ program
.option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--project ', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .option('--period ', 'Primary period for menubar-json: today, week, 30days, month, all', 'today')
+ .option('--period ', 'Primary period for menubar-json: today, week, 30days, month, all, lifetime', 'today')
.option('--day ', 'Single day for menubar-json (YYYY-MM-DD, today, or yesterday). Overrides --period when set')
.option('--from ', 'Start date (YYYY-MM-DD) for custom range')
.option('--to ', 'End date (YYYY-MM-DD) for custom range')
@@ -1723,7 +1723,7 @@ program
program
.command('optimize')
.description('Find token waste and get exact fixes')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from ', 'Custom range start (YYYY-MM-DD)')
.option('--to ', 'Custom range end (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
@@ -1806,7 +1806,7 @@ program
program
.command('compare')
.description('Compare two AI models side-by-side')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', 'all')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', 'all')
.option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format ', 'Output format: tui, json', 'tui')
.option('--model-a ', 'First model to compare')
@@ -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 ', 'Analysis period: today, week, 30days, month, all', '30days')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from ', 'Custom range start (YYYY-MM-DD)')
.option('--to ', 'Custom range end (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
@@ -1899,7 +1899,7 @@ program
program
.command('models')
.description('Per-model token + cost table, optionally exploded by task type or agent')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from ', 'Custom range start (YYYY-MM-DD)')
.option('--to ', 'Custom range end (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
@@ -1962,7 +1962,7 @@ program
program
.command('sessions')
.description('Full per-session usage report')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from ', 'Custom range start (YYYY-MM-DD)')
.option('--to ', 'Custom range end (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
@@ -1993,7 +1993,7 @@ program
program
.command('yield')
.description('Track which AI spend shipped to main vs reverted/abandoned (experimental)')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', 'week')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', 'week')
.option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
.option('--format ', 'Output format: text, json', 'text')
.action(async (opts) => {
@@ -2016,7 +2016,7 @@ program
program
.command('spend')
.description('Emit model x project spend flow data')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days')
+ .option('-p, --period ', 'Analysis period: today, week, 30days, month, all, lifetime', '30days')
.option('--from ', 'Custom range start (YYYY-MM-DD)')
.option('--to ', 'Custom range end (YYYY-MM-DD)')
.option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all')
diff --git a/tests/cli-date.test.ts b/tests/cli-date.test.ts
index f553c70b..fc496a4c 100644
--- a/tests/cli-date.test.ts
+++ b/tests/cli-date.test.ts
@@ -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')
@@ -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', () => {
@@ -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)
}
@@ -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)
}
diff --git a/tests/cli-json-daily.test.ts b/tests/cli-json-daily.test.ts
index 34ad500f..193ee90d 100644
--- a/tests/cli-json-daily.test.ts
+++ b/tests/cli-json-daily.test.ts
@@ -234,4 +234,51 @@ describe('codeburn report --format json daily[] one-shot fields (issue #279)', (
await rm(home, { recursive: true, force: true })
}
})
+
+ it('includes older sessions under --period lifetime but not under --period all', async () => {
+ const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-json-lifetime-'))
+
+ try {
+ const projectDir = join(home, '.claude', 'projects', 'app')
+ await mkdir(projectDir, { recursive: true })
+
+ await writeFile(
+ join(projectDir, 'history.jsonl'),
+ [
+ userLine('old', '2025-10-01T09:00:00Z'),
+ assistantEditLine('old', '2025-10-01T09:01:00Z', 'm-old'),
+ userLine('recent', '2026-06-01T09:00:00Z'),
+ assistantEditLine('recent', '2026-06-01T09:01:00Z', 'm-recent'),
+ ].join('\n'),
+ )
+
+ const allResult = runCli([
+ '--format', 'json',
+ '--period', 'all',
+ '--provider', 'claude',
+ ], home)
+ expect(allResult.status).toBe(0)
+ const allReport = JSON.parse(allResult.stdout) as {
+ daily: Array<{ date: string; calls: number }>
+ projects: Array<{ calls: number }>
+ }
+ expect(allReport.daily.map(d => d.date)).toEqual(['2026-06-01'])
+ expect(allReport.projects[0]?.calls).toBe(1)
+
+ const lifetimeResult = runCli([
+ '--format', 'json',
+ '--period', 'lifetime',
+ '--provider', 'claude',
+ ], home)
+ expect(lifetimeResult.status).toBe(0)
+ const lifetimeReport = JSON.parse(lifetimeResult.stdout) as {
+ daily: Array<{ date: string; calls: number }>
+ projects: Array<{ calls: number }>
+ }
+ expect(lifetimeReport.daily.map(d => d.date)).toEqual(['2025-10-01', '2026-06-01'])
+ expect(lifetimeReport.projects[0]?.calls).toBe(2)
+ } finally {
+ await rm(home, { recursive: true, force: true })
+ }
+ })
})