use anyhow::{Context, Result};
use chrono::Local;
use serde_json::{json, Value};
use std::fs;
use std::io::{self, Write as IoWrite};
use std::path::{Path, PathBuf};
use crate::scanner::{Scanner, ScannerConfig};
use crate::TreeStats;
const VALID_HOOK_KEYS: &[&str] = &[
"SessionStart",
"UserPromptSubmit",
"PreToolUse",
"PermissionRequest",
"PostToolUse",
"PostToolUseFailure",
"SubagentStart",
"SubagentStop",
"Stop",
"PreCompact",
"SessionEnd",
"Notification",
"Setup",
];
fn confirm_overwrite(path: &Path) -> bool {
print!(" ⚠️ {} exists. Overwrite? [y/N]: ", path.display());
io::stdout().flush().unwrap();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
let response = input.trim().to_lowercase();
return response == "y" || response == "yes";
}
false
}
pub fn validate_settings(path: &Path) -> Result<Option<String>> {
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(path)?;
let parsed: Result<Value, _> = serde_json::from_str(&content);
match parsed {
Err(e) => Ok(Some(format!("Invalid JSON: {}", e))),
Ok(json) => {
if let Some(hooks) = json.get("hooks") {
if let Some(obj) = hooks.as_object() {
for key in obj.keys() {
if !VALID_HOOK_KEYS.contains(&key.as_str()) {
return Ok(Some(format!(
"Invalid hook key '{}'. Valid: {}",
key,
VALID_HOOK_KEYS.join(", ")
)));
}
}
}
}
Ok(None)
}
}
}
#[derive(Debug, Clone)]
pub enum ProjectType {
Rust,
Python,
JavaScript,
TypeScript,
Mixed,
Unknown,
}
pub struct ClaudeInit {
project_path: PathBuf,
project_type: ProjectType,
stats: TreeStats,
}
impl ClaudeInit {
pub fn new(project_path: PathBuf) -> Result<Self> {
let config = ScannerConfig {
max_depth: 3,
show_hidden: false,
follow_symlinks: false,
..Default::default()
};
let scanner = Scanner::new(&project_path, config)?;
let (nodes, stats) = scanner.scan()?;
let project_type = Self::detect_project_type(&nodes, &stats);
Ok(Self {
project_path,
project_type,
stats,
})
}
fn detect_project_type(nodes: &[crate::FileNode], _stats: &TreeStats) -> ProjectType {
let mut rust_score = 0;
let mut python_score = 0;
let mut js_score = 0;
let mut ts_score = 0;
for node in nodes {
let path_str = node.path.to_string_lossy();
if path_str.contains("Cargo.toml") {
rust_score += 100;
}
if path_str.contains("package.json") {
js_score += 50;
ts_score += 30;
}
if path_str.contains("pyproject.toml") || path_str.contains("requirements.txt") {
python_score += 100;
}
if path_str.contains("tsconfig.json") {
ts_score += 100;
}
if path_str.ends_with(".rs") {
rust_score += 1;
}
if path_str.ends_with(".py") {
python_score += 1;
}
if path_str.ends_with(".js") || path_str.ends_with(".jsx") {
js_score += 1;
}
if path_str.ends_with(".ts") || path_str.ends_with(".tsx") {
ts_score += 1;
}
}
let max_score = rust_score.max(python_score).max(js_score).max(ts_score);
if max_score == 0 {
ProjectType::Unknown
} else if rust_score == max_score {
ProjectType::Rust
} else if python_score == max_score {
ProjectType::Python
} else if ts_score == max_score {
ProjectType::TypeScript
} else if js_score == max_score {
ProjectType::JavaScript
} else {
ProjectType::Mixed
}
}
pub fn setup(&self) -> Result<()> {
let claude_dir = self.project_path.join(".claude");
if claude_dir.exists() {
self.update_existing(&claude_dir)
} else {
self.init_new(&claude_dir)
}
}
fn init_new(&self, claude_dir: &Path) -> Result<()> {
fs::create_dir_all(claude_dir).context("Failed to create .claude directory")?;
self.create_settings_json(claude_dir, true)?;
self.create_claude_md(claude_dir, true)?;
println!(
"✨ Claude integration initialized for {:?} project!",
self.project_type
);
println!("📁 Created .claude/ directory with:");
println!(" • settings.json - Smart hooks configured");
println!(" • CLAUDE.md - Project-specific AI guidance");
println!("\n💡 Tip: Run 'st --setup-claude' anytime to update");
Ok(())
}
fn update_existing(&self, claude_dir: &Path) -> Result<()> {
println!("🔄 Checking existing Claude integration...");
let settings_path = claude_dir.join("settings.json");
let claude_md_path = claude_dir.join("CLAUDE.md");
let mut updated = false;
if settings_path.exists() {
if let Some(error) = validate_settings(&settings_path)? {
println!(" ⚠️ settings.json has issues: {}", error);
println!(" 💡 Suggested fix:");
self.show_suggested()?;
return Ok(());
}
let existing: Value = serde_json::from_str(&fs::read_to_string(&settings_path)?)?;
let is_auto = existing
.get("smart_tree")
.and_then(|st| st.get("auto_configured"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if is_auto {
if self.create_settings_json(claude_dir, true)? {
println!(" ✅ Updated settings.json");
updated = true;
}
} else {
println!(" ℹ️ settings.json has manual configuration");
if self.create_settings_json(claude_dir, false)? {
println!(" ✅ Updated settings.json");
updated = true;
} else {
println!(" ⏭️ Skipped settings.json");
}
}
} else if self.create_settings_json(claude_dir, true)? {
println!(" ✅ Created settings.json");
updated = true;
}
if claude_md_path.exists() {
if self.create_claude_md(claude_dir, false)? {
println!(" ✅ Updated CLAUDE.md");
updated = true;
} else {
println!(" ⏭️ Skipped CLAUDE.md");
}
} else if self.create_claude_md(claude_dir, true)? {
println!(" ✅ Created CLAUDE.md");
updated = true;
}
if updated {
println!(
"\n🎉 Claude integration updated for {:?} project!",
self.project_type
);
} else {
println!("\n✨ No changes made. Use --force to overwrite.");
}
Ok(())
}
fn create_settings_json(&self, claude_dir: &Path, force: bool) -> Result<bool> {
let settings_path = claude_dir.join("settings.json");
if settings_path.exists() && !force {
if !confirm_overwrite(&settings_path) {
return Ok(false);
}
let backup = settings_path.with_extension("json.bak");
fs::copy(&settings_path, &backup)?;
}
let hooks = match self.project_type {
ProjectType::Rust => {
json!({
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-restore"
}]
}],
"SessionEnd": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-save"
}]
}],
"PreToolUse": [{
"matcher": "cargo (build|test|run)",
"hooks": [{
"type": "command",
"command": "st -m summary --depth 3 ."
}]
}]
})
}
ProjectType::Python => {
json!({
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-restore"
}]
}],
"SessionEnd": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-save"
}]
}],
"PreToolUse": [{
"matcher": "pytest|python.*test",
"hooks": [{
"type": "command",
"command": "st -m summary --depth 3 ."
}]
}]
})
}
ProjectType::JavaScript | ProjectType::TypeScript => {
json!({
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-restore"
}]
}],
"SessionEnd": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-save"
}]
}],
"PreToolUse": [{
"matcher": "npm (test|build|run)",
"hooks": [{
"type": "command",
"command": "st -m summary --depth 3 ."
}]
}]
})
}
_ => {
json!({
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-restore"
}]
}],
"SessionEnd": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "st --claude-save"
}]
}]
})
}
};
let settings = json!({
"hooks": hooks,
"smart_tree": {
"version": env!("CARGO_PKG_VERSION"),
"project_type": format!("{:?}", self.project_type),
"auto_configured": true,
"stats": {
"files": self.stats.total_files,
"directories": self.stats.total_dirs,
"size": self.stats.total_size
}
}
});
let content = serde_json::to_string_pretty(&settings)?;
fs::write(&settings_path, &content)?;
if let Some(error) = validate_settings(&settings_path)? {
let backup = settings_path.with_extension("json.bak");
if backup.exists() {
fs::copy(&backup, &settings_path)?;
fs::remove_file(&backup)?;
}
anyhow::bail!("Validation failed, reverted: {}", error);
}
Ok(true)
}
fn create_claude_md(&self, claude_dir: &Path, force: bool) -> Result<bool> {
let claude_md_path = claude_dir.join("CLAUDE.md");
if claude_md_path.exists() && !force && !confirm_overwrite(&claude_md_path) {
return Ok(false);
}
let content = match self.project_type {
ProjectType::Rust => {
format!(
r#"# CLAUDE.md
This Rust project uses Smart Tree for optimal AI context management.
## Project Stats
- Files: {}
- Directories: {}
- Total size: {} bytes
## Essential Commands
```bash
# Build & Test
cargo build --release
cargo test -- --nocapture
cargo clippy -- -D warnings
# Smart Tree context
st -m context . # Full context with git info
st -m quantum . # Compressed for large contexts
st -m relations --focus main.rs # Code relationships
```
## Key Patterns
- Always use `Result<T>` for error handling
- Prefer `&str` over `String` for function parameters
- Use `anyhow` for error context
- Run clippy before commits
## Smart Tree Integration
This project has hooks configured to automatically provide context.
The quantum-semantic mode is used for optimal token efficiency.
"#,
self.stats.total_files, self.stats.total_dirs, self.stats.total_size
)
}
ProjectType::Python => {
format!(
r#"# CLAUDE.md
This Python project uses Smart Tree for optimal AI context management.
## Project Stats
- Files: {}
- Directories: {}
- Total size: {} bytes
## Essential Commands
```bash
# Environment & Testing
uv sync # Install dependencies with uv
pytest -v # Run tests
ruff check . # Lint code
mypy . # Type checking
# Smart Tree context
st -m context . # Full context with git info
st -m quantum . # Compressed for large contexts
```
## Key Patterns
- Use type hints for all functions
- Prefer uv over pip for package management
- Follow PEP 8 style guide
- Write docstrings for all public functions
## Smart Tree Integration
Hooks provide automatic context on prompt submission.
Test runs trigger summary of test directories.
"#,
self.stats.total_files, self.stats.total_dirs, self.stats.total_size
)
}
ProjectType::TypeScript | ProjectType::JavaScript => {
format!(
r#"# CLAUDE.md
This {0} project uses Smart Tree for optimal AI context management.
## Project Stats
- Files: {1}
- Directories: {2}
- Total size: {3} bytes
## Essential Commands
```bash
# Development
pnpm install # Install dependencies
pnpm run dev # Start dev server
pnpm test # Run tests
pnpm build # Production build
# Smart Tree context
st -m context . # Full context with git info
st -m quantum . # Compressed for large contexts
```
## Key Patterns
- Use pnpm for package management
- Implement proper TypeScript types
- Follow ESLint rules
- Component-based architecture
## Smart Tree Integration
Automatic context provision via hooks.
Node_modules excluded from summaries.
"#,
if matches!(self.project_type, ProjectType::TypeScript) {
"TypeScript"
} else {
"JavaScript"
},
self.stats.total_files,
self.stats.total_dirs,
self.stats.total_size
)
}
_ => {
format!(
r#"# CLAUDE.md
This project uses Smart Tree for optimal AI context management.
## Project Stats
- Files: {}
- Directories: {}
- Total size: {} bytes
- Type: {:?}
## Smart Tree Commands
```bash
st -m context . # Full context with git info
st -m quantum . # Compressed for large contexts
st -m summary . # Human-readable summary
st -m quantum-semantic . # Maximum compression
```
## Smart Tree Integration
This project has been configured with automatic hooks that provide
context to Claude on every prompt. The hook mode is optimized based
on your project size.
Use `st --help` to explore more features!
"#,
self.stats.total_files,
self.stats.total_dirs,
self.stats.total_size,
self.project_type
)
}
};
fs::write(claude_md_path, content)?;
Ok(true)
}
pub fn show_suggested(&self) -> Result<()> {
println!(
"📋 Suggested Claude integration for {:?} project:\n",
self.project_type
);
let hooks = match self.project_type {
ProjectType::Rust => json!({
"SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
"SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
"PreToolUse": [{"matcher": "cargo (build|test|run)", "hooks": [{"type": "command", "command": "st -m summary --depth 1 target/"}]}]
}),
ProjectType::Python => json!({
"SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
"SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
"PreToolUse": [{"matcher": "pytest|python.*test", "hooks": [{"type": "command", "command": "st -m summary --depth 2 tests/"}]}]
}),
_ => json!({
"SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
"SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}]
}),
};
let settings = json!({"hooks": hooks});
println!("━━━ Add to .claude/settings.json ━━━");
println!("{}\n", serde_json::to_string_pretty(&settings)?);
println!("💡 Or run: st --setup-claude (will ask before overwriting)");
Ok(())
}
}
#[derive(Debug)]
pub struct McpInstallResult {
pub success: bool,
pub config_path: PathBuf,
pub backup_path: Option<PathBuf>,
pub message: String,
pub was_update: bool,
}
pub struct McpInstaller {
st_binary_path: PathBuf,
custom_config_path: Option<PathBuf>,
}
impl McpInstaller {
pub fn new() -> Result<Self> {
let st_binary_path = Self::find_st_binary()?;
Ok(Self {
st_binary_path,
custom_config_path: None,
})
}
pub fn with_binary_path(path: PathBuf) -> Self {
Self {
st_binary_path: path,
custom_config_path: None,
}
}
pub fn with_config_path(mut self, path: PathBuf) -> Self {
self.custom_config_path = Some(path);
self
}
fn find_st_binary() -> Result<PathBuf> {
if let Ok(exe) = std::env::current_exe() {
if exe.file_name().map(|n| n == "st").unwrap_or(false) {
return Ok(exe);
}
}
let candidates = vec![
dirs::home_dir().map(|h| h.join(".cargo/bin/st")),
Some(PathBuf::from("/usr/local/bin/st")),
Some(PathBuf::from("/opt/homebrew/bin/st")),
];
for candidate in candidates.into_iter().flatten() {
if candidate.exists() {
return Ok(candidate);
}
}
Ok(PathBuf::from("st"))
}
pub fn get_all_target_configs() -> Vec<(&'static str, PathBuf)> {
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
if let Some(h) = dirs::home_dir() {
paths.push(("Claude Desktop", h.join("Library/Application Support/Claude/claude_desktop_config.json")));
}
#[cfg(target_os = "windows")]
if let Some(c) = dirs::config_dir() {
paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
}
#[cfg(target_os = "linux")]
if let Some(c) = dirs::config_dir() {
paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
}
if let Some(h) = dirs::home_dir() {
paths.push(("Antigravity", h.join(".gemini/antigravity/mcp_config.json")));
paths.push(("Gemini", h.join(".gemini/mcp_config.json")));
}
paths
}
pub fn install_all(&self) -> Result<Vec<McpInstallResult>> {
let targets = if let Some(custom) = &self.custom_config_path {
vec![("Custom", custom.clone())]
} else {
Self::get_all_target_configs()
};
if targets.is_empty() {
anyhow::bail!("No supported agent configurations found for this OS.");
}
let mut results = Vec::new();
for (agent_name, config_path) in targets {
if let Some(parent) = config_path.parent() {
if fs::create_dir_all(parent).is_err() {
continue; }
}
let (mut config, was_update) = if config_path.exists() {
if let Ok(content) = fs::read_to_string(&config_path) {
if let Ok(json_val) = serde_json::from_str::<Value>(&content) {
(json_val, true)
} else {
(json!({}), false)
}
} else {
(json!({}), false)
}
} else {
(json!({}), false)
};
let backup_path = if was_update {
let backup = config_path.with_extension(format!(
"json.backup.{}",
Local::now().format("%Y%m%d_%H%M%S")
));
let _ = fs::copy(&config_path, &backup);
Some(backup)
} else {
None
};
let st_config = json!({
"command": self.st_binary_path.to_string_lossy(),
"args": ["--mcp"],
"env": {}
});
if config.get("mcpServers").is_none() {
config["mcpServers"] = json!({});
}
let already_installed = config["mcpServers"].get("smart-tree").is_some();
config["mcpServers"]["smart-tree"] = st_config;
if let Ok(formatted) = serde_json::to_string_pretty(&config) {
if fs::write(&config_path, formatted).is_err() {
continue; }
}
let message = if already_installed {
format!(
"✨ Updated Smart Tree MCP server in {}!\n\
📁 Config: {}\n\
🔧 Binary: {}",
agent_name,
config_path.display(),
self.st_binary_path.display()
)
} else {
format!(
"🎉 Smart Tree MCP server installed to {}!\n\
📁 Config: {}\n\
🔧 Binary: {}",
agent_name,
config_path.display(),
self.st_binary_path.display()
)
};
results.push(McpInstallResult {
success: true,
config_path,
backup_path,
message,
was_update: already_installed,
});
}
Ok(results)
}
pub fn uninstall_all(&self) -> Result<Vec<McpInstallResult>> {
let targets = if let Some(custom) = &self.custom_config_path {
vec![("Custom", custom.clone())]
} else {
Self::get_all_target_configs()
};
let mut results = Vec::new();
for (agent_name, config_path) in targets {
if !config_path.exists() {
continue;
}
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(_) => continue,
};
let mut config: Value = match serde_json::from_str(&content) {
Ok(c) => c,
Err(_) => continue,
};
let was_removed = if let Some(servers) = config.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
servers.remove("smart-tree").is_some()
} else {
false
};
if was_removed {
let backup = config_path.with_extension(format!(
"json.backup.{}",
Local::now().format("%Y%m%d_%H%M%S")
));
let _ = fs::copy(&config_path, &backup);
if let Ok(formatted) = serde_json::to_string_pretty(&config) {
let _ = fs::write(&config_path, formatted);
}
results.push(McpInstallResult {
success: true,
config_path: config_path.clone(),
backup_path: Some(backup),
message: format!(
"🗑️ Removed Smart Tree MCP server from {}.\n\
📁 Config: {}",
agent_name,
config_path.display()
),
was_update: true,
});
}
}
Ok(results)
}
pub fn is_installed(&self) -> Result<bool> {
let targets = if let Some(custom) = &self.custom_config_path {
vec![("Custom", custom.clone())]
} else {
Self::get_all_target_configs()
};
for (_, path) in targets {
if path.exists() {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(config) = serde_json::from_str::<Value>(&content) {
if config["mcpServers"].get("smart-tree").is_some() {
return Ok(true);
}
}
}
}
}
Ok(false)
}
pub fn status(&self) -> Result<Value> {
let targets = Self::get_all_target_configs();
let is_installed = self.is_installed().unwrap_or(false);
let paths: Vec<String> = targets.into_iter()
.map(|(_, p)| p.display().to_string())
.collect();
Ok(json!({
"installed": is_installed,
"config_paths": paths,
"binary_path": self.st_binary_path.display().to_string(),
"binary_exists": self.st_binary_path.exists(),
}))
}
}
impl Default for McpInstaller {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
st_binary_path: PathBuf::from("st"),
custom_config_path: None,
})
}
}
pub fn install_mcp_to_desktop() -> Result<String> {
let installer = McpInstaller::new()?;
let results = installer.install_all()?;
let msg = results.into_iter()
.filter(|r| r.success)
.map(|r| r.message)
.collect::<Vec<_>>()
.join("\n\n");
if msg.is_empty() {
Ok("Nothing to install or update.".to_string())
} else {
Ok(msg)
}
}
pub fn uninstall_mcp_from_desktop() -> Result<String> {
let installer = McpInstaller::new()?;
let results = installer.uninstall_all()?;
let msg = results.into_iter()
.filter(|r| r.success)
.map(|r| r.message)
.collect::<Vec<_>>()
.join("\n\n");
if msg.is_empty() {
Ok("No installations found to remove.".to_string())
} else {
Ok(msg)
}
}
pub fn check_mcp_installation_status() -> Result<String> {
let installer = McpInstaller::new()?;
let status = installer.status()?;
let installed = status["installed"].as_bool().unwrap_or(false);
let config_paths = status["config_paths"].as_array();
if installed {
Ok(format!(
"✅ Smart Tree MCP server is installed!\n\
📁 Configs: {:?}\n\
🔧 Binary: {}",
config_paths,
status["binary_path"].as_str().unwrap_or("st")
))
} else {
Ok(format!(
"❌ Smart Tree MCP server is NOT installed.\n\
📁 Expected configs: {:?}\n\
💡 Run 'st --mcp-install' to install",
config_paths
))
}
}