Skip to content
Closed
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
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ function getConfigPath(): string {
export async function readConfig(): Promise<CodeburnConfig> {
try {
const raw = await readFile(getConfigPath(), 'utf-8')
return JSON.parse(raw) as CodeburnConfig
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object') return {}
if (parsed.currency && typeof parsed.currency.code !== 'string') delete parsed.currency
return parsed as CodeburnConfig
} catch {
return {}
}
Expand Down
8 changes: 5 additions & 3 deletions src/day-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ export function aggregateProjectsIntoDays(projects: ProjectSummary[]): DailyEntr

for (const project of projects) {
for (const session of project.sessions) {
const sessionDate = dateKey(session.firstTimestamp)
ensure(sessionDate).sessions += 1
const sessionDate = dateKey(session.firstTimestamp || '')
if (sessionDate) ensure(sessionDate).sessions += 1

for (const turn of session.turns) {
if (turn.assistantCalls.length === 0) continue
const turnDate = dateKey(turn.assistantCalls[0]!.timestamp)
const rawTs = turn.assistantCalls[0]!.timestamp
if (!rawTs) continue
const turnDate = dateKey(rawTs)
const turnDay = ensure(turnDate)

const editTurns = turn.hasEdits ? 1 : 0
Expand Down
6 changes: 3 additions & 3 deletions src/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types
import { getCurrency, convertCost } from './currency.js'

function escCsv(s: string): string {
const sanitized = /^[=+\-@]/.test(s) ? `'${s}` : s
const sanitized = /^[\t\r=+\-@]/.test(s) ? `'${s}` : s
if (sanitized.includes(',') || sanitized.includes('"') || sanitized.includes('\n')) {
return `"${sanitized.replace(/"/g, '""')}"`
}
Expand Down Expand Up @@ -281,7 +281,7 @@ async function clearCodeburnExportFolder(path: string): Promise<void> {
/// wipe a sensitive file (prior versions did `rm(path, { force: true })` unconditionally).
export async function exportCsv(periods: PeriodExport[], outputPath: string): Promise<string> {
const thirtyDays = periods.find(p => p.label === '30 Days')
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1].projects
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1]?.projects ?? []

let folder = resolve(outputPath)
if (folder.toLowerCase().endsWith('.csv')) {
Expand Down Expand Up @@ -323,7 +323,7 @@ export async function exportCsv(periods: PeriodExport[], outputPath: string): Pr

export async function exportJson(periods: PeriodExport[], outputPath: string): Promise<string> {
const thirtyDays = periods.find(p => p.label === '30 Days')
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1].projects
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1]?.projects ?? []
const { code, rate, symbol } = getCurrency()

const data = {
Expand Down
2 changes: 2 additions & 0 deletions src/fs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,7 @@ export async function* readSessionLines(filePath: string): AsyncGenerator<string
for await (const line of rl) yield line
} catch (err) {
warn(`stream read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
} finally {
stream.destroy()
}
}
9 changes: 6 additions & 3 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function getCachePath(): string {
}

function parseLiteLLMEntry(entry: LiteLLMEntry): ModelCosts | null {
if (!entry.input_cost_per_token || !entry.output_cost_per_token) return null
if (entry.input_cost_per_token === undefined || entry.output_cost_per_token === undefined) return null
return {
inputCostPerToken: entry.input_cost_per_token,
outputCostPerToken: entry.output_cost_per_token,
Expand Down Expand Up @@ -140,7 +140,7 @@ export function getModelCosts(model: string): ModelCosts | null {
}

for (const [key, costs] of pricingCache ?? new Map()) {
if (canonical.startsWith(key) || key.startsWith(canonical)) return costs
if (canonical.startsWith(key)) return costs
}

for (const [key, costs] of Object.entries(FALLBACK_PRICING)) {
Expand Down Expand Up @@ -202,7 +202,10 @@ export function getShortModelName(model: string): string {
'o4-mini': 'o4-mini',
'o3': 'o3',
}
for (const [key, name] of Object.entries(shortNames)) {
// Sort by key length descending so longer prefixes match first
// (e.g., 'gpt-4o-mini' before 'gpt-4o')
const sorted = Object.entries(shortNames).sort((a, b) => b[0].length - a[0].length)
for (const [key, name] of sorted) {
if (canonical.startsWith(key)) return name
}
return canonical
Expand Down
4 changes: 2 additions & 2 deletions src/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export function detectDuplicateReads(calls: ToolCall[], dateRange?: DateRange):
totalDuplicates += extra
if (entry.recent > 1) recentDuplicates += entry.recent - 1
const name = basename(file)
fileDupes.set(name, (fileDupes.get(name) ?? 0) + extra)
fileDupes.set(name, (fileDupes.get(name) ?? 0) + entry.count)
}
}

Expand All @@ -461,7 +461,7 @@ export function detectDuplicateReads(calls: ToolCall[], dateRange?: DateRange):
const worst = [...fileDupes.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, TOP_ITEMS_PREVIEW)
.map(([name, n]) => `${name} (${n + 1}x)`)
.map(([name, n]) => `${name} (${n}x)`)
.join(', ')

const tokensSaved = totalDuplicates * AVG_TOKENS_PER_READ
Expand Down
2 changes: 1 addition & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function buildSessionSummary(
const toolBreakdown: SessionSummary['toolBreakdown'] = Object.create(null)
const mcpBreakdown: SessionSummary['mcpBreakdown'] = Object.create(null)
const bashBreakdown: SessionSummary['bashBreakdown'] = Object.create(null)
const categoryBreakdown: SessionSummary['categoryBreakdown'] = {} as SessionSummary['categoryBreakdown']
const categoryBreakdown: SessionSummary['categoryBreakdown'] = Object.create(null) as SessionSummary['categoryBreakdown']

let totalCost = 0
let totalInput = 0
Expand Down
13 changes: 8 additions & 5 deletions src/providers/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const modelDisplayNames: Record<string, string> = {
'gpt-4o': 'GPT-4o',
}

// Sort by key length descending so longer prefixes match first
const modelDisplayEntries = Object.entries(modelDisplayNames).sort((a, b) => b[0].length - a[0].length)

const toolNameMap: Record<string, string> = {
exec_command: 'Bash',
read_file: 'Read',
Expand Down Expand Up @@ -205,10 +208,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
} else if (cumulativeTotal > 0) {
const total = info.total_token_usage
if (!total) continue
inputTokens = (total.input_tokens ?? 0) - prevInput
cachedInputTokens = (total.cached_input_tokens ?? 0) - prevCached
outputTokens = (total.output_tokens ?? 0) - prevOutput
reasoningTokens = (total.reasoning_output_tokens ?? 0) - prevReasoning
inputTokens = Math.max(0, (total.input_tokens ?? 0) - prevInput)
cachedInputTokens = Math.max(0, (total.cached_input_tokens ?? 0) - prevCached)
outputTokens = Math.max(0, (total.output_tokens ?? 0) - prevOutput)
reasoningTokens = Math.max(0, (total.reasoning_output_tokens ?? 0) - prevReasoning)
}

if (!last) {
Expand Down Expand Up @@ -280,7 +283,7 @@ export function createCodexProvider(codexDir?: string): Provider {
displayName: 'Codex',

modelDisplayName(model: string): string {
for (const [key, name] of Object.entries(modelDisplayNames)) {
for (const [key, name] of modelDisplayEntries) {
if (model.startsWith(key)) return name
}
return model
Expand Down