Merge pull request #60 from AgentSeal/feat/optimize

feat: codeburn optimize -- find waste and get copy-paste fixes
This commit is contained in:
AgentSeal 2026-04-17 01:31:52 +02:00 committed by GitHub
commit f2d1753d3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2331 additions and 173 deletions

View file

@ -1,5 +1,40 @@
# Changelog
## 0.7.0 - 2026-04-16
### Added
- **`codeburn optimize` command.** Scans your sessions and your `~/.claude/`
setup for 11 common waste patterns and hands back exact copy-paste fixes.
Detection-only, never writes to user files. Supports `--period` (today,
week, 30days, month, all) and `--provider` (all, claude, codex, cursor).
- **Setup health grade (A-F).** Urgency-weighted rollup of all findings, with
impact scored against observed waste so the most expensive issues rank
first. High findings penalise more, medium less, low least.
- **Trend tracking.** Repeat runs classify each finding as new, improving,
or resolved against a 48-hour recent window, so fixed issues disappear
instead of lingering as noise.
- **11 detectors:** files Claude re-reads across sessions, low Read:Edit
ratio, projects missing `.claudeignore`, uncapped `BASH_MAX_OUTPUT_LENGTH`,
unused MCP servers, ghost agents, ghost skills, ghost slash commands,
bloated `CLAUDE.md` files (with `@-import` expansion counted), cache
creation overhead, and junk directory reads.
- **Copy-paste fixes.** Each finding comes with a ready-to-paste remedy: a
`CLAUDE.md` line, a `.claudeignore` template, an environment variable, or
a `mv` command to archive unused items.
- **In-TUI optimize view.** Press `o` in the dashboard when the status bar
shows a finding count, `b` to return. Same engine as the standalone
command, scoped to the current period and provider.
- **Per-project context budget column.** By Project panel now shows the
estimated per-session context overhead for each project (system prompt +
tools + `CLAUDE.md` + skills).
- **34 filesystem-mocking tests.** Tmpdir fixtures with `os.homedir` mocked
via `vi.mock` cover the detector surface end to end. Total suite: 198
tests across 13 files.
### Performance
- **mtime pre-filter + parallel reads + 60s result cache** cut a cold scan
from 12-17s to 6-7s on a 10k-session history.
## 0.6.1 - 2026-04-16
### Added

View file

@ -75,3 +75,10 @@ gh pr comment <number> --body "Merged, thanks!"
- NEVER include personal names or usernames in commits
- Small, focused commits. One feature per commit
- Test locally before every commit
### Public-facing language (commits, PRs, release notes, README)
- Commits and release notes are public. Write like you'd publish them.
- NEVER use words like "steal", "stealing", "copy", "rip off", "inspired by" in commit messages
- Describe what the code does, not where ideas came from
- If you must credit prior art, do it in code comments or docs, not commit messages
- No snark, no filler, no self-deprecation. Treat each commit as a product statement

View file

