use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tokio::sync::Mutex as TokioMutex;
use super::context_absorber::ContextAbsorber;
use super::smart_background_searcher::{SearchConfig, SmartBackgroundSearcher};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedWatcherConfig {
pub project_name: String,
pub watch_paths: Vec<String>,
pub enable_absorption: bool,
pub enable_search: bool,
pub enable_logging: bool,
pub auto_start: bool,
}
impl Default for UnifiedWatcherConfig {
fn default() -> Self {
Self {
project_name: "smart-tree".to_string(),
watch_paths: vec![
"~/Documents/".to_string(),
"~/.config/".to_string(),
"~/Library/Application Support/Claude/".to_string(),
"~/.cursor/".to_string(),
"~/.vscode/".to_string(),
],
enable_absorption: true,
enable_search: true,
enable_logging: true,
auto_start: false,
}
}
}
pub struct UnifiedWatcher {
config: UnifiedWatcherConfig,
absorber: Option<Arc<Mutex<ContextAbsorber>>>,
searcher: Option<Arc<TokioMutex<SmartBackgroundSearcher>>>,
status: Arc<Mutex<WatcherStatus>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatcherStatus {
pub is_running: bool,
pub files_watched: usize,
pub contexts_absorbed: usize,
pub search_results_cached: usize,
pub last_activity: Option<String>,
pub watched_directories: Vec<String>,
}
impl UnifiedWatcher {
pub fn new(config: UnifiedWatcherConfig) -> Result<Self> {
let status = Arc::new(Mutex::new(WatcherStatus {
is_running: false,
files_watched: 0,
contexts_absorbed: 0,
search_results_cached: 0,
last_activity: None,
watched_directories: config.watch_paths.clone(),
}));
Ok(Self {
config,
absorber: None,
searcher: None,
status,
})
}
pub async fn start(&mut self) -> Result<()> {
println!(
"🚀 Starting Unified Watcher for project: {}",
self.config.project_name
);
if self.config.enable_logging {
crate::activity_logger::ActivityLogger::init(Some("~/.st/watcher.jsonl".to_string()))?;
crate::activity_logger::ActivityLogger::log_event(
"watcher",
"start",
serde_json::json!({
"project": self.config.project_name,
"watch_paths": self.config.watch_paths,
}),
)?;
}
let watch_paths: Vec<PathBuf> = self
.config
.watch_paths
.iter()
.map(|p| PathBuf::from(shellexpand::tilde(p).to_string()))
.filter(|p| p.exists())
.collect();
if self.config.enable_absorption {
println!("🧽 Starting Context Absorber...");
let mut absorber = ContextAbsorber::new(self.config.project_name.clone())?;
absorber.start_watching()?;
self.absorber = Some(Arc::new(Mutex::new(absorber)));
println!(" ✅ Context Absorber active");
}
if self.config.enable_search {
println!("🔍 Starting Smart Background Searcher...");
let search_config = SearchConfig {
max_lines_per_file: 1000, smart_sampling: true,
..Default::default()
};
let mut searcher = SmartBackgroundSearcher::new(search_config)?;
searcher.start_watching(watch_paths.clone())?;
self.searcher = Some(Arc::new(TokioMutex::new(searcher)));
println!(" ✅ Smart Searcher active");
}
if let Ok(mut status) = self.status.lock() {
status.is_running = true;
status.watched_directories = watch_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
status.last_activity = Some(format!("Started watching at {}", chrono::Utc::now()));
}
self.start_monitor_thread();
println!("\n✨ Unified Watcher is now active!");
println!("📂 Watching {} directories", watch_paths.len());
println!("🎯 Project: {}", self.config.project_name);
Ok(())
}
fn start_monitor_thread(&self) {
let status = self.status.clone();
let absorber = self.absorber.clone();
let _searcher = self.searcher.clone();
thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(30));
if let Ok(mut stat) = status.lock() {
if let Some(abs) = &absorber {
if let Ok(abs_lock) = abs.lock() {
stat.contexts_absorbed = abs_lock.get_absorbed_contexts().len();
}
}
stat.last_activity = Some(format!("Active at {}", chrono::Utc::now()));
}
}
});
}
pub async fn stop(&mut self) -> Result<()> {
println!("🛑 Stopping Unified Watcher...");
if let Some(abs) = &self.absorber {
if let Ok(mut abs_lock) = abs.lock() {
abs_lock.stop_watching();
}
}
if let Some(search) = &self.searcher {
let search_lock = search.lock().await;
search_lock.clear_cache();
}
if let Ok(mut status) = self.status.lock() {
status.is_running = false;
status.last_activity = Some(format!("Stopped at {}", chrono::Utc::now()));
}
if self.config.enable_logging {
crate::activity_logger::ActivityLogger::log_event(
"watcher",
"stop",
serde_json::json!({
"project": self.config.project_name,
}),
)?;
}
Ok(())
}
pub async fn search(&self, query: &str) -> Result<Vec<Value>> {
if let Some(searcher) = &self.searcher {
let search_lock = searcher.lock().await;
let paths: Vec<PathBuf> = self
.config
.watch_paths
.iter()
.map(|p| PathBuf::from(shellexpand::tilde(p).to_string()))
.collect();
let results = search_lock.search(query, paths).await;
let json_results: Vec<Value> = results
.into_iter()
.map(|r| {
serde_json::json!({
"file": r.file_path.to_string_lossy(),
"line": r.line_number,
"content": r.content,
"score": r.score,
"type": r.file_type,
})
})
.collect();
return Ok(json_results);
}
Ok(Vec::new())
}
pub fn get_status(&self) -> WatcherStatus {
self.status.lock().unwrap().clone()
}
}
pub async fn handle_unified_watcher(
params: Value,
_ctx: Arc<crate::mcp::McpContext>,
) -> Result<Value> {
let action = params["action"].as_str().unwrap_or("status");
static WATCHER: Lazy<Arc<TokioMutex<Option<UnifiedWatcher>>>> =
Lazy::new(|| Arc::new(TokioMutex::new(None)));
match action {
"start" => {
let project = params["project"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_os_string()))
.and_then(|n| n.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown".to_string())
});
let watch_paths = params["paths"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_else(|| UnifiedWatcherConfig::default().watch_paths);
let config = UnifiedWatcherConfig {
project_name: project.to_string(),
watch_paths,
enable_absorption: params["enable_absorption"].as_bool().unwrap_or(true),
enable_search: params["enable_search"].as_bool().unwrap_or(true),
enable_logging: params["enable_logging"].as_bool().unwrap_or(true),
auto_start: false,
};
let mut watcher = UnifiedWatcher::new(config)?;
watcher.start().await?;
let status = watcher.get_status();
*WATCHER.lock().await = Some(watcher);
Ok(serde_json::json!({
"status": "started",
"project": project,
"watching": status.watched_directories,
"features": {
"absorption": params["enable_absorption"].as_bool().unwrap_or(true),
"search": params["enable_search"].as_bool().unwrap_or(true),
"logging": params["enable_logging"].as_bool().unwrap_or(true),
},
"message": format!("🚀 Unified Watcher active for '{}'", project)
}))
}
"stop" => {
if let Some(mut watcher) = WATCHER.lock().await.take() {
watcher.stop().await?;
Ok(serde_json::json!({
"status": "stopped",
"message": "Watcher stopped successfully"
}))
} else {
Ok(serde_json::json!({
"status": "not_running",
"message": "No watcher is currently running"
}))
}
}
"search" => {
let query = params["query"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing query parameter"))?;
let guard = WATCHER.lock().await;
if let Some(watcher) = guard.as_ref() {
let results = watcher.search(query).await?;
Ok(serde_json::json!({
"query": query,
"results": results,
"count": results.len(),
}))
} else {
Ok(serde_json::json!({
"error": "Watcher not running",
"message": "Start the watcher first with action: 'start'"
}))
}
}
"status" => {
let guard = WATCHER.lock().await;
if let Some(watcher) = guard.as_ref() {
let status = watcher.get_status();
Ok(serde_json::json!({
"running": status.is_running,
"files_watched": status.files_watched,
"contexts_absorbed": status.contexts_absorbed,
"search_results_cached": status.search_results_cached,
"last_activity": status.last_activity,
"watched_directories": status.watched_directories,
}))
} else {
Ok(serde_json::json!({
"running": false,
"message": "No watcher configured"
}))
}
}
_ => Err(anyhow::anyhow!(
"Unknown action: {}. Valid actions: start, stop, search, status",
action
)),
}
}
use once_cell::sync::Lazy;