From 67c504a60ab53dcd436a82c96a9e1d7425468f45 Mon Sep 17 00:00:00 2001 From: Travis Haley Date: Thu, 16 Apr 2026 09:39:58 -0600 Subject: [PATCH] feat: add --project and --exclude filters for project-level filtering Adds two new repeatable flags to all commands (report, today, month, status, export): - --project : include only projects matching name (substring, case-insensitive) - --exclude : 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) --- README.md | 14 ++++++++++++ src/cli.ts | 55 +++++++++++++++++++++++++++++++---------------- src/dashboard.tsx | 16 ++++++++------ src/parser.ts | 25 +++++++++++++++++++++ 4 files changed, 84 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2a4f6e9..246aeec 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/cli.ts b/src/cli.ts index b47229f..79844f5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 { +function collect(val: string, acc: string[]): string[] { + acc.push(val) + return acc +} + +async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise { 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 ', 'Starting period: today, week, 30days, month, all', 'week') .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') .option('--format ', 'Output format: tui, json', 'tui') + .option('--project ', 'Show only projects matching name (repeatable)', collect, []) + .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', '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 ', 'Output format: terminal, menubar, json', 'terminal') .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--project ', 'Show only projects matching name (repeatable)', collect, []) + .option('--exclude ', '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 ', 'Filter by provider: all, claude, codex, cursor', 'all') .option('--format ', 'Output format: tui, json', 'tui') + .option('--project ', 'Show only projects matching name (repeatable)', collect, []) + .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', '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 ', 'Filter by provider: all, claude, codex, cursor', 'all') .option('--format ', 'Output format: tui, json', 'tui') + .option('--project ', 'Show only projects matching name (repeatable)', collect, []) + .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--refresh ', '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 ', 'Export format: csv, json', 'csv') .option('-o, --output ', 'Output file path') .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--project ', 'Show only projects matching name (repeatable)', collect, []) + .option('--exclude ', '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)) { diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 55c055b..6af5d3a 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -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(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 { +export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[]): Promise { 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( - + ) await waitUntilExit() } else { diff --git a/src/parser.ts b/src/parser.ts index 4055fea..429cb45 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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 { const key = cacheKey(dateRange, providerFilter) const cached = sessionCache.get(key)