Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ codeburn optimize # scan the last 30 days
codeburn optimize -p today # today only
codeburn optimize -p week # last 7 days
codeburn optimize --provider claude # restrict to one provider
codeburn optimize --format json # setup health + findings as JSON
```

Scans your sessions and your `~/.claude/` setup for waste patterns:
Expand Down Expand Up @@ -255,6 +256,7 @@ codeburn yield # last 7 days (default)
codeburn yield -p today # today only
codeburn yield -p 30days # last 30 days
codeburn yield -p month # this calendar month
codeburn yield --format json # productive/reverted/abandoned spend as JSON
```

Correlates AI sessions with git commits by timestamp:
Expand Down Expand Up @@ -345,7 +347,7 @@ codeburn report --format json | jq '.projects'
codeburn today --format json | jq '.overview.cost'
```

For lighter output, use `status --format json` (today and month totals only) or file exports (`export -f json`).
For lighter output, use `status --format json` (today and month totals only), `optimize --format json` (setup health, findings, and copy-paste fixes), `yield --format json` (productive/reverted/abandoned spend), or file exports (`export -f json`).

## Menu Bar

Expand Down
16 changes: 13 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,11 +1033,13 @@ program
.description('Find token waste and get exact fixes')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', '30days')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format <format>', 'Output format: text, json', 'text')
.action(async (opts) => {
assertFormat(opts.format, ['text', 'json'], 'optimize')
await loadPricing()
const { range, label } = getDateRange(opts.period)
const projects = await parseAllSessions(range, opts.provider)
await runOptimize(projects, label, range)
await runOptimize(projects, label, range, { format: opts.format })
})

program
Expand Down Expand Up @@ -1111,12 +1113,20 @@ 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('--format <format>', 'Output format: text, json', 'text')
.action(async (opts) => {
const { computeYield, formatYieldSummary } = await import('./yield.js')
assertFormat(opts.format, ['text', 'json'], 'yield')
const { computeYield, formatYieldSummary, buildYieldJsonReport } = await import('./yield.js')
await loadPricing()
const { range, label } = getDateRange(opts.period)
console.log(`\n Analyzing yield for ${label}...\n`)
if (opts.format !== 'json') {
console.log(`\n Analyzing yield for ${label}...\n`)
}
const summary = await computeYield(range, process.cwd())
if (opts.format === 'json') {
console.log(JSON.stringify(buildYieldJsonReport(summary, label, range), null, 2))
return
}
console.log(formatYieldSummary(summary))
})

Expand Down
90 changes: 87 additions & 3 deletions src/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,35 @@ export type OptimizeResult = {
healthGrade: HealthGrade
}

export type OptimizeJsonReport = {
period: {
label: string
start: string | null
end: string | null
}
summary: {
healthScore: number
healthGrade: HealthGrade
findingCount: number
periodCostUSD: number
sessions: number
calls: number
potentialSavingsTokens: number
potentialSavingsCostUSD: number
potentialSavingsPercent: number | null
costRateUSD: number
}
findings: Array<{
title: string
explanation: string
severity: Impact
trend: Trend | null
tokensSaved: number
estimatedSavingsUSD: number
fix: WasteAction
}>
}

export type ToolCall = {
name: string
input: Record<string, unknown>
Expand Down Expand Up @@ -2476,19 +2505,74 @@ export async function runOptimize(
projects: ProjectSummary[],
periodLabel: string,
dateRange?: DateRange,
opts: { format?: 'text' | 'json' } = {},
): Promise<void> {
if (projects.length === 0) {
const format = opts.format ?? 'text'
if (projects.length === 0 && format === 'text') {
console.log(chalk.dim('\n No usage data found for this period.\n'))
return
}

process.stderr.write(chalk.dim(' Analyzing your sessions...\n'))
if (format === 'text') {
process.stderr.write(chalk.dim(' Analyzing your sessions...\n'))
}

const { findings, costRate, healthScore, healthGrade } = await scanAndDetect(projects, dateRange)
const result = await scanAndDetect(projects, dateRange)
const { findings, costRate, healthScore, healthGrade } = result
const sessions = projects.flatMap(p => p.sessions)
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0)
const callCount = projects.reduce((s, p) => s + p.totalApiCalls, 0)

if (format === 'json') {
console.log(JSON.stringify(buildOptimizeJsonReport(projects, periodLabel, result, dateRange), null, 2))
return
}

const output = renderOptimize(findings, costRate, periodLabel, periodCost, sessions.length, callCount, healthScore, healthGrade)
console.log(output)
}

export function buildOptimizeJsonReport(
projects: ProjectSummary[],
periodLabel: string,
result: OptimizeResult,
dateRange?: DateRange,
): OptimizeJsonReport {
const sessions = projects.flatMap(p => p.sessions)
const periodCostUSD = projects.reduce((s, p) => s + p.totalCostUSD, 0)
const calls = projects.reduce((s, p) => s + p.totalApiCalls, 0)
const potentialSavingsTokens = result.findings.reduce((s, f) => s + f.tokensSaved, 0)
const potentialSavingsCostUSD = potentialSavingsTokens * result.costRate
const potentialSavingsPercent = periodCostUSD > 0
? Math.round((potentialSavingsCostUSD / periodCostUSD) * 1000) / 10
: null

return {
period: {
label: periodLabel,
start: dateRange?.start.toISOString() ?? null,
end: dateRange?.end.toISOString() ?? null,
},
summary: {
healthScore: result.healthScore,
healthGrade: result.healthGrade,
findingCount: result.findings.length,
periodCostUSD,
sessions: sessions.length,
calls,
potentialSavingsTokens,
potentialSavingsCostUSD,
potentialSavingsPercent,
costRateUSD: result.costRate,
},
findings: result.findings.map(f => ({
title: f.title,
explanation: f.explanation,
severity: f.impact,
trend: f.trend ?? null,
tokensSaved: f.tokensSaved,
estimatedSavingsUSD: f.tokensSaved * result.costRate,
fix: f.fix,
})),
}
}
71 changes: 71 additions & 0 deletions src/yield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ export type YieldSummary = {
details: SessionYield[]
}

export type YieldJsonReport = {
period: {
label: string
start: string
end: string
}
summary: {
productive: YieldBucketJson
reverted: YieldBucketJson
abandoned: YieldBucketJson
total: { costUSD: number; sessions: number }
productiveToRevertedCostRatio: number | null
}
details: SessionYieldJson[]
}

type YieldBucketJson = {
costUSD: number
sessions: number
costPercent: number
sessionPercent: number
}

type SessionYieldJson = Omit<SessionYield, 'cost'> & {
costUSD: number
}

const SAFE_REF_PATTERN = /^[A-Za-z0-9._/\-]+$/

function runGit(args: string[], cwd: string): string | null {
Expand Down Expand Up @@ -216,3 +243,47 @@ export function formatYieldSummary(summary: YieldSummary): string {

return lines.join('\n')
}

export function buildYieldJsonReport(
summary: YieldSummary,
periodLabel: string,
range: DateRange,
): YieldJsonReport {
const bucket = (value: { cost: number; sessions: number }): YieldBucketJson => ({
costUSD: value.cost,
sessions: value.sessions,
costPercent: summary.total.cost > 0
? Math.round((value.cost / summary.total.cost) * 1000) / 10
: 0,
sessionPercent: summary.total.sessions > 0
? Math.round((value.sessions / summary.total.sessions) * 1000) / 10
: 0,
})

return {
period: {
label: periodLabel,
start: range.start.toISOString(),
end: range.end.toISOString(),
},
summary: {
productive: bucket(summary.productive),
reverted: bucket(summary.reverted),
abandoned: bucket(summary.abandoned),
total: {
costUSD: summary.total.cost,
sessions: summary.total.sessions,
},
productiveToRevertedCostRatio: summary.reverted.cost > 0
? Math.round((summary.productive.cost / summary.reverted.cost) * 100) / 100
: null,
},
details: summary.details.map(detail => ({
sessionId: detail.sessionId,
project: detail.project,
costUSD: detail.cost,
category: detail.category,
commitCount: detail.commitCount,
})),
}
}
67 changes: 67 additions & 0 deletions tests/optimize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
detectSessionOutliers,
computeHealth,
computeTrend,
buildOptimizeJsonReport,
type ToolCall,
type ApiCallMeta,
type WasteFinding,
type OptimizeResult,
} from '../src/optimize.js'
import type { ProjectSummary } from '../src/types.js'

Expand Down Expand Up @@ -1115,3 +1117,68 @@ describe('paste-fix destination tagging (issue #277)', () => {
if (finding) checkAllPasteFixesHaveDestination([finding])
})
})

describe('buildOptimizeJsonReport', () => {
it('serializes setup health, savings, and fix details for integrations', () => {
const result: OptimizeResult = {
costRate: 0.00002,
healthScore: 72,
healthGrade: 'C',
findings: [
{
title: 'Trim stale context',
explanation: 'Old instructions are loaded every turn.',
impact: 'medium',
tokensSaved: 50_000,
trend: 'active',
fix: {
type: 'paste',
label: 'Add guardrail',
text: 'Prefer short context.',
destination: 'claude-md',
},
},
],
}
const range = {
start: new Date('2026-05-01T00:00:00.000Z'),
end: new Date('2026-05-08T00:00:00.000Z'),
}

const report = buildOptimizeJsonReport(
[projectWithSessions([3, 2])],
'7 Days',
result,
range,
)

expect(report.period).toEqual({
label: '7 Days',
start: '2026-05-01T00:00:00.000Z',
end: '2026-05-08T00:00:00.000Z',
})
expect(report.summary).toMatchObject({
healthScore: 72,
healthGrade: 'C',
findingCount: 1,
periodCostUSD: 5,
sessions: 2,
calls: 2,
potentialSavingsTokens: 50_000,
potentialSavingsCostUSD: 1,
potentialSavingsPercent: 20,
costRateUSD: 0.00002,
})
expect(report.findings[0]).toMatchObject({
title: 'Trim stale context',
severity: 'medium',
trend: 'active',
tokensSaved: 50_000,
estimatedSavingsUSD: 1,
fix: {
type: 'paste',
destination: 'claude-md',
},
})
})
})
58 changes: 58 additions & 0 deletions tests/yield.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'

import { buildYieldJsonReport, type YieldSummary } from '../src/yield.js'

describe('buildYieldJsonReport', () => {
it('serializes yield buckets, ratios, and session details', () => {
const summary: YieldSummary = {
productive: { cost: 8, sessions: 2 },
reverted: { cost: 2, sessions: 1 },
abandoned: { cost: 10, sessions: 1 },
total: { cost: 20, sessions: 4 },
details: [
{ sessionId: 's1', project: 'app', cost: 8, category: 'productive', commitCount: 2 },
{ sessionId: 's2', project: 'app', cost: 2, category: 'reverted', commitCount: 1 },
],
}
const report = buildYieldJsonReport(summary, '30 Days', {
start: new Date('2026-05-01T00:00:00.000Z'),
end: new Date('2026-05-31T23:59:59.999Z'),
})

expect(report.period).toEqual({
label: '30 Days',
start: '2026-05-01T00:00:00.000Z',
end: '2026-05-31T23:59:59.999Z',
})
expect(report.summary.productive).toEqual({
costUSD: 8,
sessions: 2,
costPercent: 40,
sessionPercent: 50,
})
expect(report.summary.reverted.costPercent).toBe(10)
expect(report.summary.abandoned.sessionPercent).toBe(25)
expect(report.summary.total).toEqual({ costUSD: 20, sessions: 4 })
expect(report.summary.productiveToRevertedCostRatio).toBe(4)
expect(report.details).toHaveLength(2)
expect(report.details[0]).toMatchObject({ sessionId: 's1', costUSD: 8, category: 'productive' })
expect(report.details[0]).not.toHaveProperty('cost')
})

it('uses null ratio when no spend was reverted', () => {
const summary: YieldSummary = {
productive: { cost: 1, sessions: 1 },
reverted: { cost: 0, sessions: 0 },
abandoned: { cost: 0, sessions: 0 },
total: { cost: 1, sessions: 1 },
details: [],
}

const report = buildYieldJsonReport(summary, 'Today', {
start: new Date('2026-06-14T00:00:00.000Z'),
end: new Date('2026-06-14T23:59:59.999Z'),
})

expect(report.summary.productiveToRevertedCostRatio).toBeNull()
})
})