Skip to main content

ccs/
cli.rs

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
31/// Run CLI search command
32pub 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
73/// Run CLI list command — enumerate all sessions with metadata
74pub 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    // Sort by last_active descending
85    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
94/// Recursively find .jsonl files and extract session metadata
95fn 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
114/// Extract metadata from a single .jsonl file by reading first and last messages
115fn 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}