codeburn/src/sessions-report.ts
iamtoruk fc1920d4f1 feat(cli): add compare and sessions JSON emitters
compare --format json: list mode (aggregateModelStats for pickers) +
full mode via --model-a/--model-b, emitting
{period, modelA, modelB, metrics, categories, workingStyle}.
Reuses computeComparison/computeCategoryComparison/computeWorkingStyle/
scanSelfCorrections; TUI path unchanged.

sessions --format json: new src/sessions-report.ts, uncapped flat
SessionRow[] (id, project, provider, models, cost, tokens, turns,
duration) via aggregateSessions; table + json formats.

Enables the desktop app's Sessions + Compare screens (Phase 3b).
2026-07-11 12:16:45 -07:00

81 lines
2.8 KiB
TypeScript

import type { ProjectSummary, SessionSummary } from './types.js'
export type SessionRow = {
sessionId: string
project: string
provider: string
models: string[]
cost: number
savingsUSD: number
calls: number
turns: number
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
startedAt: string
endedAt: string
durationMs: number
}
function inferProvider(session: SessionSummary): string {
for (const turn of session.turns) {
const provider = turn.assistantCalls[0]?.provider
if (provider) return provider
}
const models = Object.keys(session.modelBreakdown)
const model = models[0]?.toLowerCase() ?? ''
if (model.startsWith('claude')) return 'claude'
if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3') || model.startsWith('o4')) return 'codex'
if (model.startsWith('gemini')) return 'gemini'
if (model.includes('/')) return model.split('/', 1)[0] || 'unknown'
return 'unknown'
}
function durationMs(startedAt: string, endedAt: string): number {
const duration = new Date(endedAt).getTime() - new Date(startedAt).getTime()
return Number.isFinite(duration) ? duration : 0
}
export function aggregateSessions(projects: ProjectSummary[]): SessionRow[] {
return projects.flatMap(project => project.sessions.map(session => ({
sessionId: session.sessionId,
project: session.project || project.project,
provider: inferProvider(session),
models: Object.keys(session.modelBreakdown),
cost: session.totalCostUSD,
savingsUSD: session.totalSavingsUSD,
calls: session.apiCalls,
turns: session.turns.length,
inputTokens: session.totalInputTokens,
outputTokens: session.totalOutputTokens,
cacheReadTokens: session.totalCacheReadTokens,
cacheWriteTokens: session.totalCacheWriteTokens,
startedAt: session.firstTimestamp,
endedAt: session.lastTimestamp,
durationMs: durationMs(session.firstTimestamp, session.lastTimestamp),
})))
}
export function renderJson(rows: SessionRow[]): string {
return JSON.stringify(rows, null, 2)
}
export function renderTable(rows: SessionRow[]): string {
const headers = ['SESSION', 'PROJECT', 'PROVIDER', 'MODELS', 'COST', 'SAVED', 'CALLS', 'TURNS', 'STARTED']
const values = rows.map(row => [
row.sessionId,
row.project,
row.provider,
row.models.join(', '),
`$${row.cost.toFixed(2)}`,
`$${row.savingsUSD.toFixed(2)}`,
String(row.calls),
String(row.turns),
row.startedAt,
])
const widths = headers.map((header, i) => Math.max(header.length, ...values.map(row => row[i]!.length)))
const format = (row: string[]) => row.map((value, i) => value.padEnd(widths[i]!)).join(' ').trimEnd()
return [format(headers), format(widths.map(width => '-'.repeat(width))), ...values.map(format)].join('\n')
}