normalize_chat_sessions/formats/
gemini_cli.rs1use super::{LogFormat, SessionFile, read_file};
4use crate::{ContentBlock, Message, Role, Session, TokenUsage, Turn};
5use serde_json::Value;
6use std::path::{Path, PathBuf};
7
8pub struct GeminiCliFormat;
10
11impl LogFormat for GeminiCliFormat {
12 fn name(&self) -> &'static str {
13 "gemini"
14 }
15
16 fn sessions_dir(&self, _project: Option<&Path>) -> PathBuf {
17 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
18 PathBuf::from(home).join(".gemini/tmp")
19 }
20
21 fn list_sessions(&self, project: Option<&Path>) -> Vec<SessionFile> {
22 let dir = self.sessions_dir(project);
23 let mut sessions = Vec::new();
25 if let Ok(entries) = std::fs::read_dir(&dir) {
26 for entry in entries.filter_map(|e| e.ok()) {
27 let subdir = entry.path();
28 if !subdir.is_dir() {
29 continue;
30 }
31 let logs_path = subdir.join("logs.json");
32 if logs_path.exists() {
33 if let Ok(meta) = logs_path.metadata() {
34 if let Ok(mtime) = meta.modified() {
35 sessions.push(SessionFile {
36 path: logs_path,
37 mtime,
38 });
39 }
40 }
41 }
42 }
43 }
44 sessions
45 }
46
47 fn detect(&self, path: &Path) -> f64 {
48 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
49 if ext != "json" {
50 return 0.0;
51 }
52
53 let Ok(content) = read_file(path) else {
55 return 0.0;
56 };
57
58 let Ok(data) = serde_json::from_str::<Value>(&content) else {
59 return 0.0;
60 };
61
62 if data.get("sessionId").is_some() && data.get("messages").is_some() {
64 if let Some(messages) = data.get("messages").and_then(|m| m.as_array()) {
65 for msg in messages {
66 if msg.get("type").and_then(|t| t.as_str()) == Some("gemini") {
67 return 1.0;
68 }
69 }
70 }
71 return 0.5; }
73
74 0.0
75 }
76
77 fn parse(&self, path: &Path) -> Result<Session, String> {
78 let content = read_file(path)?;
79 let data: Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
80
81 let mut session = Session::new(path.to_path_buf(), self.name());
82
83 session.metadata.session_id = data
85 .get("sessionId")
86 .and_then(|v| v.as_str())
87 .map(String::from);
88 session.metadata.provider = Some("google".to_string());
89
90 let messages = data
91 .get("messages")
92 .and_then(|m| m.as_array())
93 .cloned()
94 .unwrap_or_default();
95
96 let mut current_turn = Turn::default();
97
98 for msg in &messages {
99 let msg_type = msg.get("type").and_then(|t| t.as_str()).unwrap_or("");
100
101 match msg_type {
102 "user" => {
103 if !current_turn.messages.is_empty() {
105 session.turns.push(std::mem::take(&mut current_turn));
106 }
107
108 let message = parse_user_message(msg);
109 current_turn.messages.push(message);
110 }
111 "gemini" => {
112 if session.metadata.model.is_none() {
114 session.metadata.model =
115 msg.get("model").and_then(|v| v.as_str()).map(String::from);
116 }
117
118 let message = parse_gemini_message(msg);
119 current_turn.messages.push(message);
120
121 if let Some(tokens) = msg.get("tokens") {
123 current_turn.token_usage = Some(TokenUsage {
124 input: tokens.get("input").and_then(|v| v.as_u64()).unwrap_or(0),
125 output: tokens.get("output").and_then(|v| v.as_u64()).unwrap_or(0),
126 cache_read: tokens.get("cached").and_then(|v| v.as_u64()),
127 cache_create: None,
128 });
129 }
130 }
131 _ => {}
132 }
133 }
134
135 if !current_turn.messages.is_empty() {
137 session.turns.push(current_turn);
138 }
139
140 Ok(session)
141 }
142}
143
144fn parse_user_message(msg: &Value) -> Message {
146 let mut content = Vec::new();
147
148 if let Some(text) = msg.get("content").and_then(|v| v.as_str()) {
149 content.push(ContentBlock::Text {
150 text: text.to_string(),
151 });
152 }
153
154 Message {
155 role: Role::User,
156 content,
157 timestamp: msg
158 .get("timestamp")
159 .and_then(|v| v.as_str())
160 .map(String::from),
161 }
162}
163
164fn parse_gemini_message(msg: &Value) -> Message {
166 let mut content = Vec::new();
167
168 if let Some(text) = msg.get("content").and_then(|v| v.as_str()) {
170 content.push(ContentBlock::Text {
171 text: text.to_string(),
172 });
173 }
174
175 if let Some(tool_calls) = msg.get("toolCalls").and_then(|t| t.as_array()) {
177 for tc in tool_calls {
178 let id = tc
179 .get("id")
180 .and_then(|v| v.as_str())
181 .unwrap_or("")
182 .to_string();
183 let name = tc
184 .get("name")
185 .and_then(|v| v.as_str())
186 .unwrap_or("")
187 .to_string();
188 let input = tc.get("args").cloned().unwrap_or(Value::Null);
189
190 content.push(ContentBlock::ToolUse {
191 id: id.clone(),
192 name,
193 input,
194 });
195
196 if let Some(result) = tc.get("result") {
198 let tool_use_id = id;
199 let result_content = if let Some(s) = result.as_str() {
200 s.to_string()
201 } else {
202 result.to_string()
203 };
204 let is_error = tc.get("status").and_then(|s| s.as_str()) == Some("error");
205 content.push(ContentBlock::ToolResult {
206 tool_use_id,
207 content: result_content,
208 is_error,
209 });
210 }
211 }
212 }
213
214 Message {
215 role: Role::Assistant,
216 content,
217 timestamp: msg
218 .get("timestamp")
219 .and_then(|v| v.as_str())
220 .map(String::from),
221 }
222}