From 64259c929c949672ffe84bfcf7810290a71a571a Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Tue, 28 Apr 2026 15:03:39 +0200 Subject: [PATCH] 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. --- src/cli.ts | 14 +++++------ src/providers/gemini.ts | 51 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2dd1fd8..0f37829 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -286,7 +286,7 @@ program .option('-p, --period ', 'Starting period: today, week, 30days, month, all', 'week') .option('--from ', 'Start date (YYYY-MM-DD). Overrides --period when set') .option('--to ', 'End date (YYYY-MM-DD). Overrides --period when set') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--format ', 'Output format: tui, json', 'tui') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) @@ -364,7 +364,7 @@ program .command('status') .description('Compact status output (today + week + month)') .option('--format ', 'Output format: terminal, menubar-json, json', 'terminal') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) .option('--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 ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--format ', 'Output format: tui, json', 'tui') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, []) @@ -573,7 +573,7 @@ program program .command('month') .description('This month\'s usage dashboard') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--format ', 'Output format: tui, json', 'tui') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', '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 ', 'Export format: csv, json', 'csv') .option('-o, --output ', 'Output file path') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .option('--project ', 'Show only projects matching name (repeatable)', collect, []) .option('--exclude ', '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 ', 'Analysis period: today, week, 30days, month, all', '30days') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--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 ', 'Analysis period: today, week, 30days, month, all', 'all') - .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all') + .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all') .action(async (opts) => { await loadPricing() const { range } = getDateRange(opts.period) diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 83903db..48b3107 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -144,6 +144,40 @@ function parseSession(data: GeminiSession, seenKeys: Set): 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 + 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): SessionParser { return { async *parse(): AsyncGenerator { @@ -154,14 +188,21 @@ function createParser(source: SessionSource, seenKeys: Set): 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) {