@ebowwa/claudecodehistory 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/history-service.d.ts.map +1 -1
- package/dist/history-service.js +2 -2
- package/dist/history-service.js.map +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -1
- package/package.json +32 -9
- package/rust/Cargo.lock +1955 -0
- package/rust/Cargo.toml +90 -0
- package/rust/build.rs +4 -0
- package/rust/claudecodehistory.darwin-arm64.node +0 -0
- package/rust/index.d.ts +0 -0
- package/rust/package.json +69 -0
- package/rust/src/lib.rs +800 -0
- package/rust/src/parser.rs +261 -0
- package/rust/src/search/index.rs +206 -0
- package/rust/src/search/mod.rs +291 -0
- package/rust/src/search/query.rs +458 -0
- package/rust/src/search/schema.rs +115 -0
- package/rust/src/types.rs +248 -0
- package/rust/src/utils.rs +210 -0
- package/typescript/dist/history-service.d.ts +187 -0
- package/typescript/dist/history-service.d.ts.map +1 -0
- package/typescript/dist/history-service.js +739 -0
- package/typescript/dist/history-service.js.map +1 -0
- package/typescript/dist/index.d.ts +3 -0
- package/typescript/dist/index.d.ts.map +1 -0
- package/typescript/dist/index.js +2 -0
- package/typescript/dist/index.js.map +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
//! Type definitions matching the TypeScript history-service.ts API
|
|
2
|
+
//!
|
|
3
|
+
//! These types are designed to be serializable for NAPI bindings
|
|
4
|
+
//! and match the TypeScript interfaces exactly.
|
|
5
|
+
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
|
|
8
|
+
/// Raw message from Claude Code JSONL files
|
|
9
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
10
|
+
pub struct ClaudeCodeMessage {
|
|
11
|
+
pub parent_uuid: Option<String>,
|
|
12
|
+
pub is_sidechain: bool,
|
|
13
|
+
pub user_type: String,
|
|
14
|
+
pub cwd: String,
|
|
15
|
+
pub session_id: String,
|
|
16
|
+
pub version: String,
|
|
17
|
+
#[serde(rename = "type")]
|
|
18
|
+
pub message_type: MessageType,
|
|
19
|
+
pub message: Option<MessageContent>,
|
|
20
|
+
pub uuid: String,
|
|
21
|
+
pub timestamp: String,
|
|
22
|
+
pub request_id: Option<String>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Message type enum
|
|
26
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
27
|
+
#[serde(rename_all = "lowercase")]
|
|
28
|
+
pub enum MessageType {
|
|
29
|
+
User,
|
|
30
|
+
Assistant,
|
|
31
|
+
System,
|
|
32
|
+
Result,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Message content structure
|
|
36
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
37
|
+
pub struct MessageContent {
|
|
38
|
+
pub role: String,
|
|
39
|
+
pub content: MessageContentValue,
|
|
40
|
+
pub model: Option<String>,
|
|
41
|
+
pub usage: Option<serde_json::Value>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Message content can be string or array
|
|
45
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
46
|
+
#[serde(untagged)]
|
|
47
|
+
pub enum MessageContentValue {
|
|
48
|
+
String(String),
|
|
49
|
+
Array(Vec<ContentBlock>),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Content block for array messages
|
|
53
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
54
|
+
pub struct ContentBlock {
|
|
55
|
+
#[serde(rename = "type")]
|
|
56
|
+
pub block_type: Option<String>,
|
|
57
|
+
pub text: Option<String>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Processed conversation entry for API responses
|
|
61
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
62
|
+
pub struct ConversationEntry {
|
|
63
|
+
pub session_id: String,
|
|
64
|
+
pub timestamp: String,
|
|
65
|
+
#[serde(rename = "type")]
|
|
66
|
+
pub entry_type: MessageType,
|
|
67
|
+
pub content: String,
|
|
68
|
+
pub project_path: String,
|
|
69
|
+
pub uuid: String,
|
|
70
|
+
pub formatted_time: Option<String>,
|
|
71
|
+
pub time_ago: Option<String>,
|
|
72
|
+
pub local_date: Option<String>,
|
|
73
|
+
pub metadata: Option<EntryMetadata>,
|
|
74
|
+
pub project_name: Option<String>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Metadata for conversation entries
|
|
78
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
79
|
+
pub struct EntryMetadata {
|
|
80
|
+
pub usage: Option<serde_json::Value>,
|
|
81
|
+
pub total_cost_usd: Option<f64>,
|
|
82
|
+
pub num_turns: Option<u32>,
|
|
83
|
+
pub duration_ms: Option<u64>,
|
|
84
|
+
pub is_error: Option<bool>,
|
|
85
|
+
pub error_type: Option<String>,
|
|
86
|
+
pub model: Option<String>,
|
|
87
|
+
pub request_id: Option<String>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Paginated response for conversation history
|
|
91
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
92
|
+
pub struct PaginatedConversationResponse {
|
|
93
|
+
pub entries: Vec<ConversationEntry>,
|
|
94
|
+
pub pagination: Pagination,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Pagination metadata
|
|
98
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
99
|
+
pub struct Pagination {
|
|
100
|
+
pub total_count: usize,
|
|
101
|
+
pub limit: usize,
|
|
102
|
+
pub offset: usize,
|
|
103
|
+
pub has_more: bool,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Options for querying conversation history
|
|
107
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
108
|
+
pub struct HistoryQueryOptions {
|
|
109
|
+
pub session_id: Option<String>,
|
|
110
|
+
pub start_date: Option<String>,
|
|
111
|
+
pub end_date: Option<String>,
|
|
112
|
+
pub limit: Option<usize>,
|
|
113
|
+
pub offset: Option<usize>,
|
|
114
|
+
pub timezone: Option<String>,
|
|
115
|
+
pub message_types: Option<Vec<MessageType>>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Options for listing sessions
|
|
119
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
120
|
+
pub struct SessionListOptions {
|
|
121
|
+
pub project_path: Option<String>,
|
|
122
|
+
pub start_date: Option<String>,
|
|
123
|
+
pub end_date: Option<String>,
|
|
124
|
+
pub timezone: Option<String>,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Project information
|
|
128
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
129
|
+
pub struct ProjectInfo {
|
|
130
|
+
pub project_path: String,
|
|
131
|
+
pub session_count: usize,
|
|
132
|
+
pub message_count: usize,
|
|
133
|
+
pub last_activity_time: String,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Session information
|
|
137
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
138
|
+
pub struct SessionInfo {
|
|
139
|
+
pub session_id: String,
|
|
140
|
+
pub project_path: String,
|
|
141
|
+
pub start_time: String,
|
|
142
|
+
pub end_time: String,
|
|
143
|
+
pub message_count: usize,
|
|
144
|
+
pub user_message_count: usize,
|
|
145
|
+
pub assistant_message_count: usize,
|
|
146
|
+
pub first_user_message: Option<String>,
|
|
147
|
+
pub duration_ms: Option<u64>,
|
|
148
|
+
pub duration_formatted: Option<String>,
|
|
149
|
+
pub has_errors: Option<bool>,
|
|
150
|
+
pub project_name: Option<String>,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Search options
|
|
154
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
155
|
+
pub struct SearchOptions {
|
|
156
|
+
pub limit: Option<usize>,
|
|
157
|
+
pub project_path: Option<String>,
|
|
158
|
+
pub start_date: Option<String>,
|
|
159
|
+
pub end_date: Option<String>,
|
|
160
|
+
pub timezone: Option<String>,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Current session information
|
|
164
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
165
|
+
pub struct CurrentSessionInfo {
|
|
166
|
+
pub session_id: String,
|
|
167
|
+
pub timestamp: String,
|
|
168
|
+
pub project_path: Option<String>,
|
|
169
|
+
pub display: Option<String>,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Session process information
|
|
173
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
174
|
+
pub struct SessionProcessInfo {
|
|
175
|
+
pub session_id: String,
|
|
176
|
+
pub pid: u32,
|
|
177
|
+
pub command: String,
|
|
178
|
+
pub alive: bool,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Recent activity options
|
|
182
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
183
|
+
pub struct RecentActivityOptions {
|
|
184
|
+
pub limit: Option<usize>,
|
|
185
|
+
pub include_summaries: Option<bool>,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Recent activity item
|
|
189
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
190
|
+
pub struct RecentActivityItem {
|
|
191
|
+
pub session_id: String,
|
|
192
|
+
pub project_path: String,
|
|
193
|
+
pub project_name: String,
|
|
194
|
+
pub timestamp: String,
|
|
195
|
+
pub asked: String,
|
|
196
|
+
pub done: Option<String>,
|
|
197
|
+
pub time_ago: String,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Fast parsed entry (minimal fields for quick processing)
|
|
201
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
202
|
+
pub struct FastEntry {
|
|
203
|
+
pub uuid: String,
|
|
204
|
+
pub session_id: String,
|
|
205
|
+
pub timestamp: String,
|
|
206
|
+
pub role: Option<String>,
|
|
207
|
+
pub content: Option<String>,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Parsed entry from the fast parser
|
|
211
|
+
#[derive(Debug, Clone)]
|
|
212
|
+
pub struct ParsedEntry {
|
|
213
|
+
pub uuid: String,
|
|
214
|
+
pub session_id: String,
|
|
215
|
+
pub timestamp: String,
|
|
216
|
+
pub role: String,
|
|
217
|
+
pub content: String,
|
|
218
|
+
pub file_path: String,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
impl Default for MessageType {
|
|
222
|
+
fn default() -> Self {
|
|
223
|
+
Self::User
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl std::fmt::Display for MessageType {
|
|
228
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
229
|
+
match self {
|
|
230
|
+
MessageType::User => write!(f, "user"),
|
|
231
|
+
MessageType::Assistant => write!(f, "assistant"),
|
|
232
|
+
MessageType::System => write!(f, "system"),
|
|
233
|
+
MessageType::Result => write!(f, "result"),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
impl From<&str> for MessageType {
|
|
239
|
+
fn from(s: &str) -> Self {
|
|
240
|
+
match s.to_lowercase().as_str() {
|
|
241
|
+
"user" => MessageType::User,
|
|
242
|
+
"assistant" => MessageType::Assistant,
|
|
243
|
+
"system" => MessageType::System,
|
|
244
|
+
"result" => MessageType::Result,
|
|
245
|
+
_ => MessageType::User,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
//! Utility functions for time formatting, path handling, and other helpers
|
|
2
|
+
|
|
3
|
+
use chrono::{DateTime, Local, TimeZone, Utc};
|
|
4
|
+
|
|
5
|
+
/// Format a timestamp into human-readable formats
|
|
6
|
+
///
|
|
7
|
+
/// Returns (formatted_time, time_ago, local_date)
|
|
8
|
+
pub fn format_timestamp(timestamp: &str) -> (String, String, String) {
|
|
9
|
+
let dt = parse_timestamp(timestamp);
|
|
10
|
+
|
|
11
|
+
// Format in local timezone (Asia/Tokyo style for consistency with TS)
|
|
12
|
+
let formatted_time = dt
|
|
13
|
+
.with_timezone(&Local)
|
|
14
|
+
.format("%Y/%m/%d %H:%M:%S")
|
|
15
|
+
.to_string();
|
|
16
|
+
|
|
17
|
+
// Calculate time ago
|
|
18
|
+
let time_ago = get_time_ago(&dt);
|
|
19
|
+
|
|
20
|
+
// Local date in YYYY-MM-DD format
|
|
21
|
+
let local_date = dt.with_timezone(&Local).format("%Y-%m-%d").to_string();
|
|
22
|
+
|
|
23
|
+
(formatted_time, time_ago, local_date)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Parse ISO timestamp string into DateTime
|
|
27
|
+
pub fn parse_timestamp(timestamp: &str) -> DateTime<Utc> {
|
|
28
|
+
// Try parsing as ISO 8601
|
|
29
|
+
if let Ok(dt) = DateTime::parse_from_rfc3339(timestamp) {
|
|
30
|
+
return dt.with_timezone(&Utc);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback to current time if parsing fails
|
|
34
|
+
Utc::now()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Calculate human-readable "time ago" string
|
|
38
|
+
pub fn get_time_ago(dt: &DateTime<Utc>) -> String {
|
|
39
|
+
let now = Utc::now();
|
|
40
|
+
let diff = now.signed_duration_since(*dt);
|
|
41
|
+
|
|
42
|
+
let mins = diff.num_minutes();
|
|
43
|
+
let hours = diff.num_hours();
|
|
44
|
+
let days = diff.num_days();
|
|
45
|
+
|
|
46
|
+
if mins < 1 {
|
|
47
|
+
"just now".to_string()
|
|
48
|
+
} else if mins < 60 {
|
|
49
|
+
format!("{}m ago", mins)
|
|
50
|
+
} else if hours < 24 {
|
|
51
|
+
format!("{}h ago", hours)
|
|
52
|
+
} else if days < 7 {
|
|
53
|
+
format!("{}d ago", days)
|
|
54
|
+
} else if days < 30 {
|
|
55
|
+
format!("{}w ago", days / 7)
|
|
56
|
+
} else if days < 365 {
|
|
57
|
+
format!("{}mo ago", days / 30)
|
|
58
|
+
} else {
|
|
59
|
+
format!("{}y ago", days / 365)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Format duration in milliseconds to human-readable string
|
|
64
|
+
pub fn format_duration(ms: i64) -> String {
|
|
65
|
+
if ms < 1000 {
|
|
66
|
+
return format!("{}ms", ms);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let seconds = ms / 1000;
|
|
70
|
+
if seconds < 60 {
|
|
71
|
+
return format!("{}s", seconds);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let minutes = seconds / 60;
|
|
75
|
+
let remaining_seconds = seconds % 60;
|
|
76
|
+
if minutes < 60 {
|
|
77
|
+
if remaining_seconds > 0 {
|
|
78
|
+
return format!("{}m {}s", minutes, remaining_seconds);
|
|
79
|
+
}
|
|
80
|
+
return format!("{}m", minutes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let hours = minutes / 60;
|
|
84
|
+
let remaining_minutes = minutes % 60;
|
|
85
|
+
if hours < 24 {
|
|
86
|
+
if remaining_minutes > 0 {
|
|
87
|
+
return format!("{}h {}m", hours, remaining_minutes);
|
|
88
|
+
}
|
|
89
|
+
return format!("{}h", hours);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let days = hours / 24;
|
|
93
|
+
let remaining_hours = hours % 24;
|
|
94
|
+
if remaining_hours > 0 {
|
|
95
|
+
format!("{}d {}h", days, remaining_hours)
|
|
96
|
+
} else {
|
|
97
|
+
format!("{}d", days)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Normalize a date string to ISO format with timezone support
|
|
102
|
+
///
|
|
103
|
+
/// Takes a date like "2026-02-26" and converts it to ISO format
|
|
104
|
+
/// considering the timezone.
|
|
105
|
+
pub fn normalize_date(date_string: &str, is_end_date: bool, timezone: Option<&str>) -> String {
|
|
106
|
+
// If already ISO format, return as-is
|
|
107
|
+
if date_string.contains('T') {
|
|
108
|
+
return date_string.to_string();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let time_str = if is_end_date {
|
|
112
|
+
"23:59:59.999"
|
|
113
|
+
} else {
|
|
114
|
+
"00:00:00.000"
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// For UTC, simple format
|
|
118
|
+
if timezone == Some("UTC") {
|
|
119
|
+
return format!("{}T{}Z", date_string, time_str);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For other timezones, we need to calculate the offset
|
|
123
|
+
// This is a simplified version - full implementation would use chrono-tz
|
|
124
|
+
// For now, use local timezone
|
|
125
|
+
let parts: Vec<_> = date_string.split('-').collect();
|
|
126
|
+
if parts.len() != 3 {
|
|
127
|
+
return format!("{}T{}Z", date_string, time_str);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let year: i32 = parts[0].parse().unwrap_or(1970);
|
|
131
|
+
let month: u32 = parts[1].parse().unwrap_or(1);
|
|
132
|
+
let day: u32 = parts[2].parse().unwrap_or(1);
|
|
133
|
+
|
|
134
|
+
let hour = if is_end_date { 23 } else { 0 };
|
|
135
|
+
let minute = if is_end_date { 59 } else { 0 };
|
|
136
|
+
let second = if is_end_date { 59 } else { 0 };
|
|
137
|
+
|
|
138
|
+
// Create local datetime and convert to UTC
|
|
139
|
+
Local
|
|
140
|
+
.with_ymd_and_hms(year, month, day, hour, minute, second)
|
|
141
|
+
.single()
|
|
142
|
+
.map(|dt| dt.with_timezone(&Utc).to_rfc3339())
|
|
143
|
+
.unwrap_or_else(|| format!("{}T{}Z", date_string, time_str))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Decode project path from directory name
|
|
147
|
+
///
|
|
148
|
+
/// Converts "-Users-ebowwa-Desktop-codespaces" to "/Users/ebowwa/Desktop/codespaces"
|
|
149
|
+
pub fn decode_project_path(project_dir: &str) -> String {
|
|
150
|
+
project_dir
|
|
151
|
+
.replace('-', "/")
|
|
152
|
+
.trim_start_matches('/')
|
|
153
|
+
.to_string()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Encode project path to directory name
|
|
157
|
+
///
|
|
158
|
+
/// Converts "/Users/ebowwa/Desktop/codespaces" to "-Users-ebowwa-Desktop-codespaces"
|
|
159
|
+
pub fn encode_project_path(project_path: &str) -> String {
|
|
160
|
+
let trimmed = project_path.trim_start_matches('/');
|
|
161
|
+
if trimmed.is_empty() {
|
|
162
|
+
"-".to_string()
|
|
163
|
+
} else {
|
|
164
|
+
format!("-{}", trimmed.replace('/', "-"))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[cfg(test)]
|
|
169
|
+
mod tests {
|
|
170
|
+
use super::*;
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn test_format_duration() {
|
|
174
|
+
assert_eq!(format_duration(500), "500ms");
|
|
175
|
+
assert_eq!(format_duration(5000), "5s");
|
|
176
|
+
assert_eq!(format_duration(65000), "1m 5s");
|
|
177
|
+
assert_eq!(format_duration(3600000), "1h");
|
|
178
|
+
assert_eq!(format_duration(3661000), "1h 1m");
|
|
179
|
+
assert_eq!(format_duration(90061000), "1d 1h");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn test_decode_project_path() {
|
|
184
|
+
assert_eq!(
|
|
185
|
+
decode_project_path("-Users-ebowwa-Desktop-codespaces"),
|
|
186
|
+
"Users/ebowwa/Desktop/codespaces"
|
|
187
|
+
);
|
|
188
|
+
assert_eq!(decode_project_path("simple-path"), "simple/path");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn test_encode_project_path() {
|
|
193
|
+
assert_eq!(
|
|
194
|
+
encode_project_path("/Users/ebowwa/Desktop/codespaces"),
|
|
195
|
+
"-Users-ebowwa-Desktop-codespaces"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn test_get_time_ago() {
|
|
201
|
+
let now = Utc::now();
|
|
202
|
+
let one_min_ago = now - chrono::Duration::seconds(30);
|
|
203
|
+
let one_hour_ago = now - chrono::Duration::hours(1);
|
|
204
|
+
let one_day_ago = now - chrono::Duration::days(1);
|
|
205
|
+
|
|
206
|
+
assert!(get_time_ago(&one_min_ago).contains("m ago") || get_time_ago(&one_min_ago) == "just now");
|
|
207
|
+
assert!(get_time_ago(&one_hour_ago).contains("h ago"));
|
|
208
|
+
assert!(get_time_ago(&one_day_ago).contains("d ago"));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export interface ClaudeCodeMessage {
|
|
2
|
+
parentUuid: string | null;
|
|
3
|
+
isSidechain: boolean;
|
|
4
|
+
userType: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
version: string;
|
|
8
|
+
type: 'user' | 'assistant' | 'system' | 'result';
|
|
9
|
+
message?: {
|
|
10
|
+
role: string;
|
|
11
|
+
content: string | any[];
|
|
12
|
+
model?: string;
|
|
13
|
+
usage?: any;
|
|
14
|
+
};
|
|
15
|
+
uuid: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ConversationEntry {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
type: 'user' | 'assistant' | 'system' | 'result';
|
|
23
|
+
content: string;
|
|
24
|
+
projectPath: string;
|
|
25
|
+
uuid: string;
|
|
26
|
+
formattedTime?: string;
|
|
27
|
+
timeAgo?: string;
|
|
28
|
+
localDate?: string;
|
|
29
|
+
metadata?: {
|
|
30
|
+
usage?: any;
|
|
31
|
+
totalCostUsd?: number;
|
|
32
|
+
numTurns?: number;
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
isError?: boolean;
|
|
35
|
+
errorType?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
requestId?: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface PaginatedConversationResponse {
|
|
41
|
+
entries: ConversationEntry[];
|
|
42
|
+
pagination: {
|
|
43
|
+
total_count: number;
|
|
44
|
+
limit: number;
|
|
45
|
+
offset: number;
|
|
46
|
+
has_more: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface HistoryQueryOptions {
|
|
50
|
+
sessionId?: string;
|
|
51
|
+
startDate?: string;
|
|
52
|
+
endDate?: string;
|
|
53
|
+
limit?: number;
|
|
54
|
+
offset?: number;
|
|
55
|
+
timezone?: string;
|
|
56
|
+
messageTypes?: ('user' | 'assistant' | 'system' | 'result')[];
|
|
57
|
+
}
|
|
58
|
+
export interface SessionListOptions {
|
|
59
|
+
projectPath?: string;
|
|
60
|
+
startDate?: string;
|
|
61
|
+
endDate?: string;
|
|
62
|
+
timezone?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ProjectInfo {
|
|
65
|
+
projectPath: string;
|
|
66
|
+
sessionCount: number;
|
|
67
|
+
messageCount: number;
|
|
68
|
+
lastActivityTime: string;
|
|
69
|
+
}
|
|
70
|
+
export interface SessionInfo {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
projectPath: string;
|
|
73
|
+
startTime: string;
|
|
74
|
+
endTime: string;
|
|
75
|
+
messageCount: number;
|
|
76
|
+
userMessageCount: number;
|
|
77
|
+
assistantMessageCount: number;
|
|
78
|
+
/** First user message preview (truncated to ~100 chars) */
|
|
79
|
+
firstUserMessage?: string;
|
|
80
|
+
/** Duration in milliseconds */
|
|
81
|
+
durationMs?: number;
|
|
82
|
+
/** Human-readable duration (e.g., "5m 30s") */
|
|
83
|
+
durationFormatted?: string;
|
|
84
|
+
/** Session status indicators */
|
|
85
|
+
hasErrors?: boolean;
|
|
86
|
+
/** Project name extracted from path (basename) */
|
|
87
|
+
projectName?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface SearchOptions {
|
|
90
|
+
limit?: number;
|
|
91
|
+
projectPath?: string;
|
|
92
|
+
startDate?: string;
|
|
93
|
+
endDate?: string;
|
|
94
|
+
timezone?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface CurrentSessionInfo {
|
|
97
|
+
sessionId: string;
|
|
98
|
+
timestamp: string;
|
|
99
|
+
projectPath?: string;
|
|
100
|
+
display?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface SessionProcessInfo {
|
|
103
|
+
sessionId: string;
|
|
104
|
+
pid: number;
|
|
105
|
+
command: string;
|
|
106
|
+
alive: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface RecentActivityOptions {
|
|
109
|
+
limit?: number;
|
|
110
|
+
includeSummaries?: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface RecentActivityItem {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
projectPath: string;
|
|
115
|
+
projectName: string;
|
|
116
|
+
timestamp: string;
|
|
117
|
+
/** First user message - what was asked */
|
|
118
|
+
asked: string;
|
|
119
|
+
/** Brief summary from assistant messages - what was done */
|
|
120
|
+
done?: string;
|
|
121
|
+
/** Human-readable relative time */
|
|
122
|
+
timeAgo: string;
|
|
123
|
+
}
|
|
124
|
+
export declare class ClaudeCodeHistoryService {
|
|
125
|
+
private claudeDir;
|
|
126
|
+
constructor(claudeDir?: string);
|
|
127
|
+
/**
|
|
128
|
+
* Normalize date string to ISO format for proper comparison with timezone support
|
|
129
|
+
*/
|
|
130
|
+
private normalizeDate;
|
|
131
|
+
getConversationHistory(options?: HistoryQueryOptions): Promise<PaginatedConversationResponse>;
|
|
132
|
+
searchConversations(searchQuery: string, options?: SearchOptions): Promise<ConversationEntry[]>;
|
|
133
|
+
listProjects(): Promise<ProjectInfo[]>;
|
|
134
|
+
listSessions(options?: SessionListOptions): Promise<SessionInfo[]>;
|
|
135
|
+
/**
|
|
136
|
+
* Fast path: List sessions using the Rust parser
|
|
137
|
+
* Groups entries by session_id from fast parser results
|
|
138
|
+
*/
|
|
139
|
+
private listSessionsFast;
|
|
140
|
+
/**
|
|
141
|
+
* Get recent activity across all projects
|
|
142
|
+
* Returns what was asked, what was done, and when for the most recent sessions
|
|
143
|
+
*/
|
|
144
|
+
getRecentActivity(options?: RecentActivityOptions): Promise<RecentActivityItem[]>;
|
|
145
|
+
/**
|
|
146
|
+
* Generate a brief summary of what was done in a session from assistant messages
|
|
147
|
+
*/
|
|
148
|
+
private generateSessionSummary;
|
|
149
|
+
private loadClaudeHistoryEntries;
|
|
150
|
+
private parseJsonlFile;
|
|
151
|
+
private convertClaudeMessageToEntry;
|
|
152
|
+
private getTimeAgo;
|
|
153
|
+
/**
|
|
154
|
+
* Format duration in milliseconds to human-readable string
|
|
155
|
+
*/
|
|
156
|
+
private formatDuration;
|
|
157
|
+
private decodeProjectPath;
|
|
158
|
+
/**
|
|
159
|
+
* Determines whether to skip reading a file based on its modification time
|
|
160
|
+
*/
|
|
161
|
+
private shouldSkipFile;
|
|
162
|
+
/**
|
|
163
|
+
* Gets the current active Claude Code session by reading the last line from history.jsonl
|
|
164
|
+
*/
|
|
165
|
+
getCurrentSession(): Promise<CurrentSessionInfo | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Maps a process ID to a Claude Code session by examining the process tree
|
|
168
|
+
*/
|
|
169
|
+
getSessionByPid(pid: number): Promise<SessionProcessInfo | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Lists all session UUIDs from the session-env directory
|
|
172
|
+
*/
|
|
173
|
+
listAllSessionUuids(): Promise<string[]>;
|
|
174
|
+
/**
|
|
175
|
+
* Reads only the last line from a file efficiently
|
|
176
|
+
*/
|
|
177
|
+
private readLastLineFromFile;
|
|
178
|
+
/**
|
|
179
|
+
* Extracts session ID from a Claude Code process by examining command and environment
|
|
180
|
+
*/
|
|
181
|
+
private extractSessionIdFromProcess;
|
|
182
|
+
/**
|
|
183
|
+
* Checks if a process is still alive
|
|
184
|
+
*/
|
|
185
|
+
private isProcessAlive;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=history-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-service.d.ts","sourceRoot":"","sources":["../src/history-service.ts"],"names":[],"mappings":"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"}
|