| 1 | import { |
| 2 | newSession, |
| 3 | finalizeSession, |
| 4 | pushTurn, |
| 5 | addAction, |
| 6 | addThinking, |
| 7 | flattenParts, |
| 8 | looksSynthetic, |
| 9 | noteAssistantRefusal, |
| 10 | readJsonl, |
| 11 | } from './shared.js'; |
| 12 | |
| 13 | function partsToText(content) { |
| 14 | if (typeof content === 'string') return content; |
| 15 | if (Array.isArray(content)) { |
| 16 | const out = []; |
| 17 | for (const part of content) { |
| 18 | if (typeof part === 'string') out.push(part); |
| 19 | else if (part && typeof part.text === 'string') out.push(part.text); |
| 20 | } |
| 21 | return out.join('\n'); |
| 22 | } |
| 23 | return flattenParts(content); |
| 24 | } |
| 25 | |
| 26 | function ingestRecord(session, rec, counters) { |
| 27 | const type = rec.type || rec.role; |
| 28 | const ts = rec.timestamp || null; |
| 29 | if (type === 'user') { |
| 30 | const text = partsToText(rec.content); |
| 31 | if (looksSynthetic(text)) return; |
| 32 | pushTurn(session, ++counters.turn, text, ts); |
| 33 | } else if (type === 'gemini' || type === 'model' || type === 'assistant') { |
| 34 | session.stats.assistantLines++; |
| 35 | if (rec.model) session.stats.models.add(rec.model); |
| 36 | noteAssistantRefusal(session, partsToText(rec.content)); |
| 37 | if (Array.isArray(rec.toolCalls)) { |
| 38 | for (const call of rec.toolCalls) { |
| 39 | session.stats.toolUses++; |
| 40 | const file = call && call.args && (call.args.file_path || call.args.path || call.args.absolute_path); |
| 41 | if (typeof file === 'string') session.stats.filesTouched.add(file); |
| 42 | addAction(session, { |
| 43 | tool: (call && call.name) || null, |
| 44 | file: typeof file === 'string' ? file : null, |
| 45 | command: call && call.args && typeof call.args.command === 'string' ? call.args.command : null, |
| 46 | model: rec.model || null, |
| 47 | }); |
| 48 | } |
| 49 | } |
| 50 | if (Array.isArray(rec.thoughts) && rec.thoughts.length) addThinking(session, rec.thoughts.length); |
| 51 | if (rec.tokens) { |
| 52 | session.stats.inputTokens += rec.tokens.prompt || rec.tokens.input || 0; |
| 53 | session.stats.outputTokens += rec.tokens.candidate || rec.tokens.output || 0; |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | export function detectGemini(text) { |
| 59 | for (const line of text.split(/\r?\n/)) { |
| 60 | const trimmed = line.trim(); |
| 61 | if (!trimmed || trimmed.charCodeAt(0) !== 123) continue; |
| 62 | try { |
| 63 | const rec = JSON.parse(trimmed); |
| 64 | if ((rec.type === 'user' || rec.type === 'gemini') && 'content' in rec) return true; |
| 65 | return false; |
| 66 | } catch { |
| 67 | return false; |
| 68 | } |
| 69 | } |
| 70 | return false; |
| 71 | } |
| 72 | |
| 73 | export function detectGeminiJson(parsed) { |
| 74 | if (!parsed || typeof parsed !== 'object') return false; |
| 75 | if (!Array.isArray(parsed.messages)) return false; |
| 76 | return parsed.messages.some((m) => m && (m.type === 'gemini' || m.type === 'user') && 'content' in m); |
| 77 | } |
| 78 | |
| 79 | export function parseGemini(text, path, sessionId) { |
| 80 | const session = newSession(path, sessionId); |
| 81 | const counters = { turn: 0 }; |
| 82 | for (const rec of readJsonl(text)) ingestRecord(session, rec, counters); |
| 83 | return finalizeSession(session); |
| 84 | } |
| 85 | |
| 86 | export function parseGeminiJson(parsed, path, sessionId) { |
| 87 | const session = newSession(path, parsed.sessionId || sessionId); |
| 88 | if (parsed.model) session.stats.models.add(parsed.model); |
| 89 | const counters = { turn: 0 }; |
| 90 | for (const rec of parsed.messages) ingestRecord(session, rec, counters); |
| 91 | return finalizeSession(session); |
| 92 | } |