1use crate::search::{
2 extract_project_from_path, group_by_session, search_multiple_paths, Message, SessionSource,
3};
4use chrono::{DateTime, Utc};
5use serde::Serialize;
6use std::fs;
7use std::io::{BufRead, BufReader};
8use std::path::Path;
9
10#[derive(Serialize)]
11struct SearchResult {
12 session_id: String,
13 project: String,
14 source: String,
15 file_path: String,
16 timestamp: String,
17 role: String,
18 content: String,
19}
20
21#[derive(Serialize)]
22struct ListResult {
23 session_id: String,
24 project: String,
25 source: String,
26 file_path: String,
27 last_active: String,
28 message_count: usize,
29}
30
31pub fn cli_search(query: &str, search_paths: &[String], use_regex: bool, limit: usize) {
33 let results = match search_multiple_paths(query, search_paths, use_regex) {
34 Ok(r) => r,
35 Err(e) => {
36 eprintln!("Search error: {}", e);
37 std::process::exit(1);
38 }
39 };
40
41 let groups = group_by_session(results);
42 let mut count = 0;
43
44 for group in &groups {
45 let project = extract_project_from_path(&group.file_path);
46 let source = SessionSource::from_path(&group.file_path);
47
48 for m in &group.matches {
49 if count >= limit {
50 return;
51 }
52
53 if let Some(ref msg) = m.message {
54 let result = SearchResult {
55 session_id: msg.session_id.clone(),
56 project: project.clone(),
57 source: source.display_name().to_string(),
58 file_path: m.file_path.clone(),
59 timestamp: msg.timestamp.to_rfc3339(),
60 role: msg.role.clone(),
61 content: msg.content.clone(),
62 };
63
64 if let Ok(json) = serde_json::to_string(&result) {
65 println!("{}", json);
66 count += 1;
67 }
68 }
69 }
70 }
71}
72
73pub fn cli_list(search_paths: &[String], limit: usize) {
75 let mut sessions: Vec<ListResult> = Vec::new();
76
77 for search_path in search_paths {
78 if !Path::new(search_path).exists() {
79 continue;
80 }
81 collect_sessions(search_path, &mut sessions);
82 }
83
84 sessions.sort_by(|a, b| b.last_active.cmp(&a.last_active));
86
87 for session in sessions.iter().take(limit) {
88 if let Ok(json) = serde_json::to_string(session) {
89 println!("{}", json);
90 }
91 }
92}
93
94fn collect_sessions(dir: &str, results: &mut Vec<ListResult>) {
96 let entries = match fs::read_dir(dir) {
97 Ok(e) => e,
98 Err(_) => return,
99 };
100
101 for entry in entries.flatten() {
102 let path = entry.path();
103
104 if path.is_dir() {
105 collect_sessions(path.to_str().unwrap_or(""), results);
106 } else if path.extension().is_some_and(|ext| ext == "jsonl") {
107 if let Some(session) = extract_session_metadata(&path) {
108 results.push(session);
109 }
110 }
111 }
112}
113
114fn extract_session_metadata(path: &Path) -> Option<ListResult> {
116 let file = fs::File::open(path).ok()?;
117 let reader = BufReader::new(file);
118
119 let path_str = path.to_str()?;
120 let project = extract_project_from_path(path_str);
121 let source = SessionSource::from_path(path_str);
122
123 let mut session_id: Option<String> = None;
124 let mut last_timestamp: Option<DateTime<Utc>> = None;
125 let mut message_count: usize = 0;
126
127 for line in reader.lines() {
128 let line = match line {
129 Ok(l) => l,
130 Err(_) => continue,
131 };
132
133 if let Some(msg) = Message::from_jsonl(line.trim(), 0) {
134 if session_id.is_none() {
135 session_id = Some(msg.session_id.clone());
136 }
137 if last_timestamp.is_none_or(|t| msg.timestamp > t) {
138 last_timestamp = Some(msg.timestamp);
139 }
140 message_count += 1;
141 }
142 }
143
144 let session_id = session_id?;
145 let last_active = last_timestamp?.to_rfc3339();
146
147 Some(ListResult {
148 session_id,
149 project,
150 source: source.display_name().to_string(),
151 file_path: path_str.to_string(),
152 last_active,
153 message_count,
154 })
155}