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