@ -55,6 +55,8 @@ codeburn status # compact one-liner (today + month)
codeburn status --format json
codeburn export # CSV with today, 7 days, 30 days
codeburn export -f json # JSON export
codeburn optimize # find waste, get copy-paste fixes
codeburn optimize -p week # scope the scan to last 7 days
```
Arrow keys switch between Today / 7 Days / 30 Days / Month / All Time. Press `q` to quit, `1` `2` `3` `4` `5` as shortcuts. The dashboard also shows average cost per session and the five most expensive sessions across all projects.
@ -206,6 +208,36 @@ CodeBurn surfaces the data, you read the story. A few patterns worth knowing:
These are starting points, not verdicts. A 60% cache hit on a single experimental session is fine. A persistent 60% cache hit across weeks of work is a config issue.
## Optimize
Once you know what to look for, `codeburn optimize` scans your sessions and your `~/.claude/` setup for the most common waste patterns and hands back exact, copy-paste fixes. It never writes to your files.
<p align="center">
<img src="https://raw.githubusercontent.com/AgentSeal/codeburn/main/assets/optimize.jpg" alt="CodeBurn optimize output" width="720" />
</p>
```bash
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
```
**What it detects**
- Files Claude re-reads across sessions (same content, same context, over and over)
- Low Read:Edit ratio (editing without reading leads to retries and wasted tokens)
- Projects missing a `.claudeignore` (Claude wanders into `node_modules`, `.git`, build dirs)
- Wasted bash output (uncapped `BASH_MAX_OUTPUT_LENGTH`, trailing noise)
- Unused MCP servers still paying their tool-schema overhead every session
- Ghost agents, skills, and slash commands defined in `~/.claude/` but never invoked
- Bloated `CLAUDE.md` files (with `@-import` expansion counted)
- Cache creation overhead and junk directory reads
Each finding shows the estimated token and dollar savings plus a ready-to-paste fix: a `CLAUDE.md` line, a `.claudeignore` template, an environment variable, or a `mv` command to archive unused items. Findings are ranked by urgency (impact weighted against observed waste) and rolled up into an A-F setup health grade. Repeat runs classify each finding as new, improving, or resolved against a 48-hour recent window.
You can also open it inline from the dashboard: press `o` when a finding count appears in the status bar, `b` to return.
## How it reads data
**Claude Code** stores session transcripts as JSONL at `~/.claude/projects/<sanitized-path>/<session-id>.jsonl`. Each assistant entry contains model name, token usage (input, output, cache read, cache write), tool_use blocks, and timestamps.

BIN
assets/optimize.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "codeburn",
"version": "0.6.1",
"version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeburn",
"version": "0.6.1",
"version": "0.7.0",
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",

View file

@ -1,6 +1,6 @@
{
"name": "codeburn",
"version": "0.6.1",
"version": "0.7.0",
"description": "See where your AI coding tokens go - by task, tool, model, and project",
"type": "module",
"main": "./dist/cli.js",

View file

@ -7,6 +7,7 @@ import { renderStatusBar } from './format.js'
import { installMenubar, renderMenubarFormat, type PeriodData, type ProviderCost, uninstallMenubar } from './menubar.js'
import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
import { renderDashboard } from './dashboard.js'
import { runOptimize } from './optimize.js'
import { getAllProviders } from './providers/index.js'
import { readConfig, saveConfig, getConfigFilePath } from './config.js'
import { createRequire } from 'node:module'
@ -445,4 +446,16 @@ program
console.log(` Config saved to ${getConfigFilePath()}\n`)
})
program
.command('optimize')
.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: all, claude, codex, cursor', 'all')
.action(async (opts) => {
await loadPricing()
const { range, label } = getDateRange(opts.period)
const projects = await parseAllSessions(range, opts.provider)
await runOptimize(projects, label, range)
})
program.parse()

146
src/context-budget.ts Normal file
View file

@ -0,0 +1,146 @@
import { readdir, readFile } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
const CHARS_PER_TOKEN = 4
const SYSTEM_BASE_TOKENS = 10400
const TOOL_TOKENS_OVERHEAD = 400
const SKILL_FRONTMATTER_TOKENS = 80
export type ContextBudget = {
systemBase: number
mcpTools: { count: number; tokens: number }
skills: { count: number; tokens: number }
memory: { count: number; tokens: number; files: Array<{ name: string; tokens: number }> }
total: number
modelContext: number
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / CHARS_PER_TOKEN)
}
async function readConfigFile(path: string): Promise<Record<string, unknown> | null> {
if (!existsSync(path)) return null
try {
return JSON.parse(await readFile(path, 'utf-8'))
} catch { return null }
}
async function countMcpTools(projectPath?: string): Promise<number> {
const home = homedir()
const configPaths = [
join(home, '.claude', 'settings.json'),
join(home, '.claude', 'settings.local.json'),
]
if (projectPath) {
configPaths.push(join(projectPath, '.mcp.json'))
configPaths.push(join(projectPath, '.claude', 'settings.json'))
configPaths.push(join(projectPath, '.claude', 'settings.local.json'))
}
const servers = new Set<string>()
let toolCount = 0
for (const p of configPaths) {
const config = await readConfigFile(p)
if (!config) continue
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>
for (const name of Object.keys(mcpServers)) {
if (servers.has(name)) continue
servers.add(name)
toolCount += 5
}
}
return toolCount
}
async function countSkills(projectPath?: string): Promise<number> {
const dirs = [join(homedir(), '.claude', 'skills')]
if (projectPath) dirs.push(join(projectPath, '.claude', 'skills'))
let count = 0
for (const dir of dirs) {
if (!existsSync(dir)) continue
try {
const entries = await readdir(dir)
for (const entry of entries) {
const skillFile = join(dir, entry, 'SKILL.md')
if (existsSync(skillFile)) count++
}
} catch { continue }
}
return count
}
async function scanMemoryFiles(projectPath?: string): Promise<Array<{ name: string; tokens: number }>> {
const home = homedir()
const files: Array<{ name: string; tokens: number }> = []
const paths: Array<{ path: string; name: string }> = [
{ path: join(home, '.claude', 'CLAUDE.md'), name: '~/.claude/CLAUDE.md' },
]
if (projectPath) {
paths.push({ path: join(projectPath, 'CLAUDE.md'), name: 'CLAUDE.md' })
paths.push({ path: join(projectPath, '.claude', 'CLAUDE.md'), name: '.claude/CLAUDE.md' })
paths.push({ path: join(projectPath, 'CLAUDE.local.md'), name: 'CLAUDE.local.md' })
}
for (const { path, name } of paths) {
if (!existsSync(path)) continue
try {
const content = await readFile(path, 'utf-8')
files.push({ name, tokens: estimateTokens(content) })
} catch { continue }
}
return files
}
export async function estimateContextBudget(projectPath?: string, modelContext = 1_000_000): Promise<ContextBudget> {
const mcpToolCount = await countMcpTools(projectPath)
const skillCount = await countSkills(projectPath)
const memoryFiles = await scanMemoryFiles(projectPath)
const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD
const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS
const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0)
const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens
return {
systemBase: SYSTEM_BASE_TOKENS,
mcpTools: { count: mcpToolCount, tokens: mcpTokens },
skills: { count: skillCount, tokens: skillTokens },
memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
total,
modelContext,
}
}
export async function estimateBudgetsByProject(projectPaths: Map<string, string>): Promise<Map<string, ContextBudget>> {
const results = new Map<string, ContextBudget>()
for (const [project, cwd] of projectPaths) {
const budget = await estimateContextBudget(cwd)
results.set(project, budget)
}
return results
}
export async function discoverProjectCwd(sessionDir: string): Promise<string | null> {
try {
const files = (await readdir(sessionDir)).filter(f => f.endsWith('.jsonl'))
if (files.length === 0) return null
const content = await readFile(join(sessionDir, files[0]), 'utf-8')
for (const line of content.split('\n')) {
if (!line.trim()) continue
try {
const entry = JSON.parse(line)
if (entry.cwd && typeof entry.cwd === 'string') return entry.cwd
} catch { continue }
}
} catch { return null }
return null
}

View file

@ -1,14 +1,18 @@
import { homedir } from 'os'
import React, { useState, useCallback, useEffect } from 'react'
import React, { useState, useCallback, useEffect, useRef } 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, filterProjectsByName } from './parser.js'
import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
import { join } from 'path'
type Period = 'today' | 'week' | '30days' | 'month' | 'all'
type View = 'dashboard' | 'optimize'
const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
const PERIOD_LABELS: Record<Period, string> = {
@ -71,6 +75,8 @@ const CATEGORY_COLORS: Record<TaskCategory, string> = {
general: '#666666',
}
const IMPACT_PANEL_COLORS: Record<string, string> = { high: '#F55B5B', medium: ORANGE, low: DIM }
function toHex(r: number, g: number, b: number): string {
return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')
}
@ -79,7 +85,6 @@ function lerp(a: number, b: number, t: number): number {
return a + t * (b - a)
}
// Blue -> amber -> orange gradient across the bar width
function gradientColor(pct: number): string {
if (pct <= 0.33) {
const t = pct / 0.33
@ -218,32 +223,31 @@ const _homeEncoded = homedir().replace(/\//g, '-')
function shortProject(encoded: string): string {
let path = encoded.replace(/^-/, '')
if (path.startsWith(_homeEncoded.replace(/^-/, ''))) {
path = path.slice(_homeEncoded.replace(/^-/, '').length).replace(/^-/, '')
}
path = path
.replace(/^private-tmp-[^-]+-[^-]+-/, '') // /private/tmp/<org>/<env>/
.replace(/^private-tmp-/, '')
.replace(/^tmp-/, '')
path = path.replace(/^private-tmp-[^-]+-[^-]+-/, '').replace(/^private-tmp-/, '').replace(/^tmp-/, '')
if (!path) return 'home'
const parts = path.split('-').filter(Boolean)
if (parts.length <= 3) return parts.join('/')
return parts.slice(-3).join('/')
}
const PROJECT_COL_AVG = 7
const PROJECT_COL_BASE_WIDTH = 30
const PROJECT_COL_WITH_OVERHEAD_WIDTH = 40
function ProjectBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
function ProjectBreakdown({ projects, pw, bw, budgets }: { projects: ProjectSummary[]; pw: number; bw: number; budgets?: Map<string, ContextBudget> }) {
const maxCost = Math.max(...projects.map(p => p.totalCostUSD))
const nw = Math.max(8, pw - bw - 30)
const hasBudgets = budgets && budgets.size > 0
const nw = Math.max(8, pw - bw - (hasBudgets ? PROJECT_COL_WITH_OVERHEAD_WIDTH : PROJECT_COL_BASE_WIDTH))
return (
<Panel title="By Project" color={PANEL_COLORS.project} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'avg/s'.padStart(PROJECT_COL_AVG)}{'sess'.padStart(6)}</Text>
<Text dimColor wrap="truncate-end">
{''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'avg/s'.padStart(PROJECT_COL_AVG)}{'sess'.padStart(6)}{hasBudgets ? 'overhead'.padStart(10) : ''}
</Text>
{projects.slice(0, 8).map((project, i) => {
const budget = budgets?.get(project.project)
const avgCost = project.sessions.length > 0
? formatCost(project.totalCostUSD / project.sessions.length)
: '-'
@ -254,6 +258,7 @@ function ProjectBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw
<Text color={GOLD}>{formatCost(project.totalCostUSD).padStart(8)}</Text>
<Text color={GOLD}>{avgCost.padStart(PROJECT_COL_AVG)}</Text>
<Text>{String(project.sessions.length).padStart(6)}</Text>
{hasBudgets && <Text color="#7B9EF5">{(budget ? formatTokens(budget.total) : '-').padStart(10)}</Text>}
</Text>
)
})}
@ -319,7 +324,6 @@ function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; p
}
const sorted = Object.entries(categoryTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD)
const maxCost = sorted[0]?.[1]?.costUSD ?? 0
return (
<Panel title="By Activity" color={PANEL_COLORS.activity} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 14)}{'cost'.padStart(8)}{'turns'.padStart(6)}{'1-shot'.padStart(7)}</Text>
@ -328,9 +332,7 @@ function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; p
return (
<Text key={cat} wrap="truncate-end">
<HBar value={data.costUSD} max={maxCost} width={bw} />
<Text color={CATEGORY_COLORS[cat as TaskCategory] ?? '#666666'}>
{' '}{fit(CATEGORY_LABELS[cat as TaskCategory] ?? cat, 13)}
</Text>
<Text color={CATEGORY_COLORS[cat as TaskCategory] ?? '#666666'}> {fit(CATEGORY_LABELS[cat as TaskCategory] ?? cat, 13)}</Text>
<Text color={GOLD}>{formatCost(data.costUSD).padStart(8)}</Text>
<Text>{String(data.turns).padStart(6)}</Text>
<Text color={data.editTurns === 0 ? DIM : oneShotPct === '100%' ? '#5BF58C' : ORANGE}>{String(oneShotPct).padStart(7)}</Text>
@ -346,11 +348,7 @@ function ToolBreakdown({ projects, pw, bw, title, filterPrefix }: { projects: Pr
for (const project of projects) {
for (const session of project.sessions) {
for (const [tool, data] of Object.entries(session.toolBreakdown)) {
if (filterPrefix) {
if (!tool.startsWith(filterPrefix)) continue
} else {
if (tool.startsWith('lang:')) continue
}
if (filterPrefix) { if (!tool.startsWith(filterPrefix)) continue } else { if (tool.startsWith('lang:')) continue }
toolTotals[tool] = (toolTotals[tool] ?? 0) + data.calls
}
}
@ -358,7 +356,6 @@ function ToolBreakdown({ projects, pw, bw, title, filterPrefix }: { projects: Pr
const sorted = Object.entries(toolTotals).sort(([, a], [, b]) => b - a)
const maxCalls = sorted[0]?.[1] ?? 0
const nw = Math.max(6, pw - bw - 15)
return (
<Panel title={title ?? 'Core Tools'} color={PANEL_COLORS.tools} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)}</Text>
@ -417,29 +414,16 @@ function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: num
function McpBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const mcpTotals: Record<string, number> = {}
for (const project of projects) {
for (const session of project.sessions) {
for (const [server, data] of Object.entries(session.mcpBreakdown)) {
mcpTotals[server] = (mcpTotals[server] ?? 0) + data.calls
}
}
}
for (const project of projects) { for (const session of project.sessions) { for (const [server, data] of Object.entries(session.mcpBreakdown)) { mcpTotals[server] = (mcpTotals[server] ?? 0) + data.calls } } }
const sorted = Object.entries(mcpTotals).sort(([, a], [, b]) => b - a)
if (sorted.length === 0) {
return <Panel title="MCP Servers" color={PANEL_COLORS.mcp} width={pw}><Text dimColor>No MCP usage</Text></Panel>
}
if (sorted.length === 0) return <Panel title="MCP Servers" color={PANEL_COLORS.mcp} width={pw}><Text dimColor>No MCP usage</Text></Panel>
const maxCalls = sorted[0]?.[1] ?? 0
const nw = Math.max(6, pw - bw - 15)
return (
<Panel title="MCP Servers" color={PANEL_COLORS.mcp} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(6)}</Text>
{sorted.slice(0, 8).map(([server, calls]) => (
<Text key={server} wrap="truncate-end">
<HBar value={calls} max={maxCalls} width={bw} />
<Text> {fit(server, nw)}</Text>
<Text>{String(calls).padStart(6)}</Text>
</Text>
<Text key={server} wrap="truncate-end"><HBar value={calls} max={maxCalls} width={bw} /><Text> {fit(server, nw)}</Text><Text>{String(calls).padStart(6)}</Text></Text>
))}
</Panel>
)
@ -447,29 +431,16 @@ function McpBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: nu
function BashBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const bashTotals: Record<string, number> = {}
for (const project of projects) {
for (const session of project.sessions) {
for (const [cmd, data] of Object.entries(session.bashBreakdown)) {
bashTotals[cmd] = (bashTotals[cmd] ?? 0) + data.calls
}
}
}
for (const project of projects) { for (const session of project.sessions) { for (const [cmd, data] of Object.entries(session.bashBreakdown)) { bashTotals[cmd] = (bashTotals[cmd] ?? 0) + data.calls } } }
const sorted = Object.entries(bashTotals).sort(([, a], [, b]) => b - a)
if (sorted.length === 0) {
return <Panel title="Shell Commands" color={PANEL_COLORS.bash} width={pw}><Text dimColor>No shell commands</Text></Panel>
}
if (sorted.length === 0) return <Panel title="Shell Commands" color={PANEL_COLORS.bash} width={pw}><Text dimColor>No shell commands</Text></Panel>
const maxCalls = sorted[0]?.[1] ?? 0
const nw = Math.max(6, pw - bw - 15)
return (
<Panel title="Shell Commands" color={PANEL_COLORS.bash} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'calls'.padStart(7)}</Text>
{sorted.slice(0, 10).map(([cmd, calls]) => (
<Text key={cmd} wrap="truncate-end">
<HBar value={calls} max={maxCalls} width={bw} />
<Text> {fit(cmd, nw)}</Text>
<Text>{String(calls).padStart(7)}</Text>
</Text>
<Text key={cmd} wrap="truncate-end"><HBar value={calls} max={maxCalls} width={bw} /><Text> {fit(cmd, nw)}</Text><Text>{String(calls).padStart(7)}</Text></Text>
))}
</Panel>
)
@ -483,16 +454,9 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
opencode: 'OpenCode',
pi: 'Pi',
}
function getProviderDisplayName(name: string): string { return PROVIDER_DISPLAY_NAMES[name] ?? name }
function getProviderDisplayName(name: string): string {
return PROVIDER_DISPLAY_NAMES[name] ?? name
}
function PeriodTabs({ active, providerName, showProvider }: {
active: Period
providerName?: string
showProvider?: boolean
}) {
function PeriodTabs({ active, providerName, showProvider }: { active: Period; providerName?: string; showProvider?: boolean }) {
return (
<Box justifyContent="space-between" paddingX={1}>
<Box gap={1}>
@ -503,41 +467,82 @@ function PeriodTabs({ active, providerName, showProvider }: {
))}
</Box>
{showProvider && providerName && (
<Box>
<Text color={DIM}>| </Text>
<Text color={ORANGE} bold>[p]</Text>
<Text bold color={PROVIDER_COLORS[providerName] ?? ORANGE}> {getProviderDisplayName(providerName)}</Text>
</Box>
<Box><Text color={DIM}>| </Text><Text color={ORANGE} bold>[p]</Text><Text bold color={PROVIDER_COLORS[providerName] ?? ORANGE}> {getProviderDisplayName(providerName)}</Text></Box>
)}
</Box>
)
}
function StatusBar({ width, showProvider }: { width: number; showProvider?: boolean }) {
function FindingAction({ action }: { action: WasteAction }) {
const lines = action.type === 'file-content' ? action.content.split('\n') : action.type === 'command' ? action.text.split('\n') : [action.text]
return (<><Text dimColor>{action.label}</Text>{lines.map((line, i) => <Text key={i} color="#5BF5E0"> {line}</Text>)}</>)
}
function FindingPanel({ index, finding, costRate, width }: { index: number; finding: WasteFinding; costRate: number; width: number }) {
const costSaved = finding.tokensSaved * costRate
const color = IMPACT_PANEL_COLORS[finding.impact] ?? DIM
const label = finding.impact.charAt(0).toUpperCase() + finding.impact.slice(1)
const trendBadge = finding.trend === 'improving' ? ' improving \u2193' : ''
return (
<Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1} width={width}>
<Text wrap="truncate-end">
<Text bold>{index}. {finding.title}</Text>
<Text> </Text>
<Text color={color}>{label}</Text>
{trendBadge && <Text color="#5BF5A0">{trendBadge}</Text>}
</Text>
<Text dimColor wrap="wrap">{finding.explanation}</Text>
<Text color={GOLD}>Savings: ~{formatTokens(finding.tokensSaved)} tokens (~{formatCost(costSaved)})</Text>
<Text> </Text>
<FindingAction action={finding.fix} />
</Box>
)
}
const GRADE_COLORS: Record<string, string> = { A: '#5BF5A0', B: '#5BF5A0', C: GOLD, D: ORANGE, F: '#F55B5B' }
function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade }: { findings: WasteFinding[]; costRate: number; projects: ProjectSummary[]; label: string; width: number; healthScore: number; healthGrade: string }) {
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0)
const totalTokens = findings.reduce((s, f) => s + f.tokensSaved, 0)
const totalCost = totalTokens * costRate
const pctRaw = periodCost > 0 ? (totalCost / periodCost) * 100 : 0
const pct = pctRaw >= 1 ? pctRaw.toFixed(0) : pctRaw.toFixed(1)
const gradeColor = GRADE_COLORS[healthGrade] ?? DIM
return (
<Box flexDirection="column" width={width}>
<Box flexDirection="column" borderStyle="round" borderColor={ORANGE} paddingX={1} width={width}>
<Text wrap="truncate-end">
<Text bold color={ORANGE}>CodeBurn Optimize</Text>
<Text dimColor> {label} Setup: </Text>
<Text bold color={gradeColor}>{healthGrade}</Text>
<Text dimColor> ({healthScore}/100)</Text>
</Text>
<Text color="#5BF5A0" wrap="truncate-end">Savings: ~{formatTokens(totalTokens)} tokens (~{formatCost(totalCost)}, ~{pct}% of spend)</Text>
</Box>
{findings.map((f, i) => <FindingPanel key={i} index={i + 1} finding={f} costRate={costRate} width={width} />)}
<Box paddingX={1} width={width}><Text dimColor>Token estimates are approximate.</Text></Box>
</Box>
)
}
function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable }: { width: number; showProvider?: boolean; view?: View; findingCount?: number; optimizeAvailable?: boolean }) {
const isOptimize = view === 'optimize'
return (
<Box borderStyle="round" borderColor={DIM} width={width} justifyContent="center" paddingX={1}>
<Text>
<Text color={ORANGE} bold>{'<'}</Text><Text color={ORANGE}>{'>'}</Text>
<Text dimColor> switch </Text>
<Text color={ORANGE} bold>q</Text>
<Text dimColor> quit </Text>
<Text color={ORANGE} bold>1</Text>
<Text dimColor> today </Text>
<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> all time</Text>
{showProvider && (
<>
<Text dimColor> </Text>
<Text color={ORANGE} bold>p</Text>
<Text dimColor> provider</Text>
</>
{isOptimize
? <><Text color={ORANGE} bold>b</Text><Text dimColor> back </Text></>
: <><Text color={ORANGE} bold>{'<'}</Text><Text color={ORANGE}>{'>'}</Text><Text dimColor> switch </Text></>}
<Text color={ORANGE} bold>q</Text><Text dimColor> quit </Text>
<Text color={ORANGE} bold>1</Text><Text dimColor> today </Text>
<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> all time</Text>
{!isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && (
<><Text dimColor> </Text><Text color={ORANGE} bold>o</Text><Text dimColor> optimize</Text><Text color="#F55B5B"> ({findingCount})</Text></>
)}
{showProvider && (<><Text dimColor> </Text><Text color={ORANGE} bold>p</Text><Text dimColor> provider</Text></>)}
</Text>
</Box>
)
@ -548,48 +553,22 @@ function Row({ wide, width, children }: { wide: boolean; width: number; children
return <>{children}</>
}
function DashboardContent({ projects, period, columns, activeProvider }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string }) {
function DashboardContent({ projects, period, columns, activeProvider, budgets }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string; budgets?: Map<string, ContextBudget> }) {
const { dashWidth, wide, halfWidth, barWidth } = getLayout(columns)
const isCursor = activeProvider === 'cursor'
if (projects.length === 0) {
return (
<Panel title="CodeBurn" color={ORANGE} width={dashWidth}>
<Text dimColor>No usage data found for {PERIOD_LABELS[period]}.</Text>
</Panel>
)
}
if (projects.length === 0) return <Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>No usage data found for {PERIOD_LABELS[period]}.</Text></Panel>
const pw = wide ? halfWidth : dashWidth
// undefined = no cutoff (show all days); 31 for month/30-day ranges; 14 for shorter periods
const days = period === 'all' ? undefined : (period === 'month' || period === '30days' ? 31 : 14)
return (
<Box flexDirection="column" width={dashWidth}>
<Overview projects={projects} label={PERIOD_LABELS[period]} width={dashWidth} />
<Row wide={wide} width={dashWidth}>
<DailyActivity projects={projects} days={days} pw={pw} bw={barWidth} />
<ProjectBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<Row wide={wide} width={dashWidth}><DailyActivity projects={projects} days={days} pw={pw} bw={barWidth} /><ProjectBreakdown projects={projects} pw={pw} bw={barWidth} budgets={budgets} /></Row>
<TopSessions projects={projects} pw={dashWidth} bw={barWidth} />
<Row wide={wide} width={dashWidth}>
<ActivityBreakdown projects={projects} pw={pw} bw={barWidth} />
<ModelBreakdown projects={projects} pw={pw} bw={barWidth} />
</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:" />
) : (
<>
<Row wide={wide} width={dashWidth}>
<ToolBreakdown projects={projects} pw={pw} bw={barWidth} />
<BashBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<McpBreakdown projects={projects} pw={dashWidth} bw={barWidth} />
</>
<><Row wide={wide} width={dashWidth}><ToolBreakdown projects={projects} pw={pw} bw={barWidth} /><BashBreakdown projects={projects} pw={pw} bw={barWidth} /></Row><McpBreakdown projects={projects} pw={dashWidth} bw={barWidth} /></>
)}
</Box>
)
@ -609,29 +588,59 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const [loading, setLoading] = useState(false)
const [activeProvider, setActiveProvider] = useState(initialProvider)
const [detectedProviders, setDetectedProviders] = useState<string[]>([])
const [view, setView] = useState<View>('dashboard')
const [optimizeResult, setOptimizeResult] = useState<OptimizeResult | null>(null)
const [projectBudgets, setProjectBudgets] = useState<Map<string, ContextBudget>>(new Map())
const { columns } = useWindowSize()
const { dashWidth } = getLayout(columns)
const multipleProviders = detectedProviders.length > 1
const optimizeAvailable = activeProvider === 'all' || activeProvider === 'claude'
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const findingCount = optimizeResult?.findings.length ?? 0
useEffect(() => {
let cancelled = false
async function detect() {
const found: string[] = []
const allProviders = await getAllProviders()
for (const p of allProviders) {
const sessions = await p.discoverSessions()
if (sessions.length > 0) found.push(p.name)
}
if (!cancelled) {
setDetectedProviders(found)
}
for (const p of await getAllProviders()) { const s = await p.discoverSessions(); if (s.length > 0) found.push(p.name) }
if (!cancelled) setDetectedProviders(found)
}
detect()
return () => { cancelled = true }
}, [])
useEffect(() => {
let cancelled = false
async function loadBudgets() {
const claudeDir = join(homedir(), '.claude', 'projects')
const budgets = new Map<string, ContextBudget>()
for (const project of projects.slice(0, 8)) {
if (cancelled) return
const cwd = await discoverProjectCwd(join(claudeDir, project.project))
if (!cwd) continue
budgets.set(project.project, await estimateContextBudget(cwd))
}
if (!cancelled) setProjectBudgets(budgets)
}
loadBudgets()
return () => { cancelled = true }
}, [projects])
useEffect(() => {
if (!optimizeAvailable) { setOptimizeResult(null); return }
let cancelled = false
async function scan() {
if (projects.length === 0) { setOptimizeResult(null); return }
const result = await scanAndDetect(projects, getDateRange(period))
if (!cancelled) setOptimizeResult(result)
}
scan()
return () => { cancelled = true }
}, [projects, period, optimizeAvailable])
const reloadData = useCallback(async (p: Period, prov: string) => {
setLoading(true)
setOptimizeResult(null)
const range = getDateRange(p)
const data = filterProjectsByName(await parseAllSessions(range, prov), projectFilter, excludeFilter)
setProjects(data)
@ -644,46 +653,34 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return () => clearInterval(id)
}, [refreshSeconds, period, activeProvider, reloadData])
const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
const switchPeriod = useCallback((newPeriod: Period) => {
if (newPeriod === period) return
setPeriod(newPeriod)
const switchPeriod = useCallback((np: Period) => {
if (np === period) return
setPeriod(np); setView('dashboard')
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {
reloadData(newPeriod, activeProvider)
}, 600)
debounceRef.current = setTimeout(() => { reloadData(np, activeProvider) }, 600)
}, [period, activeProvider, reloadData])
const switchPeriodImmediate = useCallback(async (newPeriod: Period) => {
if (newPeriod === period) return
setPeriod(newPeriod)
const switchPeriodImmediate = useCallback(async (np: Period) => {
if (np === period) return
setPeriod(np); setView('dashboard')
if (debounceRef.current) clearTimeout(debounceRef.current)
await reloadData(newPeriod, activeProvider)
await reloadData(np, activeProvider)
}, [period, activeProvider, reloadData])
useInput((input, key) => {
if (input === 'q') {
exit()
return
}
if (input === 'q') { exit(); return }
if (input === 'o' && findingCount > 0 && view === 'dashboard' && optimizeAvailable) { setView('optimize'); return }
if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); return }
if (input === 'p' && multipleProviders) {
const options = ['all', ...detectedProviders]
const idx = options.indexOf(activeProvider)
const next = options[(idx + 1) % options.length]
setActiveProvider(next)
const opts = ['all', ...detectedProviders]; const next = opts[(opts.indexOf(activeProvider) + 1) % opts.length]
setActiveProvider(next); setView('dashboard')
if (debounceRef.current) clearTimeout(debounceRef.current)
reloadData(period, next)
return
reloadData(period, next); return
}
const idx = PERIODS.indexOf(period)
if (key.leftArrow) {
switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length])
} else if (key.rightArrow || key.tab) {
switchPeriod(PERIODS[(idx + 1) % PERIODS.length])
} else if (input === '1') switchPeriodImmediate('today')
if (key.leftArrow && view === 'dashboard') switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length])
else if ((key.rightArrow || key.tab) && view === 'dashboard') switchPeriod(PERIODS[(idx + 1) % PERIODS.length])
else if (input === '1') switchPeriodImmediate('today')
else if (input === '2') switchPeriodImmediate('week')
else if (input === '3') switchPeriodImmediate('30days')
else if (input === '4') switchPeriodImmediate('month')
@ -694,10 +691,8 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return (
<Box flexDirection="column" width={dashWidth}>
<PeriodTabs active={period} providerName={activeProvider} showProvider={multipleProviders} />
<Panel title="CodeBurn" color={ORANGE} width={dashWidth}>
<Text dimColor>Loading {PERIOD_LABELS[period]}...</Text>
</Panel>
<StatusBar width={dashWidth} showProvider={multipleProviders} />
<Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>Loading {PERIOD_LABELS[period]}...</Text></Panel>
<StatusBar width={dashWidth} showProvider={multipleProviders} view="dashboard" findingCount={0} optimizeAvailable={false} />
</Box>
)
}
@ -705,8 +700,10 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return (
<Box flexDirection="column" width={dashWidth}>
<PeriodTabs active={period} providerName={activeProvider} showProvider={multipleProviders} />
<DashboardContent projects={projects} period={period} columns={columns} activeProvider={activeProvider} />
<StatusBar width={dashWidth} showProvider={multipleProviders} />
{view === 'optimize' && optimizeResult
? <OptimizeView findings={optimizeResult.findings} costRate={optimizeResult.costRate} projects={projects} label={PERIOD_LABELS[period]} width={dashWidth} healthScore={optimizeResult.healthScore} healthGrade={optimizeResult.healthGrade} />
: <DashboardContent projects={projects} period={period} columns={columns} activeProvider={activeProvider} budgets={projectBudgets} />}
<StatusBar width={dashWidth} showProvider={multipleProviders} view={view} findingCount={findingCount} optimizeAvailable={optimizeAvailable} />
</Box>
)
}
@ -726,19 +723,14 @@ export async function renderDashboard(period: Period = 'week', provider: string
await loadPricing()
const range = getDateRange(period)
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} projectFilter={projectFilter} excludeFilter={excludeFilter} />
)
await waitUntilExit()
} else {
const { unmount } = render(
<StaticDashboard projects={projects} period={period} activeProvider={provider} />,
{ patchConsole: false }
)
const { unmount } = render(<StaticDashboard projects={projects} period={period} activeProvider={provider} />, { patchConsole: false })
unmount()
}
}

1177
src/optimize.ts Normal file

File diff suppressed because it is too large Load diff

445
tests/optimize-fs.test.ts Normal file
View file

@ -0,0 +1,445 @@
import { describe, it, expect, afterAll, beforeEach, vi } from 'vitest'
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
vi.mock('os', async () => {
const actual = await vi.importActual<typeof import('os')>('os')
const fs = await vi.importActual<typeof import('fs')>('fs')
const fakeHome = fs.mkdtempSync(actual.tmpdir() + '/codeburn-home-')
fs.mkdirSync(fakeHome + '/.claude', { recursive: true })
process.env['CODEBURN_TEST_FAKE_HOME'] = fakeHome
return { ...actual, homedir: () => fakeHome }
})
const FAKE_HOME_FOR_MOCK = process.env['CODEBURN_TEST_FAKE_HOME']!
import {
detectMissingClaudeignore,
detectBloatedClaudeMd,
detectUnusedMcp,
detectBashBloat,
detectGhostAgents,
detectGhostSkills,
detectGhostCommands,
loadMcpConfigs,
scanJsonlFile,
scanAndDetect,
type ToolCall,
} from '../src/optimize.js'
import {
estimateContextBudget,
discoverProjectCwd,
} from '../src/context-budget.js'
// ============================================================================
// Helpers for filesystem fixtures
// ============================================================================
const FIXTURE_ROOTS: string[] = [FAKE_HOME_FOR_MOCK]
function makeFixtureRoot(): string {
const dir = mkdtempSync(join(tmpdir(), 'codeburn-test-'))
FIXTURE_ROOTS.push(dir)
return dir
}
function writeFile(path: string, content: string): void {
mkdirSync(join(path, '..'), { recursive: true })
writeFileSync(path, content)
}
function touchOld(path: string, daysAgo: number): void {
const past = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
utimesSync(path, past, past)
}
afterAll(() => {
for (const dir of FIXTURE_ROOTS) {
rmSync(dir, { recursive: true, force: true })
}
})
// ============================================================================
// detectMissingClaudeignore
// ============================================================================
describe('detectMissingClaudeignore', () => {
it('flags a project with node_modules but no .claudeignore', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(join(projectDir, 'node_modules'), { recursive: true })
const finding = detectMissingClaudeignore(new Set([projectDir]))
expect(finding).not.toBeNull()
expect(finding!.impact).toBe('medium')
})
it('does not flag when .claudeignore exists', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(join(projectDir, 'node_modules'), { recursive: true })
writeFile(join(projectDir, '.claudeignore'), 'node_modules\n')
expect(detectMissingClaudeignore(new Set([projectDir]))).toBeNull()
})
it('does not flag project without junk dirs', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(join(projectDir, 'src'), { recursive: true })
expect(detectMissingClaudeignore(new Set([projectDir]))).toBeNull()
})
it('escalates to high when three or more projects need it', () => {
const root = makeFixtureRoot()
const cwds = new Set<string>()
for (let i = 0; i < 3; i++) {
const p = join(root, `proj-${i}`)
mkdirSync(join(p, 'node_modules'), { recursive: true })
cwds.add(p)
}
const finding = detectMissingClaudeignore(cwds)
expect(finding!.impact).toBe('high')
})
})
// ============================================================================
// detectBloatedClaudeMd (including @-import expansion)
// ============================================================================
describe('detectBloatedClaudeMd', () => {
it('flags a CLAUDE.md with more than 200 lines', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
const content = Array.from({ length: 300 }, (_, i) => `line ${i}`).join('\n')
writeFile(join(projectDir, 'CLAUDE.md'), content)
const finding = detectBloatedClaudeMd(new Set([projectDir]))
expect(finding).not.toBeNull()
})
it('expands @-imports and counts transitive load', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(
join(projectDir, 'CLAUDE.md'),
'line 1\nline 2\n@./rules.md\n@./conventions.md\n',
)
writeFile(join(projectDir, 'rules.md'), Array.from({ length: 120 }, (_, i) => `rule ${i}`).join('\n'))
writeFile(join(projectDir, 'conventions.md'), Array.from({ length: 120 }, (_, i) => `conv ${i}`).join('\n'))
const finding = detectBloatedClaudeMd(new Set([projectDir]))
expect(finding).not.toBeNull()
expect(finding!.explanation).toContain('2 @-imports')
})
it('does not flag a lean CLAUDE.md under 200 lines with no imports', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, 'CLAUDE.md'), 'just a few\nlines\nhere\n')
expect(detectBloatedClaudeMd(new Set([projectDir]))).toBeNull()
})
it('does not recurse infinitely on circular @-imports', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, 'CLAUDE.md'), '@./a.md\n')
writeFile(join(projectDir, 'a.md'), '@./b.md\n')
writeFile(join(projectDir, 'b.md'), '@./a.md\n')
expect(() => detectBloatedClaudeMd(new Set([projectDir]))).not.toThrow()
})
it('ignores @ tokens that are not paths (emails, npm scopes)', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(
join(projectDir, 'CLAUDE.md'),
Array.from({ length: 250 }, (_, i) =>
i === 10 ? '@user@example.com' :
i === 20 ? '@org/package' :
`line ${i}`
).join('\n'),
)
const finding = detectBloatedClaudeMd(new Set([projectDir]))
expect(finding).not.toBeNull()
// "with N @-imports" suffix appears only when non-zero imports were resolved
expect(finding!.explanation).not.toMatch(/with \d+ @-import/)
})
})
// ============================================================================
// loadMcpConfigs + detectUnusedMcp
// ============================================================================
describe('loadMcpConfigs', () => {
it('returns empty map when no configs exist', () => {
const root = makeFixtureRoot()
const servers = loadMcpConfigs([root])
expect(servers.size).toBe(0)
})
it('reads servers from project .mcp.json', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
mcpServers: { foo: { command: 'foo' }, bar: { command: 'bar' } },
}))
const servers = loadMcpConfigs([projectDir])
expect(servers.has('foo')).toBe(true)
expect(servers.has('bar')).toBe(true)
})
it('normalizes server names by replacing colons with underscores', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
mcpServers: { 'plugin:context7:context7': { command: 'ctx' } },
}))
const servers = loadMcpConfigs([projectDir])
expect(servers.has('plugin_context7_context7')).toBe(true)
expect(servers.get('plugin_context7_context7')!.original).toBe('plugin:context7:context7')
})
it('handles malformed JSON without crashing', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), '{ not valid json')
expect(() => loadMcpConfigs([projectDir])).not.toThrow()
expect(loadMcpConfigs([projectDir]).size).toBe(0)
})
})
describe('detectUnusedMcp', () => {
it('flags servers configured but never called', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
mcpServers: { ghost: { command: 'x' } },
}))
const configFile = join(projectDir, '.mcp.json')
touchOld(configFile, 30)
const finding = detectUnusedMcp([], [], new Set([projectDir]))
expect(finding).not.toBeNull()
expect(finding!.explanation).toContain('ghost')
})
it('does not flag servers configured within 24 hours', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
mcpServers: { freshly_added: { command: 'x' } },
}))
expect(detectUnusedMcp([], [], new Set([projectDir]))).toBeNull()
})
it('does not flag servers that were called', () => {
const root = makeFixtureRoot()
const projectDir = join(root, 'myapp')
mkdirSync(projectDir, { recursive: true })
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
mcpServers: { used: { command: 'x' } },
}))
touchOld(join(projectDir, '.mcp.json'), 30)
const calls: ToolCall[] = [
{ name: 'mcp__used__some_tool', input: {}, sessionId: 's1', project: 'p1' },
]
expect(detectUnusedMcp(calls, [], new Set([projectDir]))).toBeNull()
})
})
// ============================================================================
// detectBashBloat
// ============================================================================
describe('detectBashBloat', () => {
const originalEnv = process.env['BASH_MAX_OUTPUT_LENGTH']
beforeEach(() => {
delete process.env['BASH_MAX_OUTPUT_LENGTH']
})
afterAll(() => {
if (originalEnv !== undefined) process.env['BASH_MAX_OUTPUT_LENGTH'] = originalEnv
})
it('flags when env var is unset (uses default 30K)', () => {
const finding = detectBashBloat()
expect(finding).not.toBeNull()
expect(finding!.impact).toBe('medium')
})
it('does not flag when env var is at recommended 15K', () => {
process.env['BASH_MAX_OUTPUT_LENGTH'] = '15000'
expect(detectBashBloat()).toBeNull()
})
it('does not flag when env var is below recommended', () => {
process.env['BASH_MAX_OUTPUT_LENGTH'] = '10000'
expect(detectBashBloat()).toBeNull()
})
it('flags when env var is above 15K', () => {
process.env['BASH_MAX_OUTPUT_LENGTH'] = '50000'
const finding = detectBashBloat()
expect(finding).not.toBeNull()
})
})
// ============================================================================
// detectGhostCommands (the pure-function ghost detector)
// ============================================================================
describe('detectGhostCommands', () => {
it('returns null when no commands are defined', async () => {
expect(await detectGhostCommands([])).toBeNull()
})
it('does not match /tmp or /usr or other path prefixes as command usage', async () => {
const messages = [
'check /tmp/debug.log',
'look at /usr/local/bin',
'rm -rf /var/cache',
]
expect(await detectGhostCommands(messages)).toBeNull()
})
it('matches <command-name> tags in user messages', async () => {
const messages = ['<command-name>review</command-name>']
expect(await detectGhostCommands(messages)).toBeNull()
})
})
// ============================================================================
// scanJsonlFile
// ============================================================================
describe('scanJsonlFile', () => {
it('returns empty result for nonexistent file', async () => {
const result = await scanJsonlFile('/nonexistent/path.jsonl', 'p1', undefined)
expect(result.calls).toEqual([])
expect(result.cwds).toEqual([])
expect(result.apiCalls).toEqual([])
expect(result.userMessages).toEqual([])
})
it('parses tool_use blocks from assistant entries', async () => {
const root = makeFixtureRoot()
const filePath = join(root, 'session.jsonl')
const now = new Date().toISOString()
const lines = [
JSON.stringify({
type: 'assistant',
timestamp: now,
message: {
content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/x/foo.ts' } }],
},
}),
]
writeFile(filePath, lines.join('\n'))
const result = await scanJsonlFile(filePath, 'p1', undefined)
expect(result.calls).toHaveLength(1)
expect(result.calls[0].name).toBe('Read')
})
it('skips malformed JSONL lines without crashing', async () => {
const root = makeFixtureRoot()
const filePath = join(root, 'session.jsonl')
writeFile(filePath, 'this is not json\n{broken\n{"type":"assistant","message":{"content":[]}}\n')
const result = await scanJsonlFile(filePath, 'p1', undefined)
expect(result.calls).toEqual([])
})
it('respects date-range filter for assistant entries', async () => {
const root = makeFixtureRoot()
const filePath = join(root, 'session.jsonl')
const old = '2020-01-01T00:00:00Z'
const now = new Date().toISOString()
writeFile(filePath, [
JSON.stringify({
type: 'assistant', timestamp: old,
message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/old' } }] },
}),
JSON.stringify({
type: 'assistant', timestamp: now,
message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/new' } }] },
}),
].join('\n'))
const today = new Date()
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const result = await scanJsonlFile(filePath, 'p1', { start, end: today })
expect(result.calls).toHaveLength(1)
expect((result.calls[0].input as Record<string, unknown>).file_path).toBe('/new')
})
})
// ============================================================================
// scanAndDetect (top-level integration)
// ============================================================================
describe('scanAndDetect', () => {
it('returns healthy result for empty projects', async () => {
const result = await scanAndDetect([])
expect(result.findings).toEqual([])
expect(result.healthScore).toBe(100)
expect(result.healthGrade).toBe('A')
expect(result.costRate).toBe(0)
})
})
// ============================================================================
// context-budget
// ============================================================================
describe('estimateContextBudget', () => {
it('returns only system base when project has no config', async () => {
const root = makeFixtureRoot()
const budget = await estimateContextBudget(root)
expect(budget.total).toBeGreaterThan(0)
expect(budget.mcpTools.count).toBe(0)
expect(budget.skills.count).toBe(0)
})
it('includes MCP tools from project .mcp.json', async () => {
const root = makeFixtureRoot()
writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: { a: { command: 'x' }, b: { command: 'x' } },
}))
const budget = await estimateContextBudget(root)
expect(budget.mcpTools.count).toBeGreaterThan(0)
})
it('includes memory file tokens from CLAUDE.md', async () => {
const root = makeFixtureRoot()
writeFile(join(root, 'CLAUDE.md'), 'Project context for Claude.\n')
const budget = await estimateContextBudget(root)
expect(budget.memory.count).toBeGreaterThan(0)
expect(budget.memory.tokens).toBeGreaterThan(0)
})
})
describe('discoverProjectCwd', () => {
it('returns null for empty directory', async () => {
const root = makeFixtureRoot()
expect(await discoverProjectCwd(root)).toBeNull()
})
it('returns null for directory with no jsonl files', async () => {
const root = makeFixtureRoot()
writeFile(join(root, 'readme.txt'), 'hi')
expect(await discoverProjectCwd(root)).toBeNull()
})
it('extracts cwd from the first jsonl entry', async () => {
const root = makeFixtureRoot()
const entry = JSON.stringify({ type: 'assistant', cwd: '/Users/test/project', timestamp: new Date().toISOString() })
writeFile(join(root, 'session.jsonl'), entry + '\n')
expect(await discoverProjectCwd(root)).toBe('/Users/test/project')
})
})

311
tests/optimize.test.ts Normal file
View file

@ -0,0 +1,311 @@
import { describe, it, expect } from 'vitest'
import {
detectJunkReads,
detectDuplicateReads,
detectLowReadEditRatio,
detectCacheBloat,
detectBloatedClaudeMd,
detectMissingClaudeignore,
computeHealth,
computeTrend,
type ToolCall,
type ApiCallMeta,
type WasteFinding,
} from '../src/optimize.js'
import type { ProjectSummary } from '../src/types.js'
function call(name: string, input: Record<string, unknown>, sessionId = 's1', project = 'p1'): ToolCall {
return { name, input, sessionId, project }
}
function emptyProjects(): ProjectSummary[] {
return []
}
describe('detectJunkReads', () => {
it('returns null below minimum threshold', () => {
const calls = [
call('Read', { file_path: '/x/node_modules/a.js' }),
call('Read', { file_path: '/x/node_modules/b.js' }),
]
expect(detectJunkReads(calls)).toBeNull()
})
it('flags when threshold is met', () => {
const calls = [
call('Read', { file_path: '/x/node_modules/a.js' }),
call('Read', { file_path: '/x/node_modules/b.js' }),
call('Read', { file_path: '/x/.git/config' }),
]
const finding = detectJunkReads(calls)
expect(finding).not.toBeNull()
expect(finding!.impact).toBe('low')
})
it('scales impact with read count', () => {
const make = (n: number) => Array.from({ length: n }, (_, i) =>
call('Read', { file_path: `/x/node_modules/file-${i}.js` })
)
expect(detectJunkReads(make(25))!.impact).toBe('high')
expect(detectJunkReads(make(10))!.impact).toBe('medium')
})
it('ignores non-junk paths', () => {
const calls = [
call('Read', { file_path: '/x/src/a.ts' }),
call('Read', { file_path: '/x/src/b.ts' }),
call('Read', { file_path: '/x/README.md' }),
]
expect(detectJunkReads(calls)).toBeNull()
})
it('ignores non-read tools', () => {
const calls = [
call('Edit', { file_path: '/x/node_modules/a.js' }),
call('Bash', { command: 'ls node_modules' }),
call('Grep', { pattern: 'test', path: '/x/node_modules' }),
]
expect(detectJunkReads(calls)).toBeNull()
})
it('handles missing file_path gracefully', () => {
const calls = [
call('Read', {}),
call('Read', { file_path: null as unknown as string }),
]
expect(detectJunkReads(calls)).toBeNull()
})
it('builds .claudeignore content from detected + common extras', () => {
const calls = Array.from({ length: 5 }, () => call('Read', { file_path: '/x/node_modules/a.js' }))
const finding = detectJunkReads(calls)!
expect(finding.fix.type).toBe('file-content')
if (finding.fix.type === 'file-content') {
expect(finding.fix.content).toContain('node_modules')
}
})
})
describe('detectDuplicateReads', () => {
it('counts same file read multiple times in same session', () => {
const calls = [
...Array.from({ length: 4 }, () => call('Read', { file_path: '/src/a.ts' }, 's1')),
...Array.from({ length: 4 }, () => call('Read', { file_path: '/src/b.ts' }, 's1')),
]
const finding = detectDuplicateReads(calls)
expect(finding).not.toBeNull()
})
it('does not count across sessions', () => {
const calls = [
call('Read', { file_path: '/src/a.ts' }, 's1'),
call('Read', { file_path: '/src/a.ts' }, 's2'),
call('Read', { file_path: '/src/a.ts' }, 's3'),
]
expect(detectDuplicateReads(calls)).toBeNull()
})
it('excludes junk directory reads', () => {
const calls = Array.from({ length: 10 }, () =>
call('Read', { file_path: '/x/node_modules/foo.js' }, 's1')
)
expect(detectDuplicateReads(calls)).toBeNull()
})
it('returns null for single reads', () => {
const calls = [
call('Read', { file_path: '/src/a.ts' }, 's1'),
call('Read', { file_path: '/src/b.ts' }, 's1'),
]
expect(detectDuplicateReads(calls)).toBeNull()
})
})
describe('detectLowReadEditRatio', () => {
it('returns null below minimum edit count', () => {
const calls = [
call('Edit', {}),
call('Edit', {}),
call('Read', {}),
]
expect(detectLowReadEditRatio(calls)).toBeNull()
})
it('returns null when ratio is healthy', () => {
const calls = [
...Array.from({ length: 40 }, () => call('Read', {})),
...Array.from({ length: 10 }, () => call('Edit', {})),
]
expect(detectLowReadEditRatio(calls)).toBeNull()
})
it('flags when edits outpace reads', () => {
const calls = [
...Array.from({ length: 5 }, () => call('Read', {})),
...Array.from({ length: 10 }, () => call('Edit', {})),
]
const finding = detectLowReadEditRatio(calls)
expect(finding).not.toBeNull()
expect(finding!.impact).toBe('high')
})
it('counts Grep and Glob as reads for ratio', () => {
const calls = [
...Array.from({ length: 40 }, () => call('Grep', {})),
...Array.from({ length: 10 }, () => call('Edit', {})),
]
expect(detectLowReadEditRatio(calls)).toBeNull()
})
it('counts Write as edit', () => {
const calls = [
...Array.from({ length: 15 }, () => call('Read', {})),
...Array.from({ length: 10 }, () => call('Write', {})),
]
const finding = detectLowReadEditRatio(calls)
expect(finding).not.toBeNull()
})
})
describe('detectCacheBloat', () => {
it('returns null below minimum api calls', () => {
const apiCalls: ApiCallMeta[] = [
{ cacheCreationTokens: 80000, version: '2.1.100' },
{ cacheCreationTokens: 80000, version: '2.1.100' },
]
expect(detectCacheBloat(apiCalls, emptyProjects())).toBeNull()
})
it('returns null when median is close to baseline', () => {
const apiCalls: ApiCallMeta[] = Array.from({ length: 20 }, () => ({
cacheCreationTokens: 50000,
version: '2.1.98',
}))
expect(detectCacheBloat(apiCalls, emptyProjects())).toBeNull()
})
it('flags when median exceeds 1.4x baseline', () => {
const apiCalls: ApiCallMeta[] = Array.from({ length: 20 }, () => ({
cacheCreationTokens: 80000,
version: '2.1.100',
}))
const finding = detectCacheBloat(apiCalls, emptyProjects())
expect(finding).not.toBeNull()
})
})
describe('detectBloatedClaudeMd', () => {
it('returns null when no projects have CLAUDE.md', () => {
const result = detectBloatedClaudeMd(new Set(['/nonexistent/path']))
expect(result).toBeNull()
})
it('returns null for empty project set', () => {
const result = detectBloatedClaudeMd(new Set())
expect(result).toBeNull()
})
})
describe('detectMissingClaudeignore', () => {
it('returns null for empty set', () => {
expect(detectMissingClaudeignore(new Set())).toBeNull()
})
it('returns null for non-existent cwds', () => {
expect(detectMissingClaudeignore(new Set(['/does/not/exist']))).toBeNull()
})
})
describe('computeHealth', () => {
it('returns A with 100 for no findings', () => {
const { score, grade } = computeHealth([])
expect(score).toBe(100)
expect(grade).toBe('A')
})
function mockFinding(impact: 'high' | 'medium' | 'low'): WasteFinding {
return {
title: 't', explanation: 'e', impact, tokensSaved: 1000,
fix: { type: 'paste', label: 'l', text: 't' },
}
}
it('one low finding stays at A', () => {
const { score, grade } = computeHealth([mockFinding('low')])
expect(score).toBe(97)
expect(grade).toBe('A')
})
it('two high findings drop to C', () => {
const { score, grade } = computeHealth([mockFinding('high'), mockFinding('high')])
expect(score).toBe(70)
expect(grade).toBe('C')
})
it('caps penalty at 80 to prevent score below 20', () => {
const findings = Array.from({ length: 20 }, () => mockFinding('high'))
const { score } = computeHealth(findings)
expect(score).toBe(20)
})
it('progresses grades predictably', () => {
expect(computeHealth([mockFinding('low')]).grade).toBe('A')
expect(computeHealth([mockFinding('medium')]).grade).toBe('A')
expect(computeHealth([mockFinding('medium'), mockFinding('medium')]).grade).toBe('B')
expect(computeHealth([mockFinding('high'), mockFinding('high'), mockFinding('high')]).grade).toBe('C')
expect(computeHealth([mockFinding('high'), mockFinding('high'), mockFinding('high'), mockFinding('high'), mockFinding('high')]).grade).toBe('F')
})
})
describe('computeTrend', () => {
const window = 48 * 60 * 60 * 1000
const baselineWindow = 5 * 24 * 60 * 60 * 1000
it('returns active when no recent activity detected', () => {
const trend = computeTrend({
recentCount: 0, recentWindowMs: window,
baselineCount: 100, baselineWindowMs: baselineWindow,
hasRecentActivity: false,
})
expect(trend).toBe('active')
})
it('returns resolved when recent activity exists but zero waste in it', () => {
const trend = computeTrend({
recentCount: 0, recentWindowMs: window,
baselineCount: 100, baselineWindowMs: baselineWindow,
hasRecentActivity: true,
})
expect(trend).toBe('resolved')
})
it('returns improving when recent rate is less than half of baseline rate', () => {
const trend = computeTrend({
recentCount: 5, recentWindowMs: window,
baselineCount: 100, baselineWindowMs: baselineWindow,
hasRecentActivity: true,
})
expect(trend).toBe('improving')
})
it('returns active when recent rate matches baseline rate', () => {
const recentRate = 100 / baselineWindow
const recentCount = Math.ceil(recentRate * window)
const trend = computeTrend({
recentCount, recentWindowMs: window,
baselineCount: 100, baselineWindowMs: baselineWindow,
hasRecentActivity: true,
})
expect(trend).toBe('active')
})
it('returns active when baseline is empty (new finding)', () => {
const trend = computeTrend({
recentCount: 10, recentWindowMs: window,
baselineCount: 0, baselineWindowMs: baselineWindow,
hasRecentActivity: true,
})
expect(trend).toBe('active')
})
})