diff --git a/src/config.ts b/src/config.ts index 19ad4561..f916b0ec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,7 +20,10 @@ function getConfigPath(): string { export async function readConfig(): Promise { 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 {} } diff --git a/src/day-aggregator.ts b/src/day-aggregator.ts index 5030f8d8..79cd5c64 100644 --- a/src/day-aggregator.ts +++ b/src/day-aggregator.ts @@ -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 diff --git a/src/export.ts b/src/export.ts index d07f08d8..1ac34816 100644 --- a/src/export.ts +++ b/src/export.ts @@ -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, '""')}"` } @@ -281,7 +281,7 @@ async function clearCodeburnExportFolder(path: string): Promise { /// wipe a sensitive file (prior versions did `rm(path, { force: true })` unconditionally). export async function exportCsv(periods: PeriodExport[], outputPath: string): Promise { 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')) { @@ -323,7 +323,7 @@ export async function exportCsv(periods: PeriodExport[], outputPath: string): Pr export async function exportJson(periods: PeriodExport[], outputPath: string): Promise { 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 = { diff --git a/src/fs-utils.ts b/src/fs-utils.ts index 49eff20b..823a630a 100644 --- a/src/fs-utils.ts +++ b/src/fs-utils.ts @@ -89,5 +89,7 @@ export async function* readSessionLines(filePath: string): AsyncGenerator b[0].length - a[0].length) + for (const [key, name] of sorted) { if (canonical.startsWith(key)) return name } return canonical diff --git a/src/optimize.ts b/src/optimize.ts index 2e8913c7..748e0c5f 100644 --- a/src/optimize.ts +++ b/src/optimize.ts @@ -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) } } @@ -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 diff --git a/src/parser.ts b/src/parser.ts index 4adcf3bc..fcb2fabc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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 diff --git a/src/providers/codex.ts b/src/providers/codex.ts index 01d48b70..eb3f836b 100644 --- a/src/providers/codex.ts +++ b/src/providers/codex.ts @@ -15,6 +15,9 @@ const modelDisplayNames: Record = { '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 = { exec_command: 'Bash', read_file: 'Read', @@ -205,10 +208,10 @@ function createParser(source: SessionSource, seenKeys: Set): 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) { @@ -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