1use anyhow::Result;
4use rusqlite::{params, Connection};
5
6use crate::commands::swarm::transcript::{MessageContent, Transcript};
7
8#[derive(Debug)]
9pub struct TranscriptSearchResult {
10 pub session_id: String,
11 pub task_id: Option<String>,
12 pub timestamp: String,
13 pub role: String,
14 pub content_preview: String,
15}
16
17#[derive(Debug)]
18pub struct TranscriptStats {
19 pub total_messages: i64,
20 pub total_tool_calls: i64,
21 pub total_sessions: i64,
22}
23
24pub fn insert_transcript(
26 conn: &Connection,
27 transcript: &Transcript,
28 scud_session_id: Option<&str>,
29 task_id: Option<&str>,
30) -> Result<()> {
31 let tx = conn.unchecked_transaction()?;
32
33 for msg in &transcript.messages {
34 let content = match &msg.content {
35 MessageContent::Text(t) => t.clone(),
36 MessageContent::Structured(s) => serde_json::to_string(s)?,
37 };
38
39 let (model, input_tokens, output_tokens) = match &msg.content {
41 MessageContent::Structured(s) => (
42 s.model.clone(),
43 s.usage
44 .as_ref()
45 .and_then(|u| u.input_tokens)
46 .map(|t| t as i64),
47 s.usage
48 .as_ref()
49 .and_then(|u| u.output_tokens)
50 .map(|t| t as i64),
51 ),
52 _ => (None, None, None),
53 };
54
55 tx.execute(
56 "INSERT INTO transcript_messages
57 (claude_session_id, scud_session_id, task_id, timestamp, uuid, parent_uuid,
58 role, content, model, input_tokens, output_tokens)
59 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
60 params![
61 transcript.session_id,
62 scud_session_id,
63 task_id,
64 msg.timestamp.to_rfc3339(),
65 msg.uuid,
66 msg.parent_uuid,
67 msg.role,
68 content,
69 model,
70 input_tokens,
71 output_tokens,
72 ],
73 )?;
74 let message_id = tx.last_insert_rowid();
75
76 for tool_call in &transcript.tool_calls {
78 if tool_call.timestamp == msg.timestamp {
79 tx.execute(
80 "INSERT INTO tool_calls
81 (message_id, claude_session_id, timestamp, tool_id, tool_name, input_json)
82 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
83 params![
84 message_id,
85 transcript.session_id,
86 tool_call.timestamp.to_rfc3339(),
87 tool_call.id,
88 tool_call.name,
89 tool_call.input.to_string(),
90 ],
91 )?;
92 }
93 }
94
95 for tool_result in &transcript.tool_results {
97 if tool_result.timestamp == msg.timestamp {
98 tx.execute(
99 "INSERT INTO tool_results
100 (message_id, claude_session_id, timestamp, tool_use_id, content, is_error)
101 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
102 params![
103 message_id,
104 transcript.session_id,
105 tool_result.timestamp.to_rfc3339(),
106 tool_result.tool_use_id,
107 tool_result.content,
108 tool_result.is_error as i32,
109 ],
110 )?;
111 }
112 }
113 }
114
115 tx.commit()?;
116 Ok(())
117}
118
119pub fn search_transcripts(conn: &Connection, query: &str) -> Result<Vec<TranscriptSearchResult>> {
120 let mut stmt = conn.prepare(
121 "SELECT tm.claude_session_id, tm.task_id, tm.timestamp, tm.role,
122 substr(tm.content, 1, 200) as content_preview
123 FROM transcript_messages tm
124 WHERE tm.content LIKE ?1
125 ORDER BY tm.timestamp DESC
126 LIMIT 100",
127 )?;
128
129 let pattern = format!("%{}%", query);
130 let results = stmt.query_map(params![pattern], |row| {
131 Ok(TranscriptSearchResult {
132 session_id: row.get(0)?,
133 task_id: row.get(1)?,
134 timestamp: row.get(2)?,
135 role: row.get(3)?,
136 content_preview: row.get(4)?,
137 })
138 })?;
139
140 results.collect::<Result<Vec<_>, _>>().map_err(Into::into)
141}
142
143pub fn get_transcript_stats(conn: &Connection) -> Result<TranscriptStats> {
144 let total_messages: i64 =
145 conn.query_row("SELECT COUNT(*) FROM transcript_messages", [], |r| r.get(0))?;
146 let total_tool_calls: i64 =
147 conn.query_row("SELECT COUNT(*) FROM tool_calls", [], |r| r.get(0))?;
148 let total_sessions: i64 = conn.query_row(
149 "SELECT COUNT(DISTINCT claude_session_id) FROM transcript_messages",
150 [],
151 |r| r.get(0),
152 )?;
153
154 Ok(TranscriptStats {
155 total_messages,
156 total_tool_calls,
157 total_sessions,
158 })
159}
160
161pub fn list_transcript_sessions(conn: &Connection) -> Result<Vec<TranscriptSessionInfo>> {
163 let mut stmt = conn.prepare(
164 "SELECT claude_session_id,
165 COUNT(*) as msg_count,
166 MIN(timestamp) as first_msg,
167 MAX(timestamp) as last_msg
168 FROM transcript_messages
169 GROUP BY claude_session_id
170 ORDER BY first_msg DESC
171 LIMIT 50",
172 )?;
173
174 let results = stmt.query_map([], |row| {
175 Ok(TranscriptSessionInfo {
176 session_id: row.get(0)?,
177 message_count: row.get(1)?,
178 first_message: row.get(2)?,
179 last_message: row.get(3)?,
180 })
181 })?;
182
183 results.collect::<Result<Vec<_>, _>>().map_err(Into::into)
184}
185
186#[derive(Debug)]
187pub struct TranscriptSessionInfo {
188 pub session_id: String,
189 pub message_count: i64,
190 pub first_message: String,
191 pub last_message: String,
192}