1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::path::PathBuf;
11use tokio::fs;
12use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
13
14use crate::activations::arbor::{ArborStorage, TreeId};
15use crate::activations::claudecode::types::NodeEvent;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "kebab-case")]
24pub enum SessionEvent {
25 #[serde(rename = "user")]
27 User {
28 #[serde(flatten)]
29 data: UserEvent,
30 },
31 #[serde(rename = "assistant")]
33 Assistant {
34 #[serde(flatten)]
35 data: AssistantEvent,
36 },
37 #[serde(rename = "system")]
39 System {
40 #[serde(flatten)]
41 data: SystemEvent,
42 },
43 #[serde(rename = "file-history-snapshot")]
45 FileHistorySnapshot {
46 timestamp: Option<String>,
47 },
48 #[serde(rename = "queue-operation")]
50 QueueOperation {
51 operation: String,
52 timestamp: String,
53 #[serde(rename = "sessionId")]
54 session_id: String,
55 },
56 #[serde(other)]
58 Unknown,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct UserEvent {
63 pub uuid: String,
64 #[serde(rename = "parentUuid")]
65 pub parent_uuid: Option<String>,
66 #[serde(rename = "sessionId")]
67 pub session_id: String,
68 pub timestamp: String,
69 pub cwd: String,
70 pub message: UserMessage,
71 #[serde(rename = "isSidechain")]
72 pub is_sidechain: Option<bool>,
73 #[serde(rename = "gitBranch")]
74 pub git_branch: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct UserMessage {
79 pub role: String,
80 pub content: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct AssistantEvent {
85 pub uuid: String,
86 #[serde(rename = "parentUuid")]
87 pub parent_uuid: Option<String>,
88 #[serde(rename = "sessionId")]
89 pub session_id: String,
90 pub timestamp: String,
91 pub cwd: Option<String>,
92 pub message: AssistantMessage,
93 #[serde(rename = "requestId")]
94 pub request_id: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum AssistantMessage {
100 Full {
102 role: String,
103 content: Vec<ContentBlock>,
104 model: Option<String>,
105 id: Option<String>,
106 #[serde(rename = "stop_reason")]
107 stop_reason: Option<String>,
108 usage: Option<Value>,
109 },
110 Simple(String),
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(tag = "type", rename_all = "snake_case")]
116pub enum ContentBlock {
117 Text { text: String },
118 ToolUse {
119 id: String,
120 name: String,
121 input: Value,
122 },
123 ToolResult {
124 #[serde(rename = "tool_use_id")]
125 tool_use_id: String,
126 content: String,
127 #[serde(rename = "is_error")]
128 is_error: Option<bool>,
129 },
130 Thinking {
131 thinking: String,
132 signature: Option<String>,
133 },
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct SystemEvent {
138 pub uuid: String,
139 #[serde(rename = "sessionId")]
140 pub session_id: String,
141 pub timestamp: String,
142 pub message: String,
143}
144
145pub fn get_sessions_base_dir() -> PathBuf {
151 dirs::home_dir()
152 .expect("Could not determine home directory")
153 .join(".claude")
154 .join("projects")
155}
156
157pub fn get_session_path(project_path: &str, session_id: &str) -> PathBuf {
159 get_sessions_base_dir()
160 .join(project_path)
161 .join(format!("{}.jsonl", session_id))
162}
163
164pub async fn list_sessions(project_path: &str) -> Result<Vec<String>, String> {
166 let dir = get_sessions_base_dir().join(project_path);
167
168 if !dir.exists() {
169 return Ok(vec![]);
170 }
171
172 let mut sessions = vec![];
173 let mut entries = fs::read_dir(&dir)
174 .await
175 .map_err(|e| format!("Failed to read directory: {}", e))?;
176
177 while let Some(entry) = entries
178 .next_entry()
179 .await
180 .map_err(|e| format!("Failed to read entry: {}", e))?
181 {
182 let path = entry.path();
183 if path.extension().and_then(|s| s.to_str()) == Some("jsonl") {
184 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
185 sessions.push(stem.to_string());
186 }
187 }
188 }
189
190 Ok(sessions)
191}
192
193pub async fn read_session(
195 project_path: &str,
196 session_id: &str,
197) -> Result<Vec<SessionEvent>, String> {
198 let path = get_session_path(project_path, session_id);
199
200 if !path.exists() {
201 return Err(format!("Session not found: {}", session_id));
202 }
203
204 let file = fs::File::open(&path)
205 .await
206 .map_err(|e| format!("Failed to open session file: {}", e))?;
207
208 let reader = BufReader::new(file);
209 let mut lines = reader.lines();
210 let mut events = vec![];
211
212 while let Some(line) = lines
213 .next_line()
214 .await
215 .map_err(|e| format!("Failed to read line: {}", e))?
216 {
217 if line.trim().is_empty() {
218 continue;
219 }
220
221 match serde_json::from_str::<SessionEvent>(&line) {
222 Ok(event) => events.push(event),
223 Err(e) => {
224 eprintln!("Warning: Failed to parse event: {} - {}", e, &line[..line.len().min(100)]);
225 }
227 }
228 }
229
230 Ok(events)
231}
232
233pub async fn append_to_session(
235 project_path: &str,
236 session_id: &str,
237 event: &SessionEvent,
238) -> Result<(), String> {
239 let path = get_session_path(project_path, session_id);
240
241 if let Some(parent) = path.parent() {
243 fs::create_dir_all(parent)
244 .await
245 .map_err(|e| format!("Failed to create directory: {}", e))?;
246 }
247
248 let json = serde_json::to_string(event).map_err(|e| format!("Failed to serialize event: {}", e))?;
249
250 let mut file = fs::OpenOptions::new()
251 .create(true)
252 .append(true)
253 .open(&path)
254 .await
255 .map_err(|e| format!("Failed to open session file: {}", e))?;
256
257 file.write_all(json.as_bytes())
258 .await
259 .map_err(|e| format!("Failed to write to session: {}", e))?;
260 file.write_all(b"\n")
261 .await
262 .map_err(|e| format!("Failed to write newline: {}", e))?;
263
264 Ok(())
265}
266
267pub async fn delete_session(project_path: &str, session_id: &str) -> Result<(), String> {
269 let path = get_session_path(project_path, session_id);
270
271 if !path.exists() {
272 return Err(format!("Session not found: {}", session_id));
273 }
274
275 fs::remove_file(&path)
276 .await
277 .map_err(|e| format!("Failed to delete session: {}", e))?;
278
279 Ok(())
280}
281
282pub async fn import_to_arbor(
290 arbor: &ArborStorage,
291 project_path: &str,
292 session_id: &str,
293 owner_id: &str,
294) -> Result<TreeId, String> {
295 let events = read_session(project_path, session_id).await?;
296
297 let metadata = serde_json::json!({
299 "source": "claude_session_import",
300 "session_id": session_id,
301 "project_path": project_path,
302 });
303
304 let tree_id = arbor
305 .tree_create(Some(metadata), owner_id)
306 .await
307 .map_err(|e| e.to_string())?;
308
309 let tree = arbor.tree_get(&tree_id).await.map_err(|e| e.to_string())?;
310 let mut current_parent = tree.root;
311
312 for event in events {
314 match event {
315 SessionEvent::User { data } => {
316 let node_event = NodeEvent::UserMessage {
318 content: data.message.content.clone(),
319 };
320 let json =
321 serde_json::to_string(&node_event).map_err(|e| format!("Serialize error: {}", e))?;
322
323 let node_id = arbor
324 .node_create_text(&tree_id, Some(current_parent), json, None)
325 .await
326 .map_err(|e| e.to_string())?;
327
328 current_parent = node_id;
329 }
330 SessionEvent::Assistant { data } => {
331 let start_event = NodeEvent::AssistantStart;
333 let json = serde_json::to_string(&start_event)
334 .map_err(|e| format!("Serialize error: {}", e))?;
335
336 let start_node = arbor
337 .node_create_text(&tree_id, Some(current_parent), json, None)
338 .await
339 .map_err(|e| e.to_string())?;
340
341 current_parent = start_node;
342
343 if let AssistantMessage::Full { content, .. } = data.message {
345 for block in content {
346 let node_event = match block {
347 ContentBlock::Text { text } => NodeEvent::ContentText { text },
348 ContentBlock::ToolUse { id, name, input } => {
349 NodeEvent::ContentToolUse { id, name, input }
350 }
351 ContentBlock::ToolResult {
352 tool_use_id,
353 content,
354 is_error,
355 } => NodeEvent::UserToolResult {
356 tool_use_id,
357 content,
358 is_error: is_error.unwrap_or(false),
359 },
360 ContentBlock::Thinking { thinking, .. } => {
361 NodeEvent::ContentThinking { thinking }
362 }
363 };
364
365 let json = serde_json::to_string(&node_event)
366 .map_err(|e| format!("Serialize error: {}", e))?;
367
368 let node_id = arbor
369 .node_create_text(&tree_id, Some(current_parent), json, None)
370 .await
371 .map_err(|e| e.to_string())?;
372
373 current_parent = node_id;
374 }
375 }
376
377 let complete_event = NodeEvent::AssistantComplete { usage: None };
379 let json = serde_json::to_string(&complete_event)
380 .map_err(|e| format!("Serialize error: {}", e))?;
381
382 let complete_node = arbor
383 .node_create_text(&tree_id, Some(current_parent), json, None)
384 .await
385 .map_err(|e| e.to_string())?;
386
387 current_parent = complete_node;
388 }
389 _ => {
390 }
392 }
393 }
394
395 Ok(tree_id)
396}
397
398pub async fn export_from_arbor(
402 arbor: &ArborStorage,
403 tree_id: &TreeId,
404 project_path: &str,
405 session_id: &str,
406) -> Result<(), String> {
407 use crate::activations::arbor::NodeType;
408 use crate::activations::claudecode::types::NodeEvent;
409
410 let tree = arbor.tree_get(tree_id).await.map_err(|e| e.to_string())?;
411
412 let traverse_dfs = |tree: &crate::activations::arbor::Tree| -> Vec<TreeId> {
414 use std::collections::HashMap;
415
416 let mut children: HashMap<TreeId, Vec<TreeId>> = HashMap::new();
418 for (node_id, node) in &tree.nodes {
419 if let Some(parent_id) = &node.parent {
420 children.entry(*parent_id)
421 .or_insert_with(Vec::new)
422 .push(*node_id);
423 }
424 }
425
426 let mut visited = Vec::new();
428 let mut stack = vec![tree.root];
429
430 while let Some(current) = stack.pop() {
431 visited.push(current);
432 if let Some(child_ids) = children.get(¤t) {
433 for child_id in child_ids.iter().rev() {
435 stack.push(*child_id);
436 }
437 }
438 }
439
440 visited
441 };
442
443 let node_ids = traverse_dfs(&tree);
444
445 let mut session_events = Vec::new();
447 let mut current_assistant_blocks: Vec<ContentBlock> = Vec::new();
448 let mut in_assistant = false;
449
450 for node_id in node_ids {
451 let node = tree.nodes.get(&node_id).unwrap();
452
453 if let NodeType::Text { content } = &node.data {
454 if content.is_empty() {
456 continue;
457 }
458
459 let node_event: NodeEvent = match serde_json::from_str(content) {
461 Ok(e) => e,
462 Err(_) => continue, };
464
465 match node_event {
466 NodeEvent::UserMessage { content } => {
467 if in_assistant && !current_assistant_blocks.is_empty() {
469 session_events.push(build_assistant_event(
470 std::mem::take(&mut current_assistant_blocks),
471 session_id,
472 ));
473 in_assistant = false;
474 }
475
476 session_events.push(SessionEvent::User {
478 data: UserEvent {
479 uuid: uuid::Uuid::new_v4().to_string(),
480 parent_uuid: None,
481 session_id: session_id.to_string(),
482 timestamp: chrono::Utc::now().to_rfc3339(),
483 cwd: std::env::current_dir()
484 .ok()
485 .and_then(|p| p.to_str().map(String::from))
486 .unwrap_or_default(),
487 message: UserMessage {
488 role: "user".to_string(),
489 content,
490 },
491 is_sidechain: None,
492 git_branch: None,
493 },
494 });
495 }
496
497 NodeEvent::AssistantStart => {
498 in_assistant = true;
499 current_assistant_blocks.clear();
500 }
501
502 NodeEvent::ContentText { text } => {
503 if in_assistant {
504 current_assistant_blocks.push(ContentBlock::Text { text });
505 }
506 }
507
508 NodeEvent::ContentToolUse { id, name, input } => {
509 if in_assistant {
510 current_assistant_blocks.push(ContentBlock::ToolUse { id, name, input });
511 }
512 }
513
514 NodeEvent::ContentThinking { thinking } => {
515 if in_assistant {
516 current_assistant_blocks.push(ContentBlock::Thinking {
517 thinking,
518 signature: None,
519 });
520 }
521 }
522
523 NodeEvent::UserToolResult {
524 tool_use_id,
525 content,
526 is_error,
527 } => {
528 if in_assistant && !current_assistant_blocks.is_empty() {
530 session_events.push(build_assistant_event(
531 std::mem::take(&mut current_assistant_blocks),
532 session_id,
533 ));
534 in_assistant = false;
535 }
536
537 let content_str = serde_json::to_string(&vec![ContentBlock::ToolResult {
539 tool_use_id,
540 content,
541 is_error: Some(is_error),
542 }])
543 .unwrap_or_default();
544
545 session_events.push(SessionEvent::User {
546 data: UserEvent {
547 uuid: uuid::Uuid::new_v4().to_string(),
548 parent_uuid: None,
549 session_id: session_id.to_string(),
550 timestamp: chrono::Utc::now().to_rfc3339(),
551 cwd: std::env::current_dir()
552 .ok()
553 .and_then(|p| p.to_str().map(String::from))
554 .unwrap_or_default(),
555 message: UserMessage {
556 role: "user".to_string(),
557 content: content_str,
558 },
559 is_sidechain: None,
560 git_branch: None,
561 },
562 });
563 }
564
565 NodeEvent::AssistantComplete { .. } => {
566 if in_assistant && !current_assistant_blocks.is_empty() {
567 session_events.push(build_assistant_event(
568 std::mem::take(&mut current_assistant_blocks),
569 session_id,
570 ));
571 in_assistant = false;
572 }
573 }
574 }
575 }
576 }
577
578 if in_assistant && !current_assistant_blocks.is_empty() {
580 session_events.push(build_assistant_event(current_assistant_blocks, session_id));
581 }
582
583 for event in session_events {
585 append_to_session(project_path, session_id, &event).await?;
586 }
587
588 Ok(())
589}
590
591fn build_assistant_event(blocks: Vec<ContentBlock>, session_id: &str) -> SessionEvent {
593 SessionEvent::Assistant {
594 data: AssistantEvent {
595 uuid: uuid::Uuid::new_v4().to_string(),
596 parent_uuid: None,
597 session_id: session_id.to_string(),
598 timestamp: chrono::Utc::now().to_rfc3339(),
599 cwd: std::env::current_dir()
600 .ok()
601 .and_then(|p| p.to_str().map(String::from)),
602 message: AssistantMessage::Full {
603 role: "assistant".to_string(),
604 content: blocks,
605 model: None,
606 id: None,
607 stop_reason: None,
608 usage: None,
609 },
610 request_id: None,
611 },
612 }
613}