Fix Gemini provider for JSONL format (CLI 0.39+)

Gemini CLI 0.39 switched from single JSON to JSONL with one object
per line and $set metadata lines. Parser now handles both formats.
Also updated --provider help text to list all providers.
This commit is contained in:
AgentSeal 2026-04-28 15:03:39 +02:00
parent 220d3193db
commit 64259c929c
2 changed files with 53 additions and 12 deletions

View file

@ -286,7 +286,7 @@ program
.option('-p, --period <period>', 'Starting period: today, week, 30days, month, all', 'week')
.option('--from <date>', 'Start date (YYYY-MM-DD). Overrides --period when set')
.option('--to <date>', 'End date (YYYY-MM-DD). Overrides --period when set')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
@ -364,7 +364,7 @@ program
.command('status')
.description('Compact status output (today + week + month)')
.option('--format <format>', 'Output format: terminal, menubar-json, json', 'terminal')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.option('--period <period>', 'Primary period for menubar-json: today, week, 30days, month, all', 'today')
@ -557,7 +557,7 @@ program
program
.command('today')
.description('Today\'s usage dashboard')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
@ -573,7 +573,7 @@ program
program
.command('month')
.description('This month\'s usage dashboard')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--format <format>', 'Output format: tui, json', 'tui')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
@ -591,7 +591,7 @@ program
.description('Export usage data to CSV or JSON (includes 1 day, 7 days, 30 days)')
.option('-f, --format <format>', 'Export format: csv, json', 'csv')
.option('-o, --output <path>', 'Output file path')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.option('--project <name>', 'Show only projects matching name (repeatable)', collect, [])
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
.action(async (opts) => {
@ -870,7 +870,7 @@ 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')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.action(async (opts) => {
await loadPricing()
const { range, label } = getDateRange(opts.period)
@ -882,7 +882,7 @@ program
.command('compare')
.description('Compare two AI models side-by-side')
.option('-p, --period <period>', 'Analysis period: today, week, 30days, month, all', 'all')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
.action(async (opts) => {
await loadPricing()
const { range } = getDateRange(opts.period)

View file

@ -144,6 +144,40 @@ function parseSession(data: GeminiSession, seenKeys: Set<string>): ParsedProvide
return results
}
function parseJsonl(raw: string): GeminiSession | null {
const lines = raw.split('\n').filter(l => l.trim())
if (lines.length === 0) return null
let sessionId = ''
let startTime = ''
let projectHash: string | undefined
let lastUpdated: string | undefined
let kind: string | undefined
const messages: GeminiMessage[] = []
for (const line of lines) {
let obj: Record<string, unknown>
try {
obj = JSON.parse(line)
} catch {
continue
}
if (obj['$set'] !== undefined) continue
if (obj['sessionId'] && obj['startTime'] && !sessionId) {
sessionId = obj['sessionId'] as string
startTime = obj['startTime'] as string
projectHash = obj['projectHash'] as string | undefined
lastUpdated = obj['lastUpdated'] as string | undefined
kind = obj['kind'] as string | undefined
} else if (obj['id'] && obj['type']) {
messages.push(obj as unknown as GeminiMessage)
}
}
if (!sessionId) return null
return { sessionId, projectHash, startTime, lastUpdated, kind, messages }
}
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
return {
async *parse(): AsyncGenerator<ParsedProviderCall> {
@ -154,14 +188,21 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
return
}
let data: GeminiSession
let data: GeminiSession | null = null
// Try single JSON first (Gemini CLI <=0.38), then JSONL (>=0.39)
try {
data = JSON.parse(raw)
} catch {
return
const parsed = JSON.parse(raw)
if (parsed.messages && parsed.sessionId) {
data = parsed
}
} catch { /* not single JSON */ }
if (!data) {
data = parseJsonl(raw)
}
if (!data.messages || !data.sessionId) return
if (!data?.messages || !data.sessionId) return
const calls = parseSession(data, seenKeys)
for (const call of calls) {