Zion Boggan
repos/TreeTrace/src/adapters/gemini.js
zionboggan.com ↗
92 lines · javascript
History for this file →
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
}