mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-07 07:34:39 +00:00
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).
81 lines
2.8 KiB
TypeScript
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')
|
|
}
|