grok-cli-acp 0.1.2
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/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- package/src/utils/telemetry.rs +41 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
//! Chat session logging module
|
|
2
|
+
//!
|
|
3
|
+
//! Provides comprehensive logging of chat sessions including:
|
|
4
|
+
//! - User prompts and assistant responses
|
|
5
|
+
//! - Session metadata and timestamps
|
|
6
|
+
//! - Multiple output formats (JSON, text)
|
|
7
|
+
//! - Automatic file rotation and management
|
|
8
|
+
//! - Robust error handling for network/disk issues
|
|
9
|
+
|
|
10
|
+
use anyhow::{Context, Result};
|
|
11
|
+
use chrono::{DateTime, Utc};
|
|
12
|
+
use serde::{Deserialize, Serialize};
|
|
13
|
+
use std::fs::{self, File, OpenOptions};
|
|
14
|
+
use std::io::Write;
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::sync::Mutex;
|
|
17
|
+
use tracing::{debug, error, info, warn};
|
|
18
|
+
|
|
19
|
+
/// Configuration for chat logging
|
|
20
|
+
#[derive(Debug, Clone)]
|
|
21
|
+
pub struct ChatLoggerConfig {
|
|
22
|
+
/// Enable chat logging
|
|
23
|
+
pub enabled: bool,
|
|
24
|
+
/// Directory to store chat logs
|
|
25
|
+
pub log_dir: PathBuf,
|
|
26
|
+
/// Enable JSON format
|
|
27
|
+
pub json_format: bool,
|
|
28
|
+
/// Enable human-readable text format
|
|
29
|
+
pub text_format: bool,
|
|
30
|
+
/// Maximum log file size in MB before rotation
|
|
31
|
+
pub max_file_size_mb: u64,
|
|
32
|
+
/// Number of rotated files to keep
|
|
33
|
+
pub rotation_count: usize,
|
|
34
|
+
/// Include system messages in logs
|
|
35
|
+
pub include_system: bool,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Default for ChatLoggerConfig {
|
|
39
|
+
fn default() -> Self {
|
|
40
|
+
let log_dir = dirs::home_dir()
|
|
41
|
+
.unwrap_or_else(|| PathBuf::from("."))
|
|
42
|
+
.join(".grok")
|
|
43
|
+
.join("logs")
|
|
44
|
+
.join("chat_sessions");
|
|
45
|
+
|
|
46
|
+
Self {
|
|
47
|
+
enabled: true,
|
|
48
|
+
log_dir,
|
|
49
|
+
json_format: true,
|
|
50
|
+
text_format: true,
|
|
51
|
+
max_file_size_mb: 10,
|
|
52
|
+
rotation_count: 5,
|
|
53
|
+
include_system: true,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Represents a single chat message
|
|
59
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
60
|
+
pub struct ChatMessage {
|
|
61
|
+
/// Timestamp of the message
|
|
62
|
+
pub timestamp: DateTime<Utc>,
|
|
63
|
+
/// Role: "user", "assistant", or "system"
|
|
64
|
+
pub role: String,
|
|
65
|
+
/// Message content
|
|
66
|
+
pub content: String,
|
|
67
|
+
/// Optional metadata (model used, tokens, etc.)
|
|
68
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
69
|
+
pub metadata: Option<serde_json::Value>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl ChatMessage {
|
|
73
|
+
pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
|
|
74
|
+
Self {
|
|
75
|
+
timestamp: Utc::now(),
|
|
76
|
+
role: role.into(),
|
|
77
|
+
content: content.into(),
|
|
78
|
+
metadata: None,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
|
|
83
|
+
self.metadata = Some(metadata);
|
|
84
|
+
self
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Represents a complete chat session
|
|
89
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
90
|
+
pub struct ChatSession {
|
|
91
|
+
/// Unique session identifier
|
|
92
|
+
pub session_id: String,
|
|
93
|
+
/// Session start time
|
|
94
|
+
pub start_time: DateTime<Utc>,
|
|
95
|
+
/// Session end time (if completed)
|
|
96
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
97
|
+
pub end_time: Option<DateTime<Utc>>,
|
|
98
|
+
/// All messages in the session
|
|
99
|
+
pub messages: Vec<ChatMessage>,
|
|
100
|
+
/// Session metadata
|
|
101
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
102
|
+
pub metadata: Option<serde_json::Value>,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
impl ChatSession {
|
|
106
|
+
pub fn new(session_id: impl Into<String>) -> Self {
|
|
107
|
+
Self {
|
|
108
|
+
session_id: session_id.into(),
|
|
109
|
+
start_time: Utc::now(),
|
|
110
|
+
end_time: None,
|
|
111
|
+
messages: Vec::new(),
|
|
112
|
+
metadata: None,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
|
|
117
|
+
self.metadata = Some(metadata);
|
|
118
|
+
self
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn add_message(&mut self, message: ChatMessage) {
|
|
122
|
+
self.messages.push(message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub fn end_session(&mut self) {
|
|
126
|
+
self.end_time = Some(Utc::now());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Chat logger instance
|
|
131
|
+
pub struct ChatLogger {
|
|
132
|
+
config: ChatLoggerConfig,
|
|
133
|
+
current_session: Mutex<Option<ChatSession>>,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
impl ChatLogger {
|
|
137
|
+
/// Create a new chat logger with the given configuration
|
|
138
|
+
pub fn new(config: ChatLoggerConfig) -> Result<Self> {
|
|
139
|
+
// Ensure log directory exists
|
|
140
|
+
if config.enabled {
|
|
141
|
+
fs::create_dir_all(&config.log_dir)
|
|
142
|
+
.with_context(|| format!("Failed to create log directory: {:?}", config.log_dir))?;
|
|
143
|
+
info!("Chat logger initialized: {:?}", config.log_dir);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Ok(Self {
|
|
147
|
+
config,
|
|
148
|
+
current_session: Mutex::new(None),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Start a new chat session
|
|
153
|
+
pub fn start_session(&self, session_id: impl Into<String>) -> Result<()> {
|
|
154
|
+
if !self.config.enabled {
|
|
155
|
+
return Ok(());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let session_id = session_id.into();
|
|
159
|
+
let mut current = self.current_session.lock().unwrap();
|
|
160
|
+
|
|
161
|
+
// End previous session if exists
|
|
162
|
+
if let Some(prev_session) = current.take() {
|
|
163
|
+
warn!(
|
|
164
|
+
"Starting new session {} while session {} was active. Ending previous session.",
|
|
165
|
+
session_id, prev_session.session_id
|
|
166
|
+
);
|
|
167
|
+
drop(current); // Release lock before saving
|
|
168
|
+
self.save_session(&prev_session)?;
|
|
169
|
+
current = self.current_session.lock().unwrap();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let session = ChatSession::new(&session_id);
|
|
173
|
+
info!("Started chat session: {}", session_id);
|
|
174
|
+
*current = Some(session);
|
|
175
|
+
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Log a user prompt
|
|
180
|
+
pub fn log_user_message(&self, content: impl Into<String>) -> Result<()> {
|
|
181
|
+
self.log_message("user", content, None)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Log an assistant response
|
|
185
|
+
pub fn log_assistant_message(&self, content: impl Into<String>) -> Result<()> {
|
|
186
|
+
self.log_message("assistant", content, None)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Log a system message
|
|
190
|
+
pub fn log_system_message(&self, content: impl Into<String>) -> Result<()> {
|
|
191
|
+
if !self.config.include_system {
|
|
192
|
+
return Ok(());
|
|
193
|
+
}
|
|
194
|
+
self.log_message("system", content, None)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Log a message with optional metadata
|
|
198
|
+
pub fn log_message(
|
|
199
|
+
&self,
|
|
200
|
+
role: impl Into<String>,
|
|
201
|
+
content: impl Into<String>,
|
|
202
|
+
metadata: Option<serde_json::Value>,
|
|
203
|
+
) -> Result<()> {
|
|
204
|
+
if !self.config.enabled {
|
|
205
|
+
return Ok(());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let mut current = self.current_session.lock().unwrap();
|
|
209
|
+
if let Some(session) = current.as_mut() {
|
|
210
|
+
let mut message = ChatMessage::new(role, content);
|
|
211
|
+
if let Some(meta) = metadata {
|
|
212
|
+
message = message.with_metadata(meta);
|
|
213
|
+
}
|
|
214
|
+
debug!(
|
|
215
|
+
"Logging message in session {}: {}",
|
|
216
|
+
session.session_id, message.role
|
|
217
|
+
);
|
|
218
|
+
session.add_message(message);
|
|
219
|
+
} else {
|
|
220
|
+
warn!("Attempted to log message without an active session");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// End the current session and save it
|
|
227
|
+
pub fn end_session(&self) -> Result<()> {
|
|
228
|
+
if !self.config.enabled {
|
|
229
|
+
return Ok(());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let mut current = self.current_session.lock().unwrap();
|
|
233
|
+
if let Some(mut session) = current.take() {
|
|
234
|
+
session.end_session();
|
|
235
|
+
let session_id = session.session_id.clone();
|
|
236
|
+
drop(current); // Release lock before saving
|
|
237
|
+
self.save_session(&session)?;
|
|
238
|
+
info!("Ended chat session: {}", session_id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Ok(())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Save a session to disk
|
|
245
|
+
fn save_session(&self, session: &ChatSession) -> Result<()> {
|
|
246
|
+
let base_path = self.config.log_dir.join(&session.session_id);
|
|
247
|
+
|
|
248
|
+
// Save JSON format
|
|
249
|
+
if self.config.json_format {
|
|
250
|
+
let json_path = base_path.with_extension("json");
|
|
251
|
+
self.save_json(session, &json_path)
|
|
252
|
+
.with_context(|| format!("Failed to save JSON log: {:?}", json_path))?;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Save text format
|
|
256
|
+
if self.config.text_format {
|
|
257
|
+
let text_path = base_path.with_extension("txt");
|
|
258
|
+
self.save_text(session, &text_path)
|
|
259
|
+
.with_context(|| format!("Failed to save text log: {:?}", text_path))?;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check and rotate if needed
|
|
263
|
+
self.rotate_logs_if_needed()?;
|
|
264
|
+
|
|
265
|
+
Ok(())
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Save session as JSON
|
|
269
|
+
fn save_json(&self, session: &ChatSession, path: &Path) -> Result<()> {
|
|
270
|
+
let json = serde_json::to_string_pretty(session)
|
|
271
|
+
.with_context(|| "Failed to serialize session to JSON")?;
|
|
272
|
+
|
|
273
|
+
let mut file = OpenOptions::new()
|
|
274
|
+
.create(true)
|
|
275
|
+
.write(true)
|
|
276
|
+
.truncate(true)
|
|
277
|
+
.open(path)
|
|
278
|
+
.with_context(|| format!("Failed to open file: {:?}", path))?;
|
|
279
|
+
|
|
280
|
+
file.write_all(json.as_bytes())
|
|
281
|
+
.with_context(|| format!("Failed to write JSON to file: {:?}", path))?;
|
|
282
|
+
|
|
283
|
+
debug!("Saved JSON log: {:?}", path);
|
|
284
|
+
Ok(())
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Save session as human-readable text
|
|
288
|
+
fn save_text(&self, session: &ChatSession, path: &Path) -> Result<()> {
|
|
289
|
+
let mut file = OpenOptions::new()
|
|
290
|
+
.create(true)
|
|
291
|
+
.write(true)
|
|
292
|
+
.truncate(true)
|
|
293
|
+
.open(path)
|
|
294
|
+
.with_context(|| format!("Failed to open file: {:?}", path))?;
|
|
295
|
+
|
|
296
|
+
// Write header
|
|
297
|
+
let separator = "=".repeat(80);
|
|
298
|
+
writeln!(file, "{}", separator)?;
|
|
299
|
+
writeln!(file, "GROK CLI CHAT SESSION LOG")?;
|
|
300
|
+
writeln!(file, "{}", separator)?;
|
|
301
|
+
writeln!(file, "Session ID: {}", session.session_id)?;
|
|
302
|
+
writeln!(
|
|
303
|
+
file,
|
|
304
|
+
"Start Time: {}",
|
|
305
|
+
session.start_time.format("%Y-%m-%d %H:%M:%S UTC")
|
|
306
|
+
)?;
|
|
307
|
+
if let Some(end_time) = session.end_time {
|
|
308
|
+
writeln!(
|
|
309
|
+
file,
|
|
310
|
+
"End Time: {}",
|
|
311
|
+
end_time.format("%Y-%m-%d %H:%M:%S UTC")
|
|
312
|
+
)?;
|
|
313
|
+
let duration = end_time.signed_duration_since(session.start_time);
|
|
314
|
+
writeln!(file, "Duration: {} seconds", duration.num_seconds())?;
|
|
315
|
+
}
|
|
316
|
+
writeln!(file, "Messages: {}", session.messages.len())?;
|
|
317
|
+
let separator = "=".repeat(80);
|
|
318
|
+
writeln!(file, "{}", separator)?;
|
|
319
|
+
writeln!(file)?;
|
|
320
|
+
|
|
321
|
+
// Write messages
|
|
322
|
+
for (i, msg) in session.messages.iter().enumerate() {
|
|
323
|
+
writeln!(
|
|
324
|
+
file,
|
|
325
|
+
"[{}] {} - {}",
|
|
326
|
+
i + 1,
|
|
327
|
+
msg.role.to_uppercase(),
|
|
328
|
+
msg.timestamp.format("%H:%M:%S")
|
|
329
|
+
)?;
|
|
330
|
+
let line_sep = "-".repeat(80);
|
|
331
|
+
writeln!(file, "{}", line_sep)?;
|
|
332
|
+
writeln!(file, "{}", msg.content)?;
|
|
333
|
+
|
|
334
|
+
if let Some(metadata) = &msg.metadata {
|
|
335
|
+
writeln!(
|
|
336
|
+
file,
|
|
337
|
+
"\nMetadata: {}",
|
|
338
|
+
serde_json::to_string_pretty(metadata).unwrap_or_default()
|
|
339
|
+
)?;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
writeln!(file)?;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Write footer
|
|
346
|
+
let separator = "=".repeat(80);
|
|
347
|
+
writeln!(file, "{}", separator)?;
|
|
348
|
+
writeln!(file, "END OF SESSION")?;
|
|
349
|
+
writeln!(file, "{}", separator)?;
|
|
350
|
+
|
|
351
|
+
debug!("Saved text log: {:?}", path);
|
|
352
|
+
Ok(())
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// Rotate logs if they exceed the size limit
|
|
356
|
+
fn rotate_logs_if_needed(&self) -> Result<()> {
|
|
357
|
+
let max_bytes = self.config.max_file_size_mb * 1024 * 1024;
|
|
358
|
+
|
|
359
|
+
// Get all log files
|
|
360
|
+
let entries = match fs::read_dir(&self.config.log_dir) {
|
|
361
|
+
Ok(entries) => entries,
|
|
362
|
+
Err(e) => {
|
|
363
|
+
warn!("Failed to read log directory for rotation: {}", e);
|
|
364
|
+
return Ok(()); // Don't fail on rotation issues
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
let mut total_size: u64 = 0;
|
|
369
|
+
let mut files: Vec<(PathBuf, u64)> = Vec::new();
|
|
370
|
+
|
|
371
|
+
for entry in entries.flatten() {
|
|
372
|
+
if let Ok(metadata) = entry.metadata()
|
|
373
|
+
&& metadata.is_file()
|
|
374
|
+
{
|
|
375
|
+
let size = metadata.len();
|
|
376
|
+
total_size += size;
|
|
377
|
+
files.push((entry.path(), size));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// If total size exceeds limit, remove oldest files
|
|
382
|
+
if total_size > max_bytes {
|
|
383
|
+
// Sort by modification time (oldest first)
|
|
384
|
+
files.sort_by_key(|(path, _)| {
|
|
385
|
+
fs::metadata(path)
|
|
386
|
+
.and_then(|m| m.modified())
|
|
387
|
+
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Calculate how many to remove
|
|
391
|
+
let files_to_keep = files.len().saturating_sub(self.config.rotation_count);
|
|
392
|
+
|
|
393
|
+
for (path, _) in files.iter().take(files_to_keep) {
|
|
394
|
+
if let Err(e) = fs::remove_file(path) {
|
|
395
|
+
warn!("Failed to remove old log file {:?}: {}", path, e);
|
|
396
|
+
} else {
|
|
397
|
+
debug!("Rotated old log file: {:?}", path);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
Ok(())
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// List all saved sessions
|
|
406
|
+
pub fn list_sessions(&self) -> Result<Vec<String>> {
|
|
407
|
+
let mut sessions = Vec::new();
|
|
408
|
+
|
|
409
|
+
let entries = fs::read_dir(&self.config.log_dir)
|
|
410
|
+
.with_context(|| format!("Failed to read log directory: {:?}", self.config.log_dir))?;
|
|
411
|
+
|
|
412
|
+
for entry in entries.flatten() {
|
|
413
|
+
let path = entry.path();
|
|
414
|
+
if path.extension().and_then(|s| s.to_str()) == Some("json")
|
|
415
|
+
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
|
|
416
|
+
{
|
|
417
|
+
sessions.push(stem.to_string());
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
sessions.sort();
|
|
422
|
+
sessions.reverse(); // Most recent first
|
|
423
|
+
|
|
424
|
+
Ok(sessions)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Load a session from disk
|
|
428
|
+
pub fn load_session(&self, session_id: &str) -> Result<ChatSession> {
|
|
429
|
+
let json_path = self.config.log_dir.join(session_id).with_extension("json");
|
|
430
|
+
|
|
431
|
+
let content = fs::read_to_string(&json_path)
|
|
432
|
+
.with_context(|| format!("Failed to read session file: {:?}", json_path))?;
|
|
433
|
+
|
|
434
|
+
let session: ChatSession = serde_json::from_str(&content)
|
|
435
|
+
.with_context(|| format!("Failed to parse session JSON: {:?}", json_path))?;
|
|
436
|
+
|
|
437
|
+
Ok(session)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Global chat logger instance
|
|
442
|
+
static GLOBAL_LOGGER: Mutex<Option<ChatLogger>> = Mutex::new(None);
|
|
443
|
+
|
|
444
|
+
/// Initialize the global chat logger
|
|
445
|
+
pub fn init(config: ChatLoggerConfig) -> Result<()> {
|
|
446
|
+
let logger = ChatLogger::new(config)?;
|
|
447
|
+
let mut global = GLOBAL_LOGGER.lock().unwrap();
|
|
448
|
+
*global = Some(logger);
|
|
449
|
+
Ok(())
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Get a reference to the global logger (if initialized)
|
|
453
|
+
pub fn get_logger() -> Option<ChatLogger> {
|
|
454
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
455
|
+
global.as_ref().map(|logger| ChatLogger {
|
|
456
|
+
config: logger.config.clone(),
|
|
457
|
+
current_session: Mutex::new(None),
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// Start a new session using the global logger
|
|
462
|
+
pub fn start_session(session_id: impl Into<String>) -> Result<()> {
|
|
463
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
464
|
+
if let Some(logger) = global.as_ref() {
|
|
465
|
+
logger.start_session(session_id)?;
|
|
466
|
+
}
|
|
467
|
+
Ok(())
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Log a user message using the global logger
|
|
471
|
+
pub fn log_user(content: impl Into<String>) -> Result<()> {
|
|
472
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
473
|
+
if let Some(logger) = global.as_ref() {
|
|
474
|
+
logger.log_user_message(content)?;
|
|
475
|
+
}
|
|
476
|
+
Ok(())
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/// Log an assistant message using the global logger
|
|
480
|
+
pub fn log_assistant(content: impl Into<String>) -> Result<()> {
|
|
481
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
482
|
+
if let Some(logger) = global.as_ref() {
|
|
483
|
+
logger.log_assistant_message(content)?;
|
|
484
|
+
}
|
|
485
|
+
Ok(())
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Log a system message using the global logger
|
|
489
|
+
pub fn log_system(content: impl Into<String>) -> Result<()> {
|
|
490
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
491
|
+
if let Some(logger) = global.as_ref() {
|
|
492
|
+
logger.log_system_message(content)?;
|
|
493
|
+
}
|
|
494
|
+
Ok(())
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// End the current session using the global logger
|
|
498
|
+
pub fn end_session() -> Result<()> {
|
|
499
|
+
let global = GLOBAL_LOGGER.lock().unwrap();
|
|
500
|
+
if let Some(logger) = global.as_ref() {
|
|
501
|
+
logger.end_session()?;
|
|
502
|
+
}
|
|
503
|
+
Ok(())
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[cfg(test)]
|
|
507
|
+
mod tests {
|
|
508
|
+
use super::*;
|
|
509
|
+
use tempfile::TempDir;
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn test_chat_session_creation() {
|
|
513
|
+
let session = ChatSession::new("test-session-123");
|
|
514
|
+
assert_eq!(session.session_id, "test-session-123");
|
|
515
|
+
assert!(session.messages.is_empty());
|
|
516
|
+
assert!(session.end_time.is_none());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn test_chat_message_creation() {
|
|
521
|
+
let msg = ChatMessage::new("user", "Hello, world!");
|
|
522
|
+
assert_eq!(msg.role, "user");
|
|
523
|
+
assert_eq!(msg.content, "Hello, world!");
|
|
524
|
+
assert!(msg.metadata.is_none());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[test]
|
|
528
|
+
fn test_session_add_message() {
|
|
529
|
+
let mut session = ChatSession::new("test-session");
|
|
530
|
+
session.add_message(ChatMessage::new("user", "Hello"));
|
|
531
|
+
session.add_message(ChatMessage::new("assistant", "Hi there!"));
|
|
532
|
+
assert_eq!(session.messages.len(), 2);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#[test]
|
|
536
|
+
fn test_logger_initialization() {
|
|
537
|
+
let temp_dir = TempDir::new().unwrap();
|
|
538
|
+
let config = ChatLoggerConfig {
|
|
539
|
+
enabled: true,
|
|
540
|
+
log_dir: temp_dir.path().to_path_buf(),
|
|
541
|
+
..Default::default()
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
let logger = ChatLogger::new(config).unwrap();
|
|
545
|
+
assert!(temp_dir.path().exists());
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn test_session_lifecycle() {
|
|
550
|
+
let temp_dir = TempDir::new().unwrap();
|
|
551
|
+
let config = ChatLoggerConfig {
|
|
552
|
+
enabled: true,
|
|
553
|
+
log_dir: temp_dir.path().to_path_buf(),
|
|
554
|
+
..Default::default()
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
let logger = ChatLogger::new(config).unwrap();
|
|
558
|
+
|
|
559
|
+
logger.start_session("test-lifecycle").unwrap();
|
|
560
|
+
logger.log_user_message("Test message").unwrap();
|
|
561
|
+
logger.log_assistant_message("Test response").unwrap();
|
|
562
|
+
logger.end_session().unwrap();
|
|
563
|
+
|
|
564
|
+
// Check that files were created
|
|
565
|
+
assert!(temp_dir.path().join("test-lifecycle.json").exists());
|
|
566
|
+
assert!(temp_dir.path().join("test-lifecycle.txt").exists());
|
|
567
|
+
}
|
|
568
|
+
}
|