normalize_chat_sessions/formats/
gemini_cli.rs1use super::{LogFormat, ParseError, 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 && let Ok(meta) = logs_path.metadata()
34 && let Ok(mtime) = meta.modified()
35 {
36 sessions.push(SessionFile {
37 path: logs_path,
38 mtime,
39 parent_id: None,
40 agent_id: None,
41 subagent_type: Some("interactive".into()),
42 });
43 }
44 }
45 }
46 sessions
47 }
48
49 fn detect(&self, path: &Path) -> f64 {
50 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
51 if ext != "json" {
52 return 0.0;
53 }
54
55 let Ok(content) = read_file(path) else {
57 return 0.0;
58 };
59
60 let Ok(data) = serde_json::from_str::<Value>(&content) else {
61 return 0.0;
62 };
63
64 if data.get("sessionId").is_some() && data.get("messages").is_some() {
66 if let Some(messages) = data.get("messages").and_then(|m| m.as_array()) {
67 for msg in messages {
68 if msg.get("type").and_then(|t| t.as_str()) == Some("gemini") {
69 return 1.0;
70 }
71 }
72 }
73 return 0.5; }
75
76 0.0
77 }
78
79 fn parse(&self, path: &Path) -> Result<Session, ParseError> {
80 let content = read_file(path)?;
81 let data: Value = serde_json::from_str(&content).map_err(|e| ParseError::Format {
82 path: path.to_path_buf(),
83 message: e.to_string(),
84 })?;
85
86 let mut session = Session::new(path.to_path_buf(), self.name());
87 session.subagent_type = Some("interactive".into());
88
89 session.metadata.session_id = data
91 .get("sessionId")
92 .and_then(|v| v.as_str())
93 .map(String::from);
94 session.metadata.provider = Some("google".to_string());
95
96 let messages = data
97 .get("messages")
98 .and_then(|m| m.as_array())
99 .cloned()
100 .unwrap_or_default();
101
102 let mut current_turn = Turn::default();
103
104 for msg in &messages {
105 let msg_type = msg.get("type").and_then(|t| t.as_str()).unwrap_or("");
106
107 match msg_type {
108 "user" => {
109 if !current_turn.messages.is_empty() {
111 session.turns.push(std::mem::take(&mut current_turn));
112 }
113
114 let message = parse_user_message(msg);
115 current_turn.messages.push(message);
116 }
117 "gemini" => {
118 if session.metadata.model.is_none() {
120 session.metadata.model =
121 msg.get("model").and_then(|v| v.as_str()).map(String::from);
122 }
123
124 let message = parse_gemini_message(msg);
125 current_turn.messages.push(message);
126
127 if let Some(tokens) = msg.get("tokens") {
129 current_turn.token_usage = Some(TokenUsage {
130 input: tokens.get("input").and_then(|v| v.as_u64()).unwrap_or(0),
131 output: tokens.get("output").and_then(|v| v.as_u64()).unwrap_or(0),
132 cache_read: tokens.get("cached").and_then(|v| v.as_u64()),
133 cache_create: None,
134 model: msg.get("model").and_then(|v| v.as_str()).map(String::from),
135 });
136 }
137 }
138 _ => {}
139 }
140 }
141
142 if !current_turn.messages.is_empty() {
144 session.turns.push(current_turn);
145 }
146
147 Ok(session)
148 }
149}
150
151fn parse_user_message(msg: &Value) -> Message {
153 let mut content = Vec::new();
154
155 if let Some(text) = msg.get("content").and_then(|v| v.as_str()) {
156 content.push(ContentBlock::Text {
157 text: text.to_string(),
158 });
159 }
160
161 Message {
162 role: Role::User,
163 content,
164 timestamp: msg
165 .get("timestamp")
166 .and_then(|v| v.as_str())
167 .map(String::from),
168 }
169}
170
171fn parse_gemini_message(msg: &Value) -> Message {
173 let mut content = Vec::new();
174
175 if let Some(text) = msg.get("content").and_then(|v| v.as_str()) {
177 content.push(ContentBlock::Text {
178 text: text.to_string(),
179 });
180 }
181
182 if let Some(tool_calls) = msg.get("toolCalls").and_then(|t| t.as_array()) {
184 for tc in tool_calls {
185 let id = tc
186 .get("id")
187 .and_then(|v| v.as_str())
188 .unwrap_or("")
189 .to_string();
190 let name = tc
191 .get("name")
192 .and_then(|v| v.as_str())
193 .unwrap_or("")
194 .to_string();
195 let input = tc.get("args").cloned().unwrap_or(Value::Null);
196
197 content.push(ContentBlock::ToolUse {
198 id: id.clone(),
199 name,
200 input,
201 });
202
203 if let Some(result) = tc.get("result") {
205 let result_content = if let Some(s) = result.as_str() {
206 s.to_string()
207 } else {
208 result.to_string()
209 };
210 let is_error = tc.get("status").and_then(|s| s.as_str()) == Some("error");
211 content.push(ContentBlock::ToolResult {
212 tool_use_id: id,
213 content: result_content,
214 is_error,
215 });
216 }
217 }
218 }
219
220 Message {
221 role: Role::Assistant,
222 content,
223 timestamp: msg
224 .get("timestamp")
225 .and_then(|v| v.as_str())
226 .map(String::from),
227 }
228}