feat: add --project and --exclude filters for project-level filtering

Adds two new repeatable flags to all commands (report, today, month, status, export):
- --project <name>: include only projects matching name (substring, case-insensitive)
- --exclude <name>: exclude projects matching name (substring, case-insensitive)

Both flags can be specified multiple times to match multiple projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Travis Haley 2026-04-16 09:39:58 -06:00
parent 4154413e59
commit 67c504a60a
4 changed files with 84 additions and 26 deletions

View file

@ -97,6 +97,20 @@ codeburn export --provider claude # export Claude data only
The `--provider` flag works on all commands: `report`, `today`, `month`, `status`, `export`.
### Project filtering
Filter results by project name (case-insensitive substring match). Both flags are repeatable:
```bash
codeburn report --project myapp # show only projects matching "myapp"
codeburn report --exclude myapp # show everything except "myapp"
codeburn report --exclude myapp --exclude tests # exclude multiple projects
codeburn month --project api --project web # include multiple projects
codeburn export --project inventory # export only "inventory" project data
```
The `--project` and `--exclude` flags work on all commands and can be combined with `--provider`.
### Supported providers
| Provider | Data location | Status |

View file

@ -1,7 +1,7 @@
import { Command } from 'commander'
import { exportCsv, exportJson, type PeriodExport } from './export.js'
import { loadPricing } from './models.js'
import { parseAllSessions } from './parser.js'
import { parseAllSessions, filterProjectsByName } from './parser.js'
import { convertCost } from './currency.js'
import { renderStatusBar } from './format.js'
import { installMenubar, renderMenubarFormat, type PeriodData, type ProviderCost, uninstallMenubar } from './menubar.js'
@ -61,10 +61,15 @@ function toPeriod(s: string): Period {
return 'week'
}
async function runJsonReport(period: Period, provider: string): Promise<void> {
function collect(val: string, acc: string[]): string[] {
acc.push(val)
return acc
}
async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise<void> {
await loadPricing()
const { range, label } = getDateRange(period)
const projects = await parseAllSessions(range, provider)
const projects = filterProjectsByName(await parseAllSessions(range, provider), project, exclude)
console.log(JSON.stringify(buildJsonReport(projects, label, period), null, 2))
}
@ -211,14 +216,16 @@ program
.option('-p, --period <period>', 'Starting period: today, week, 30days, month, all', 'week')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.option('--refresh <seconds>', 'Auto-refresh interval in seconds', parseInt)
.action(async (opts) => {
const period = toPeriod(opts.period)
if (opts.format === 'json') {
await runJsonReport(period, opts.provider)
await runJsonReport(period, opts.provider, opts.project, opts.exclude)
return
}
await renderDashboard(period, opts.provider, opts.refresh)
await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude)
})
function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData {
@ -265,15 +272,18 @@ program
.description('Compact status output (today + week + month)')
.option('--format <format>', 'Output format: terminal, menubar, json', 'terminal')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.action(async (opts) => {
await loadPricing()
const pf = opts.provider
const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
if (opts.format === 'menubar') {
const todayRange = getDateRange('today').range
const todayData = buildPeriodData('Today', await parseAllSessions(todayRange, pf))
const weekData = buildPeriodData('7 Days', await parseAllSessions(getDateRange('week').range, pf))
const thirtyDayData = buildPeriodData('30 Days', await parseAllSessions(getDateRange('30days').range, pf))
const monthData = buildPeriodData('Month', await parseAllSessions(getDateRange('month').range, pf))
const todayData = buildPeriodData('Today', fp(await parseAllSessions(todayRange, pf)))
const weekData = buildPeriodData('7 Days', fp(await parseAllSessions(getDateRange('week').range, pf)))
const thirtyDayData = buildPeriodData('30 Days', fp(await parseAllSessions(getDateRange('30days').range, pf)))
const monthData = buildPeriodData('Month', fp(await parseAllSessions(getDateRange('month').range, pf)))
const todayProviders: ProviderCost[] = []
for (const p of await getAllProviders()) {
const data = await parseAllSessions(todayRange, p.name)
@ -285,8 +295,8 @@ program
}
if (opts.format === 'json') {
const todayData = buildPeriodData('today', await parseAllSessions(getDateRange('today').range, pf))
const monthData = buildPeriodData('month', await parseAllSessions(getDateRange('month').range, pf))
const todayData = buildPeriodData('today', fp(await parseAllSessions(getDateRange('today').range, pf)))
const monthData = buildPeriodData('month', fp(await parseAllSessions(getDateRange('month').range, pf)))
const { code, rate } = getCurrency()
console.log(JSON.stringify({
currency: code,
@ -296,7 +306,7 @@ program
return
}
const monthProjects = await parseAllSessions(getDateRange('month').range, pf)
const monthProjects = fp(await parseAllSessions(getDateRange('month').range, pf))
console.log(renderStatusBar(monthProjects))
})
@ -305,13 +315,15 @@ program
.description('Today\'s usage dashboard')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.option('--refresh <seconds>', 'Auto-refresh interval in seconds', parseInt)
.action(async (opts) => {
if (opts.format === 'json') {
await runJsonReport('today', opts.provider)
await runJsonReport('today', opts.provider, opts.project, opts.exclude)
return
}
await renderDashboard('today', opts.provider, opts.refresh)
await renderDashboard('today', opts.provider, opts.refresh, opts.project, opts.exclude)
})
program
@ -319,13 +331,15 @@ program
.description('This month\'s usage dashboard')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.option('--refresh <seconds>', 'Auto-refresh interval in seconds', parseInt)
.action(async (opts) => {
if (opts.format === 'json') {
await runJsonReport('month', opts.provider)
await runJsonReport('month', opts.provider, opts.project, opts.exclude)
return
}
await renderDashboard('month', opts.provider, opts.refresh)
await renderDashboard('month', opts.provider, opts.refresh, opts.project, opts.exclude)
})
program
@ -334,13 +348,16 @@ program
.option('-f, --format <format>', 'Export format: csv, json', 'csv')
.option('-o, --output <path>', 'Output file path')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.action(async (opts) => {
await loadPricing()
const pf = opts.provider
const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
const periods: PeriodExport[] = [
{ label: 'Today', projects: await parseAllSessions(getDateRange('today').range, pf) },
{ label: '7 Days', projects: await parseAllSessions(getDateRange('week').range, pf) },
{ label: '30 Days', projects: await parseAllSessions(getDateRange('30days').range, pf) },
{ label: 'Today', projects: fp(await parseAllSessions(getDateRange('today').range, pf)) },
{ label: '7 Days', projects: fp(await parseAllSessions(getDateRange('week').range, pf)) },
{ label: '30 Days', projects: fp(await parseAllSessions(getDateRange('30days').range, pf)) },
]
if (periods.every(p => p.projects.length === 0)) {

View file

@ -4,7 +4,7 @@ import React, { useState, useCallback, useEffect } from 'react'
import { render, Box, Text, useInput, useApp, useWindowSize } from 'ink'
import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js'
import { formatCost, formatTokens } from './format.js'
import { parseAllSessions } from './parser.js'
import { parseAllSessions, filterProjectsByName } from './parser.js'
import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
@ -593,11 +593,13 @@ function DashboardContent({ projects, period, columns, activeProvider }: { proje
)
}
function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, refreshSeconds }: {
function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, refreshSeconds, projectFilter, excludeFilter }: {
initialProjects: ProjectSummary[]
initialPeriod: Period
initialProvider: string
refreshSeconds?: number
projectFilter?: string[]
excludeFilter?: string[]
}) {
const { exit } = useApp()
const [period, setPeriod] = useState<Period>(initialPeriod)
@ -629,10 +631,10 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const reloadData = useCallback(async (p: Period, prov: string) => {
setLoading(true)
const range = getDateRange(p)
const data = await parseAllSessions(range, prov)
const data = filterProjectsByName(await parseAllSessions(range, prov), projectFilter, excludeFilter)
setProjects(data)
setLoading(false)
}, [])
}, [projectFilter, excludeFilter])
useEffect(() => {
if (!refreshSeconds || refreshSeconds <= 0) return
@ -718,16 +720,16 @@ function StaticDashboard({ projects, period, activeProvider }: { projects: Proje
)
}
export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number): Promise<void> {
export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[]): Promise<void> {
await loadPricing()
const range = getDateRange(period)
const projects = await parseAllSessions(range, provider)
const projects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
const isTTY = process.stdin.isTTY && process.stdout.isTTY
if (isTTY) {
const { waitUntilExit } = render(
<InteractiveDashboard initialProjects={projects} initialPeriod={period} initialProvider={provider} refreshSeconds={refreshSeconds} />
<InteractiveDashboard initialProjects={projects} initialPeriod={period} initialProvider={provider} refreshSeconds={refreshSeconds} projectFilter={projectFilter} excludeFilter={excludeFilter} />
)
await waitUntilExit()
} else {

View file

@ -462,6 +462,31 @@ function cachePut(key: string, data: ProjectSummary[]) {
sessionCache.set(key, { data, ts: now })
}
export function filterProjectsByName(
projects: ProjectSummary[],
include?: string[],
exclude?: string[],
): ProjectSummary[] {
let result = projects
if (include && include.length > 0) {
const patterns = include.map(s => s.toLowerCase())
result = result.filter(p => {
const name = p.project.toLowerCase()
const path = p.projectPath.toLowerCase()
return patterns.some(pat => name.includes(pat) || path.includes(pat))
})
}
if (exclude && exclude.length > 0) {
const patterns = exclude.map(s => s.toLowerCase())
result = result.filter(p => {
const name = p.project.toLowerCase()
const path = p.projectPath.toLowerCase()
return !patterns.some(pat => name.includes(pat) || path.includes(pat))
})
}
return result
}
export async function parseAllSessions(dateRange?: DateRange, providerFilter?: string): Promise<ProjectSummary[]> {
const key = cacheKey(dateRange, providerFilter)
const cached = sessionCache.get(key)