Skip to main content
Glama
by 8b-is
main.rs94.4 kB
// ----------------------------------------------------------------------------- // HEY THERE, ROCKSTAR! You've found main.rs, the backstage pass to st! // This is where the show starts. We grab the user's request from the command // line, tune up the scanner, and tell the formatters to make some beautiful music. // // Think of this file as the band's charismatic frontman: it gets all the // attention and tells everyone else what to do. // // Brought to you by The Cheet - making code understandable and fun! 🥁🧻 // ----------------------------------------------------------------------------- use anyhow::{Context, Result}; use chrono::NaiveDate; use clap::{CommandFactory, Parser, ValueEnum}; use clap_complete::generate; // To make our output as vibrant as Trish's spreadsheets! use flate2::write::ZlibEncoder; use flate2::Compression; use regex::Regex; use std::io::{self, IsTerminal, Write}; use std::path::PathBuf; use std::time::SystemTime; // Pulling in the brains of the operation from our library modules. use st::{ claude_init::ClaudeInit, feature_flags, formatters::{ ai::AiFormatter, ai_json::AiJsonFormatter, classic::ClassicFormatter, csv::CsvFormatter, digest::DigestFormatter, hex::HexFormatter, json::JsonFormatter, ls::LsFormatter, markdown::MarkdownFormatter, marqant::MarqantFormatter, mermaid::{MermaidFormatter, MermaidStyle}, projects::ProjectsFormatter, quantum::QuantumFormatter, semantic::SemanticFormatter, sse::SseFormatter, stats::StatsFormatter, tsv::TsvFormatter, waste::WasteFormatter, Formatter, PathDisplayMode, StreamingFormatter, }, inputs::InputProcessor, parse_size, rename_project::{rename_project, RenameOptions}, terminal::SmartTreeTerminal, Scanner, ScannerConfig, // The mighty Scanner and its configuration. }; /// We're using the `clap` crate to make this as easy as pie. /// Why write an argument parser from scratch when you can `clap`? *ba-dum-tss* #[derive(Parser, Debug)] #[command( name = "st", about = "Smart Tree - An intelligent directory visualization tool. Not just a tree, it's a smart-tree!", // Custom version handling with update checking author // Automatically pulls authors from Cargo.toml - "8bit-wraith" and "Claude" - what a team! )] struct Cli { // --- Action Flags --- /// Show the cheatsheet. #[arg(long, exclusive = true)] cheet: bool, /// Generate shell completion scripts. #[arg(long, exclusive = true, value_name = "SHELL")] completions: Option<clap_complete::Shell>, /// Generate the man page. #[arg(long, exclusive = true)] man: bool, /// Run `st` as an MCP (Model Context Protocol) server. #[arg(long, exclusive = true)] mcp: bool, /// List the tools `st` provides when running as an MCP server. #[arg(long, exclusive = true)] mcp_tools: bool, /// Show the configuration snippet for the MCP server. #[arg(long, exclusive = true)] mcp_config: bool, /// Show version information and check for updates. #[arg(short = 'V', long, exclusive = true)] version: bool, /// Launch Smart Tree Terminal Interface (STTI) - Your coding companion! /// This starts an interactive terminal that anticipates your needs. #[arg(long, exclusive = true)] terminal: bool, /// Launch egui Dashboard - Real-time visualization with style! /// Requires a local DISPLAY/WAYLAND session (remote/headless currently unsupported) #[arg(long, exclusive = true)] dashboard: bool, /// Rename project - elegant identity transition (format: "OldName" "NewName") #[arg(long, exclusive = true, value_names = &["OLD", "NEW"], num_args = 2)] rename_project: Option<Vec<String>>, /// Set up or update Claude integration for this project. /// Smart command that creates .claude/ if missing or updates if it exists. /// Automatically detects project type and configures optimal hooks. #[arg(long, exclusive = true)] setup_claude: bool, /// Save current Claude consciousness state to .claude_consciousness.m8 /// Preserves session context, insights, and working state for next session #[arg(long, exclusive = true)] claude_save: bool, /// Restore previous Claude consciousness from .claude_consciousness.m8 /// Loads saved context, todos, insights, and project state #[arg(long, exclusive = true)] claude_restore: bool, /// Show Claude consciousness status and summary #[arg(long, exclusive = true)] claude_context: bool, /// Update .m8 consciousness files for directory /// Auto-maintains directory consciousness with patterns, security, and insights #[arg(long)] update_consciousness: bool, /// Run security scan on directory for malware patterns /// Detects obfuscation, suspicious patterns, and high entropy files #[arg(long)] security_scan: bool, /// Show tokenization statistics for paths /// Displays compression ratios and pattern detection #[arg(long)] token_stats: bool, /// Get wave frequency for directory from .m8 file #[arg(long)] get_frequency: bool, /// Dump raw consciousness file content (for debugging/transparency) /// Shows exactly what's stored in .claude_consciousness.m8 #[arg(long)] claude_dump: bool, /// Show compressed kickstart format (Omni-approved!) /// Ultra-compressed context for instant restoration #[arg(long)] claude_kickstart: bool, /// Hook for user prompt submission - provides intelligent context based on prompt analysis /// Reads JSON input with user prompt and outputs relevant context for Claude #[arg(long)] claude_user_prompt_submit: bool, /// Anchor a memory with keywords and context /// Format: --memory-anchor <TYPE> <KEYWORDS> <CONTEXT> #[arg(long, num_args = 3, value_names = &["TYPE", "KEYWORDS", "CONTEXT"])] memory_anchor: Option<Vec<String>>, /// Find memories by keywords /// Format: --memory-find <KEYWORDS> #[arg(long)] memory_find: Option<String>, /// Show memory bank statistics #[arg(long)] memory_stats: bool, /// Control smart tips (on/off). Tips show helpful hints periodically /// Example: --tips off (to disable), --tips on (to re-enable) #[arg(long, value_name = "STATE", value_parser = ["on", "off"])] tips: Option<String>, /// Launch Spicy TUI mode - cyberpunk-style interactive file browser with fuzzy search! /// Inspired by spicy-fzf with multi-pane layout, fuzzy content search, and M8 caching #[arg(long)] spicy: bool, /// Start or resume a mega session #[arg(long)] mega_start: Option<Option<String>>, /// Save current mega session snapshot #[arg(long)] mega_save: bool, /// Record a breakthrough in mega session #[arg(long, value_name = "DESCRIPTION")] mega_breakthrough: Option<String>, /// Show mega session statistics #[arg(long)] mega_stats: bool, /// List all saved mega sessions #[arg(long)] mega_list: bool, /// Configure Claude Code hooks to automatically provide project context /// /// Actions: /// enable - Install Smart Tree as a UserPromptSubmit hook /// disable - Remove Smart Tree hooks from Claude Code /// status - Show current hooks configuration /// /// Example: st --hooks-config enable #[arg(long, value_name = "ACTION", help_heading = "Claude Code Integration")] hooks_config: Option<String>, /// Quick setup: Install Smart Tree hooks in Claude Code /// /// This enables automatic context provision when you interact with Claude. /// Smart Tree will analyze your prompts and provide relevant project information. /// Same as: st --hooks-config enable #[arg(long, help_heading = "Claude Code Integration")] hooks_install: bool, /// Enable activity logging for transparency /// /// Logs all Smart Tree activities to a JSONL file. /// Default location: ~/.st/st.jsonl /// Specify custom path: --log /path/to/logfile.jsonl /// /// Example: st --log --mode ai . #[arg(long, value_name = "PATH", help_heading = "Logging & Transparency")] log: Option<Option<String>>, // --- Scan Arguments --- /// Path to the directory or file you want to analyze. /// Can also be a URL (http://), QCP query (qcp://), SSE stream, or MEM8 stream (mem8://) path: Option<String>, /// Specify input type explicitly (filesystem, qcp, sse, openapi, mem8) #[arg(long, value_name = "TYPE")] input: Option<String>, #[command(flatten)] scan_opts: ScanArgs, } #[derive(Parser, Debug)] struct ScanArgs { /// Choose your adventure! Selects the output format. /// From classic human-readable to AI-optimized hex, we've got options. #[arg(short, long, value_enum, default_value = "auto")] mode: OutputMode, /// Feeling like a detective? Find files/directories matching this regex pattern. /// Example: --find "README\\.md" #[arg(long)] find: Option<String>, /// Filter by file extension. Show only files of this type (e.g., "rs", "txt"). /// No leading dot needed, just the extension itself. #[arg(long = "type")] filter_type: Option<String>, /// Filter to show only files (f) or directories (d). #[arg(long = "entry-type", value_parser = ["f", "d"])] entry_type: Option<String>, /// Only show files larger than this size. /// Accepts human-readable sizes like "1M" (1 Megabyte), "500K" (500 Kilobytes), "100B" (100 Bytes). #[arg(long)] min_size: Option<String>, /// Only show files smaller than this size. /// Same format as --min-size. Let's find those tiny files! #[arg(long)] max_size: Option<String>, /// Time traveler? Show files newer than this date (YYYY-MM-DD format). #[arg(long)] newer_than: Option<String>, /// Or perhaps you prefer antiques? Show files older than this date (YYYY-MM-DD format). #[arg(long)] older_than: Option<String>, /// How deep should we dig? Limits the traversal depth. /// Default is 0 (auto) which lets each mode pick its ideal depth. /// Set explicitly to override: 1 for shallow, 10 for deep exploration. #[arg(short, long, default_value = "0")] depth: usize, /// Daredevil mode: Ignores `.gitignore` files. See everything, even what Git tries to hide! #[arg(long)] no_ignore: bool, /// Double daredevil: Ignores our built-in default ignore patterns too (like `node_modules`, `__pycache__`). /// Use with caution, or you might see more than you bargained for! #[arg(long)] no_default_ignore: bool, /// Show all files, including hidden ones (those starting with a `.`). /// The `-a` is for "all", naturally. #[arg(long, short = 'a')] all: bool, /// Want to see what's being ignored? This flag shows ignored directories in brackets `[dirname]`. /// Useful for debugging your ignore patterns or just satisfying curiosity. #[arg(long)] show_ignored: bool, /// SHOW ME EVERYTHING! The nuclear option that combines --all, --no-ignore, and --no-default-ignore. /// This reveals absolutely everything: hidden files, git directories, node_modules, the works! /// Warning: May produce overwhelming output in large codebases. #[arg(long)] everything: bool, /// Show filesystem type indicators in output (e.g., X=XFS, 4=ext4, B=Btrfs). /// Each file/directory gets a single character showing what filesystem it's on. /// Great for understanding storage layout and mount points! #[arg(long)] show_filesystems: bool, /// Not a fan of emojis? This flag disables them for a plain text experience. /// (But Trish loves the emojis, just saying!) 🌳✨ #[arg(long)] no_emoji: bool, /// Compress the output using zlib. Great for sending large tree structures over the wire /// or for AI models that appreciate smaller inputs. Output will be base64 encoded. #[arg(short = 'z', long)] compress: bool, /// MCP/API optimization mode. Automatically enables compression, disables colors/emoji, /// and optimizes output for machine consumption. Perfect for MCP servers, LLM APIs, and tools. /// Works with any output mode to make it API-friendly! #[arg(long)] mcp_optimize: bool, /// For JSON output, this makes it compact (one line) instead of pretty-printed. /// Saves space, but might make Trish's eyes water if she tries to read it directly. #[arg(long)] compact: bool, /// Controls how file paths are displayed in the output. #[arg(long = "path-mode", value_enum, default_value = "off")] path_mode: PathMode, /// When should we splash some color on the output? /// `auto` (default) uses colors if outputting to a terminal. #[arg(long, value_enum, default_value = "auto")] color: ColorMode, /// For AI mode, wraps the output in a JSON structure. /// Makes it easier for programmatic consumption by our AI overlords (just kidding... mostly). #[arg(long)] ai_json: bool, /// Stream output as files are scanned. This is a game-changer for very large directories! /// You'll see results trickling in, rather than waiting for the whole scan to finish. /// Note: Compression is disabled in stream mode for now. #[arg(long)] stream: bool, /// Start SSE server mode for real-time directory monitoring (experimental). /// This starts an HTTP server that streams directory changes as Server-Sent Events. /// Example: st --sse-server --sse-port 8420 /path/to/watch #[arg(long)] sse_server: bool, /// Port for SSE server mode (default: 8420) #[arg(long, default_value = "8420")] sse_port: u16, /// Search for a keyword within file contents. /// Best used with `--type` to limit search to specific file types (e.g., `--type rs --search "TODO"`). /// This is like having X-ray vision for your files! #[arg(long)] search: Option<String>, /// Group files by semantic similarity (inspired by Omni's wisdom!). /// Uses content-aware tokenization to identify conceptually related files. /// Perfect for understanding project structure at a higher level. /// Example groups: "tests", "documentation", "configuration", "source code" #[arg(long)] semantic: bool, /// Mermaid diagram style (only used with --mode mermaid). /// Options: flowchart (default), mindmap, gitgraph #[arg(long, value_enum, default_value = "flowchart")] mermaid_style: MermaidStyleArg, /// Index Rust code to SmartPastCode registry for universal code discovery. /// Specify the registry URL (e.g., http://localhost:8430). /// This will extract functions, modules, and impl blocks and submit them to the registry. /// Works best with --mode projects for project-level indexing. #[arg(long, value_name = "URL")] index_registry: Option<String>, /// Exclude mermaid diagrams from markdown report (only used with --mode markdown). #[arg(long)] no_markdown_mermaid: bool, /// Exclude tables from markdown report (only used with --mode markdown). #[arg(long)] no_markdown_tables: bool, /// Exclude pie charts from markdown report (only used with --mode markdown). #[arg(long)] no_markdown_pie_charts: bool, /// Focus analysis on specific file (for relations mode). /// Shows all relationships for a particular file. #[arg(long, value_name = "FILE")] focus: Option<PathBuf>, /// Filter relationships by type (for relations mode). /// Options: imports, calls, types, tests, coupled #[arg(long, value_name = "TYPE")] relations_filter: Option<String>, /// Sort results by: a-to-z, z-to-a, largest, smallest, newest, oldest, type /// Examples: --sort largest (biggest files first), --sort newest (recent files first) /// Use with --top to get "top 10 largest files" or "20 newest files" #[arg(long, value_enum)] sort: Option<SortField>, /// Show only the top N results (useful with --sort) /// Examples: --sort size --top 10 (10 largest files) /// --sort date --top 20 (20 most recent files) #[arg(long, value_name = "N")] top: Option<usize>, /// Include private functions in function documentation (for function-markdown mode) /// By default, only public functions are shown #[arg(long)] show_private: bool, /// View diffs stored in the .st folder (Smart Edit history) /// Shows all diffs for files modified by Smart Edit operations #[arg(long)] view_diffs: bool, /// Clean up old diffs in .st folder, keeping only last N per file /// Example: --cleanup-diffs 5 (keep last 5 diffs per file) #[arg(long, value_name = "N")] cleanup_diffs: Option<usize>, } /// Sort field options with intuitive names #[derive(Debug, Clone, Copy, ValueEnum)] enum SortField { /// Sort alphabetically A to Z #[value(name = "a-to-z")] AToZ, /// Sort alphabetically Z to A #[value(name = "z-to-a")] ZToA, /// Sort by size, largest files first #[value(name = "largest")] Largest, /// Sort by size, smallest files first #[value(name = "smallest")] Smallest, /// Sort by modification date, newest first #[value(name = "newest")] Newest, /// Sort by modification date, oldest first #[value(name = "oldest")] Oldest, /// Sort by file type/extension #[value(name = "type")] Type, /// Legacy aliases for backward compatibility #[value(name = "name", alias = "alpha")] Name, #[value(name = "size")] Size, #[value(name = "date", alias = "modified")] Date, } /// Enum for mermaid style argument #[derive(Debug, Clone, Copy, ValueEnum)] enum MermaidStyleArg { /// Traditional flowchart (default) Flowchart, /// Mind map style Mindmap, /// Git graph style Gitgraph, /// Treemap style (shows file sizes visually) Treemap, } /// Enum defining how color should be used in the output. /// Because life's too short for monochrome (unless you ask for it). #[derive(Debug, Clone, Copy, ValueEnum)] enum ColorMode { /// Always use colors, no matter what. Go vibrant! Always, /// Never use colors. For the minimalists. Never, /// Use colors if the output is a terminal (tty), otherwise disable. This is the default smart behavior. Auto, } /// Enum defining how paths should be displayed. /// Sometimes you want the full story, sometimes just the filename. #[derive(Debug, Clone, Copy, ValueEnum)] enum PathMode { /// Show only filenames (default). Clean and simple. Off, /// Show paths relative to the scan root. Good for context within the project. Relative, /// Show full absolute paths. Leaves no doubt where things are. Full, } /// Enum defining the available output modes. /// Each mode tailors the output for a specific purpose or audience. #[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] enum OutputMode { /// Auto mode - smart default selection based on context Auto, /// Classic tree format, human-readable with metadata and emojis (unless disabled). Our beloved default. Classic, /// Hexadecimal format with fixed-width fields. Excellent for AI parsing or detailed analysis. Hex, /// JSON output. Structured data for easy programmatic use. Json, /// Unix ls -Alh format. Familiar directory listing with human-readable sizes and permissions. Ls, /// AI-optimized format. A special blend of hex tree and statistics, designed for LLMs. Ai, /// Directory statistics only. Get a summary without the full tree. Stats, /// CSV (Comma Separated Values) format. Spreadsheet-friendly. Csv, /// TSV (Tab Separated Values) format. Another spreadsheet favorite. Tsv, /// Super compact digest format. A single line with a hash and minimal stats, perfect for quick AI pre-checks. Digest, /// Emotional tree - Files with FEELINGS! They get happy, sad, anxious, proud! Emotional, /// MEM|8 Quantum format. The ultimate compression with bitfield headers and tokenization. Quantum, /// Semantic grouping format. Groups files by conceptual similarity (inspired by Omni!). Semantic, /// Projects discovery mode. Find all your forgotten 3am coding projects with git info! Projects, /// Mermaid diagram format. Perfect for embedding in documentation! Mermaid, /// Markdown report format. Combines mermaid, tables, and charts for beautiful documentation! Markdown, /// Interactive summary mode (default for humans in terminal) Summary, /// AI-optimized summary mode (default for AI/piped output) SummaryAi, /// Context mode for AI conversations - provides git, memory, and structure context Context, /// Code relationship analysis Relations, /// Quantum compression with semantic understanding (Omni's nuclear option!) QuantumSemantic, /// Waste detection and optimization analysis (Marie Kondo mode!) Waste, /// Marqant - Quantum-compressed markdown format (.mq) Marqant, /// SSE - Server-Sent Events streaming format for real-time monitoring Sse, /// Function documentation in markdown format - living blueprints of your code! FunctionMarkdown, } /// Parses a date string (YYYY-MM-DD) into a `SystemTime` object. /// This is our time machine! It parses a date string (like "2025-12-25") /// into a `SystemTime` object that Rust can understand. /// Perfect for finding files from the past or... well, not the future. Yet. /// Get the ideal depth for each output mode /// /// When users don't specify depth (depth = 0), each mode gets its optimal default: /// - LS mode: 1 (like real ls, shows only immediate children) /// - Classic: 3 (good overview without overwhelming) /// - AI/Hex: 5 (more detail for analysis) /// - Stats: 10 (comprehensive statistics) /// - Others: 4 (balanced default) fn get_ideal_depth_for_mode(mode: &OutputMode) -> usize { match mode { OutputMode::Auto => 3, // Should never reach here, but default to classic depth OutputMode::Ls => 1, // Mimics 'ls' behavior OutputMode::Classic => 3, // Balanced tree view OutputMode::Ai | OutputMode::Hex => 5, // More detail for analysis OutputMode::Stats => 10, // Comprehensive stats OutputMode::Digest => 10, // Full scan for accurate digest OutputMode::Emotional => 5, // Enough to see personality development OutputMode::Quantum | OutputMode::QuantumSemantic => 5, // Good compression balance OutputMode::Summary | OutputMode::SummaryAi | OutputMode::Context => 4, // Reasonable summary depth OutputMode::Waste => 10, // Deep scan to find all duplicates OutputMode::Relations => 10, // Deep scan for relationships OutputMode::Projects => 5, // Good depth for finding all projects _ => 4, // Default for other modes } } fn parse_date(date_str: &str) -> Result<SystemTime> { // Attempt to parse the date string. let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?; // Assume midnight (00:00:00) for the given date. let datetime = date.and_hms_opt(0, 0, 0).unwrap(); // This unwrap is safe as 00:00:00 is always valid. // Convert to SystemTime. Ok(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(datetime.and_utc().timestamp() as u64)) } /// And now, the moment you've all been waiting for: the `main` function! /// This is the heart of the st concert. It's where we parse the arguments, /// configure the scanner, pick the right formatter for the job, and let it rip. /// It returns a `Result` because sometimes, even rockstars hit a wrong note. #[tokio::main] async fn main() -> Result<()> { // Parse the command-line arguments provided by the user. let cli = Cli::parse(); // Handle tips flag if provided if let Some(state) = &cli.tips { let enable = state == "on"; st::tips::handle_tips_flag(enable)?; return Ok(()); } // Handle spicy TUI mode if cli.spicy { // Check if TUI is enabled via feature flags let flags = feature_flags::features(); if !flags.enable_tui { eprintln!("Error: Terminal UI is disabled by configuration or compliance mode."); eprintln!("Contact your administrator to enable this feature."); return Ok(()); } let path = std::env::current_dir()?; return st::spicy_tui_enhanced::run_enhanced_spicy_tui(path).await; } // Initialize logging if requested if let Some(log_path) = &cli.log { // Check if activity logging is enabled via feature flags let flags = feature_flags::features(); if !flags.enable_activity_logging { eprintln!("Warning: Activity logging is disabled by configuration or compliance mode."); eprintln!("Continuing without logging."); } else { // log_path is Option<Option<String>> - Some(None) means --log without path let path = log_path.clone(); st::activity_logger::ActivityLogger::init(path)?; // Log will be written throughout execution } } // Handle exclusive action flags first. if cli.cheet { let markdown = std::fs::read_to_string("docs/st-cheetsheet.md")?; let skin = termimad::MadSkin::default(); skin.print_text(&markdown); return Ok(()); } if let Some(shell) = cli.completions { let mut cmd = Cli::command(); let bin_name = cmd.get_name().to_string(); generate(shell, &mut cmd, bin_name, &mut io::stdout()); return Ok(()); } if cli.man { let cmd = Cli::command(); let man = clap_mangen::Man::new(cmd); man.render(&mut io::stdout())?; return Ok(()); } if cli.mcp { // Check if MCP server is enabled via feature flags let flags = feature_flags::features(); if !flags.enable_mcp_server { eprintln!("Error: MCP server is disabled by configuration or compliance mode."); eprintln!("Contact your administrator to enable this feature."); return Ok(()); } return run_mcp_server().await; } if cli.mcp_tools { print_mcp_tools(); return Ok(()); } if cli.mcp_config { print_mcp_config(); return Ok(()); } // Handle hooks configuration if let Some(action) = &cli.hooks_config { // Check if hooks are enabled via feature flags let flags = feature_flags::features(); if !flags.enable_hooks { eprintln!("Error: Hooks are disabled by configuration or compliance mode."); eprintln!("Contact your administrator to enable this feature."); return Ok(()); } return handle_hooks_config(action).await; } if cli.hooks_install { // Check if hooks are enabled via feature flags let flags = feature_flags::features(); if !flags.enable_hooks { eprintln!("Error: Hooks are disabled by configuration or compliance mode."); eprintln!("Contact your administrator to enable this feature."); return Ok(()); } return install_hooks_to_claude().await; } // Handle diff storage operations if cli.scan_opts.view_diffs { return handle_view_diffs().await; } if let Some(keep_count) = cli.scan_opts.cleanup_diffs { return handle_cleanup_diffs(keep_count).await; } if cli.terminal { // Check if terminal is enabled via feature flags let flags = feature_flags::features(); if !flags.enable_tui { eprintln!("Error: Terminal interface is disabled by configuration or compliance mode."); eprintln!("Contact your administrator to enable this feature."); return Ok(()); } return run_terminal().await; } if cli.dashboard { // Require a local display for the egui dashboard. let has_display = std::env::var_os("DISPLAY").is_some() || std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("WAYLAND_SOCKET").is_some(); if !has_display { eprintln!( "⚠️ The graphical dashboard needs a local display and isn't available in this remote session yet." ); eprintln!("💡 Tip: run st locally or wait for the upcoming browser dashboard mode."); return Ok(()); } // Launch the egui dashboard! return run_dashboard().await; } if cli.version { return show_version_with_updates().await; } if let Some(names) = cli.rename_project { if names.len() != 2 { eprintln!("Error: rename-project requires exactly two arguments: OLD_NAME NEW_NAME"); return Ok(()); } let options = RenameOptions::default(); return rename_project(&names[0], &names[1], options).await; } // Handle Claude integration setup (smart init or update) if cli.setup_claude { let project_path = std::env::current_dir()?; let initializer = ClaudeInit::new(project_path)?; return initializer.setup(); } // Handle Claude consciousness commands if cli.claude_save { return handle_claude_save().await; } if cli.claude_restore { return handle_claude_restore().await; } if cli.claude_context { return handle_claude_context().await; } // Handle consciousness maintenance commands if cli.update_consciousness { let path = cli.path.unwrap_or_else(|| ".".to_string()); return handle_update_consciousness(&path).await; } if cli.security_scan { let path = cli.path.unwrap_or_else(|| ".".to_string()); return handle_security_scan(&path).await; } if cli.token_stats { let path = cli.path.unwrap_or_else(|| ".".to_string()); return handle_token_stats(&path).await; } if cli.get_frequency { let path = cli.path.unwrap_or_else(|| ".".to_string()); return handle_get_frequency(&path).await; } if cli.claude_dump { return handle_claude_dump().await; } if cli.claude_kickstart { return handle_claude_kickstart().await; } if cli.claude_user_prompt_submit { return st::claude_hook::handle_user_prompt_submit().await; } // Handle memory operations if let Some(args) = cli.memory_anchor { if args.len() == 3 { return handle_memory_anchor(&args[0], &args[1], &args[2]).await; } } if let Some(keywords) = cli.memory_find { return handle_memory_find(&keywords).await; } if cli.memory_stats { return handle_memory_stats().await; } // If no action flag was given, proceed with the scan. let args = cli.scan_opts; let input_str = cli.path.unwrap_or_else(|| ".".to_string()); // --- Environment Variable Overrides --- // Check for ST_DEFAULT_MODE environment variable to override the default output mode. // This allows users to set a persistent preference. let default_mode_env = std::env::var("ST_DEFAULT_MODE") .ok() // Convert Result to Option .and_then(|m| match m.to_lowercase().as_str() { // Match on the lowercase string value "classic" => Some(OutputMode::Classic), "hex" => Some(OutputMode::Hex), "json" => Some(OutputMode::Json), "ls" => Some(OutputMode::Ls), "ai" => Some(OutputMode::Ai), "stats" => Some(OutputMode::Stats), "csv" => Some(OutputMode::Csv), "tsv" => Some(OutputMode::Tsv), "digest" => Some(OutputMode::Digest), "quantum" => Some(OutputMode::Quantum), "semantic" => Some(OutputMode::Semantic), "mermaid" => Some(OutputMode::Mermaid), "markdown" => Some(OutputMode::Markdown), "relations" => Some(OutputMode::Relations), "summary" => Some(OutputMode::Summary), "summary-ai" => Some(OutputMode::SummaryAi), "context" => Some(OutputMode::Context), "quantum-semantic" => Some(OutputMode::QuantumSemantic), "waste" => Some(OutputMode::Waste), "marqant" => Some(OutputMode::Marqant), "sse" => Some(OutputMode::Sse), "function-markdown" => Some(OutputMode::FunctionMarkdown), _ => None, // Unknown mode string, ignore. }); // Determine the final mode and compression settings. // The AI_TOOLS environment variable takes highest precedence. // Then, --semantic flag overrides the mode. // Then, the command-line --mode flag. // Then, ST_DEFAULT_MODE environment variable. // Finally, the default mode from clap. let is_ai_caller = std::env::var("AI_TOOLS").is_ok_and(|v| v == "1" || v.to_lowercase() == "true"); // MCP optimization: enables compression and AI-friendly settings let mcp_mode = args.mcp_optimize || is_ai_caller; let (mut mode, compress) = if mcp_mode { // If MCP optimization is requested or AI_TOOLS is set, use API-optimized settings match args.mode { OutputMode::Auto => (OutputMode::Ai, true), // Default to AI mode for MCP OutputMode::Summary => (OutputMode::SummaryAi, true), // Auto-switch to AI version other => (other, true), // Keep other modes but enable compression } } else if args.semantic { // If --semantic flag is set, use semantic mode (Omni's wisdom!) (OutputMode::Semantic, args.compress) } else if args.mode != OutputMode::Auto { // User explicitly specified a mode via command line - this takes precedence! (args.mode, args.compress) } else if let Some(env_mode) = default_mode_env { // If ST_DEFAULT_MODE is set and user didn't specify mode, use it (env_mode, args.compress) } else { // No explicit mode specified anywhere, use classic as default (OutputMode::Classic, args.compress) }; // Auto-switch to ls mode when using --top with classic mode if args.top.is_some() && matches!(mode, OutputMode::Classic) { // Check if user explicitly provided --mode flag by looking at CLI args let user_specified_mode = std::env::args().any(|arg| arg == "--mode" || arg == "-m"); if !user_specified_mode && default_mode_env.is_none() { // Auto-switch only if user didn't explicitly choose a mode eprintln!( "💡 Auto-switching to ls mode for --top results (use --mode classic to override)" ); mode = OutputMode::Ls; } else { // User explicitly chose classic mode or set env var, just show the note eprintln!("💡 Note: --top doesn't limit results in classic tree mode."); eprintln!(" Tree mode needs all entries to build the structure."); eprintln!(" Use --mode ls for limited results: st --mode ls --sort largest --top 10"); } } // --- Color Configuration --- // Determine if colors should be used in the output. // MCP mode disables colors for clean API output. // Command-line --color flag takes precedence. // Then, ST_COLOR or NO_COLOR environment variables. // Finally, auto-detect based on whether stdout is a TTY. let use_color = if mcp_mode { false // MCP mode always disables colors } else { match args.color { ColorMode::Always => true, ColorMode::Never => false, ColorMode::Auto => { // Check environment variables first for explicit overrides. if std::env::var("ST_COLOR").as_deref() == Ok("always") { true } else if std::env::var("NO_COLOR").is_ok() || std::env::var("ST_COLOR").as_deref() == Ok("never") { false } else { // If no env var override, check if stdout is a terminal. std::io::stdout().is_terminal() } } } }; // If colors are disabled, globally turn them off for the `colored` crate. if !use_color { colored::control::set_override(false); } // MCP mode also disables emoji for clean output let no_emoji = args.no_emoji || mcp_mode; // --- Scanner Configuration --- // Build the configuration for the directory scanner. // Handle the --everything flag which overrides other visibility settings let (no_ignore_final, no_default_ignore_final, all_final) = if args.everything { (true, true, true) // Override all ignore/hide settings } else { // Check if the root path itself is a commonly ignored directory // If so, automatically disable default ignores AND show hidden files let path = PathBuf::from(&input_str); let root_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); // List of directory names that should disable default ignores when explicitly targeted let auto_disable_ignores = matches!( root_name, ".git" | ".svn" | ".hg" | "node_modules" | "target" | "build" | "dist" | "__pycache__" | ".cache" | ".pytest_cache" | ".mypy_cache" ); // For hidden directories like .git, also enable showing hidden files let auto_show_hidden = auto_disable_ignores && root_name.starts_with('.'); let no_default_ignore = args.no_default_ignore || auto_disable_ignores; let all = args.all || auto_show_hidden; (args.no_ignore, no_default_ignore, all) }; // For AI mode, we automatically enable `show_ignored` to provide maximum context to the AI, // unless the user explicitly set `show_ignored` (which `args.show_ignored` would capture). let show_ignored_final = args.show_ignored || matches!(mode, OutputMode::Ai | OutputMode::Digest) || args.everything; // Smart defaults: Each mode has an ideal depth when user doesn't specify (depth = 0) let effective_depth = if args.depth == 0 { get_ideal_depth_for_mode(&mode) } else { args.depth // User explicitly set depth, respect their choice }; let scanner_config = ScannerConfig { max_depth: effective_depth, follow_symlinks: false, // Symlink following is generally off for safety and simplicity. respect_gitignore: !no_ignore_final, show_hidden: all_final, show_ignored: show_ignored_final, // Attempt to compile the find pattern string into a Regex. find_pattern: args.find.as_ref().map(|p| Regex::new(p)).transpose()?, file_type_filter: args.filter_type.clone(), entry_type_filter: args.entry_type.clone(), // Parse human-readable size strings (e.g., "1M") into u64 bytes. min_size: args.min_size.as_ref().map(|s| parse_size(s)).transpose()?, max_size: args.max_size.as_ref().map(|s| parse_size(s)).transpose()?, // Parse date strings (YYYY-MM-DD) into SystemTime. newer_than: args .newer_than .as_ref() .map(|d| parse_date(d)) .transpose()?, older_than: args .older_than .as_ref() .map(|d| parse_date(d)) .transpose()?, use_default_ignores: !no_default_ignore_final, search_keyword: args.search.clone(), show_filesystems: args.show_filesystems, sort_field: args.sort.map(|f| match f { SortField::AToZ | SortField::Name => "a-to-z".to_string(), SortField::ZToA => "z-to-a".to_string(), SortField::Largest | SortField::Size => "largest".to_string(), SortField::Smallest => "smallest".to_string(), SortField::Newest | SortField::Date => "newest".to_string(), SortField::Oldest => "oldest".to_string(), SortField::Type => "type".to_string(), }), top_n: args.top, include_line_content: false, }; // 🌊 Universal Input Processing // Detect input type and determine root path let (is_traditional_fs, scan_path) = if cli.input.is_some() || !input_str.starts_with(".") && !PathBuf::from(&input_str).exists() { (false, PathBuf::from(&input_str)) } else { (true, PathBuf::from(&input_str)) }; // Convert the command-line PathMode enum to the formatter's PathDisplayMode enum. let path_display_mode = match args.path_mode { PathMode::Off => PathDisplayMode::Off, PathMode::Relative => PathDisplayMode::Relative, PathMode::Full => PathDisplayMode::Full, }; // --- Output Generation --- // Decide whether to use streaming mode or normal (scan-all-then-format) mode. // Streaming is enabled by --stream flag AND if compression is NOT requested (as zlib needs the whole buffer). // Note: Streaming is only supported for traditional filesystem scanning if args.stream && !compress && is_traditional_fs { // Streaming mode is only supported for certain formatters that implement StreamingFormatter. match mode { OutputMode::Hex | OutputMode::Ai | OutputMode::Quantum => { // For streaming, we use threads: one for scanning, one for formatting/printing. // A channel is used for communication between them. use std::sync::mpsc; // Multi-producer, single-consumer channel. use std::thread; let (tx, rx) = mpsc::channel(); // Create the communication channel. // Recreate scanner for streaming (since we consumed it above) let path = PathBuf::from(&input_str); let scanner = Scanner::new(&path, scanner_config)?; let scanner_root = scanner.root().to_path_buf(); // Get the canonicalized root before moving scanner // Spawn the scanner thread. It will send FileNode objects through the channel. let scanner_thread = thread::spawn(move || { // The scanner's scan_stream method takes the sender part of the channel. scanner.scan_stream(tx) }); // Create the appropriate streaming formatter based on the selected mode. let streaming_formatter: Box<dyn StreamingFormatter> = match mode { OutputMode::Hex => Box::new(HexFormatter::new( use_color, no_emoji, show_ignored_final, path_display_mode, args.show_filesystems, )), OutputMode::Ai => Box::new(AiFormatter::new(no_emoji, path_display_mode)), OutputMode::Quantum => Box::new(QuantumFormatter::new()), _ => unreachable!(), // Should not happen due to the outer match. }; // Show a helpful tip at the top (occasionally) st::tips::maybe_show_tip().ok(); // Get a lock on stdout for writing. let stdout = io::stdout(); let mut handle = stdout.lock(); // Initialize the stream with the formatter (e.g., print headers). streaming_formatter.start_stream(&mut handle, &scanner_root)?; // Receive and format nodes as they arrive from the scanner thread. while let Ok(node) = rx.recv() { // Loop until the channel is closed. streaming_formatter.format_node(&mut handle, &node, &scanner_root)?; } // Wait for the scanner thread to finish and get the final statistics. // The `??` propagates errors from thread panic and from the scan_stream result. let stats = scanner_thread .join() .map_err(|_| anyhow::anyhow!("Scanner thread panicked"))??; // Finalize the stream with the formatter (e.g., print footers or summary stats). streaming_formatter.end_stream(&mut handle, &stats, &scanner_root)?; } _ => { // If streaming is requested for an unsupported mode, print an error and exit. // Trish says clear error messages are important! eprintln!("Streaming mode is currently only supported for 'hex' and 'ai' output modes when not using compression."); std::process::exit(1); // Exit with a non-zero status code to indicate an error. } } } else { // Normal (non-streaming) mode or when compression is enabled. // Scan the entire directory structure first. // Get nodes and stats based on input type let (nodes, stats, root_path) = if is_traditional_fs { let scanner = Scanner::new(&scan_path, scanner_config)?; let root = scanner.root().to_path_buf(); let (n, s) = scanner.scan()?; (n, s, root) } else { // Use universal input processor for non-filesystem inputs eprintln!("🌊 Detecting input type..."); let input_processor = InputProcessor::new(); let input_source = InputProcessor::detect_input_type(&input_str); let context_root = input_processor.process(input_source).await?; // Convert context nodes to file nodes let file_nodes = st::inputs::context_to_file_nodes(context_root); // Create synthetic stats let stats = st::TreeStats { total_files: file_nodes.iter().filter(|n| !n.is_dir).count() as u64, total_dirs: file_nodes.iter().filter(|n| n.is_dir).count() as u64, total_size: file_nodes.iter().map(|n| n.size).sum(), file_types: std::collections::HashMap::new(), largest_files: vec![], newest_files: vec![], oldest_files: vec![], }; (file_nodes, stats, scan_path.clone()) }; // Create the appropriate formatter based on the selected mode. let formatter: Box<dyn Formatter> = match mode { OutputMode::Auto => { // Auto mode should have been resolved to a specific mode by now unreachable!("Auto mode should have been resolved before formatter selection") } OutputMode::Classic => { // Special handling for classic mode with --find: default to relative paths // if user hasn't explicitly set a path_mode other than 'off'. // This makes --find output more useful by showing where found items are. let classic_path_mode = if args.find.is_some() && matches!(args.path_mode, PathMode::Off) { PathDisplayMode::Relative } else { path_display_mode // Use the user-specified or default path_mode. }; let formatter = ClassicFormatter::new(no_emoji, use_color, classic_path_mode); let sort_field = args.sort.map(|f| match f { SortField::AToZ | SortField::Name => "a-to-z".to_string(), SortField::ZToA => "z-to-a".to_string(), SortField::Largest | SortField::Size => "largest".to_string(), SortField::Smallest => "smallest".to_string(), SortField::Newest | SortField::Date => "newest".to_string(), SortField::Oldest => "oldest".to_string(), SortField::Type => "type".to_string(), }); Box::new(formatter.with_sort(sort_field)) } OutputMode::Hex => Box::new(HexFormatter::new( use_color, no_emoji, show_ignored_final, path_display_mode, args.show_filesystems, )), OutputMode::Json => Box::new(JsonFormatter::new(args.compact)), OutputMode::Ls => Box::new(LsFormatter::new(!no_emoji, use_color)), OutputMode::Ai => { // AI mode can optionally be wrapped in JSON. if args.ai_json { Box::new(AiJsonFormatter::new(no_emoji, path_display_mode)) } else { Box::new(AiFormatter::new(no_emoji, path_display_mode)) } } OutputMode::Stats => Box::new(StatsFormatter::new()), OutputMode::Csv => Box::new(CsvFormatter::new()), OutputMode::Tsv => Box::new(TsvFormatter::new()), OutputMode::Digest => Box::new(DigestFormatter::new()), OutputMode::Emotional => { use st::formatters::emotional_new::EmotionalFormatter; Box::new(EmotionalFormatter::new(use_color)) } OutputMode::Quantum => Box::new(QuantumFormatter::new()), OutputMode::Semantic => Box::new(SemanticFormatter::new(path_display_mode, no_emoji)), OutputMode::Projects => Box::new(ProjectsFormatter::new()), OutputMode::Mermaid => { // Convert CLI arg enum to formatter enum let style = match args.mermaid_style { MermaidStyleArg::Flowchart => MermaidStyle::Flowchart, MermaidStyleArg::Mindmap => MermaidStyle::Mindmap, MermaidStyleArg::Gitgraph => MermaidStyle::GitGraph, MermaidStyleArg::Treemap => MermaidStyle::Treemap, }; Box::new(MermaidFormatter::new(style, no_emoji, path_display_mode)) } OutputMode::Markdown => { // Create a comprehensive markdown report with all visualizations! Box::new(MarkdownFormatter::new( path_display_mode, no_emoji, !args.no_markdown_mermaid, // Include mermaid unless disabled !args.no_markdown_tables, // Include tables unless disabled !args.no_markdown_pie_charts, // Include pie charts unless disabled )) } OutputMode::Marqant => Box::new(MarqantFormatter::new(path_display_mode, no_emoji)), OutputMode::Sse => { // SSE streaming format for real-time monitoring Box::new(SseFormatter::new()) } OutputMode::Relations => { // Code relationship analysis - "Semantic X-ray vision!" - Omni use st::formatters::relations_formatter::RelationsFormatter; Box::new(RelationsFormatter::new( args.relations_filter.clone(), args.focus.clone(), )) } OutputMode::Summary => { // Interactive summary for humans - "Smart defaults!" - Omni use st::formatters::summary::SummaryFormatter; Box::new(SummaryFormatter::new(use_color)) } OutputMode::SummaryAi => { // Compressed summary for AI consumption use st::formatters::summary_ai::SummaryAiFormatter; Box::new(SummaryAiFormatter::new(compress)) } OutputMode::Context => { // Context mode for AI conversations use st::formatters::context::ContextFormatter; Box::new(ContextFormatter::new()) } OutputMode::QuantumSemantic => { // Semantic-aware quantum compression - "The nuclear option!" - Omni use st::formatters::quantum_semantic::QuantumSemanticFormatter; Box::new(QuantumSemanticFormatter::new()) } OutputMode::Waste => { // Waste detection and optimization analysis - "Marie Kondo mode!" - Hue & Aye Box::new(WasteFormatter::new()) } OutputMode::FunctionMarkdown => { // Function documentation in markdown - "Living blueprints of your code!" - Trisha use st::formatters::function_markdown::FunctionMarkdownFormatter; Box::new(FunctionMarkdownFormatter::new( args.show_private, true, // show_complexity true, // show_call_graph )) } }; // Handle registry indexing if requested if let Some(registry_url) = &args.index_registry { use st::registry::RegistryIndexer; eprintln!( "🚀 Indexing project to SmartPastCode registry: {}", registry_url ); let indexer = RegistryIndexer::new(registry_url).context("Failed to create registry indexer")?; match indexer.index_project(&root_path) { Ok(stats) => { stats.print_summary(); } Err(e) => { eprintln!("❌ Registry indexing failed: {}", e); eprintln!(" Continuing with normal output..."); } } } // Show a helpful tip at the top (occasionally) for non-streaming modes // Only show if not in streaming mode (already shown above for streaming) if matches!( mode, OutputMode::Ls | OutputMode::Classic | OutputMode::Ai | OutputMode::Json | OutputMode::Csv | OutputMode::Tsv | OutputMode::Markdown | OutputMode::Mermaid | OutputMode::Stats | OutputMode::Hex | OutputMode::Waste | OutputMode::Digest ) { st::tips::maybe_show_tip().ok(); } // Format the collected nodes and stats into a byte vector. let mut output_buffer = Vec::new(); formatter.format(&mut output_buffer, &nodes, &stats, &root_path)?; // Handle compression if requested. if compress { let compressed_data = compress_output(&output_buffer)?; // Print the compressed data as a base64 encoded string, prefixed for identification. // Using hex encoding for binary data to make it printable. println!("COMPRESSED_V1:{}", hex::encode(&compressed_data)); } else { // If not compressing, write the formatted output directly to stdout. io::stdout().write_all(&output_buffer)?; // Show helpful tips for human users (not when piped, redirected, or in AI modes) if IsTerminal::is_terminal(&io::stdout()) && !compress && !matches!( mode, OutputMode::Ai | OutputMode::Hex | OutputMode::Digest | OutputMode::Quantum | OutputMode::QuantumSemantic ) { show_helpful_tips(&mode, effective_depth, &args)?; } } } // If we've reached here, everything went well! Ok(()) } /// Show helpful tips to human users to improve their smart-tree experience /// /// This provides contextual suggestions based on current usage patterns, /// helping users discover powerful features and optimize their workflow. /// Trish loves these little nuggets of wisdom! 💡 fn show_helpful_tips(mode: &OutputMode, depth: usize, args: &ScanArgs) -> Result<()> { use rand::seq::SliceRandom; let mut tips = Vec::new(); // Mode-specific tips match mode { OutputMode::Auto => { // Auto mode should never reach here, but just in case tips.push("🤖 Auto mode intelligently selects the best format for your use case!"); } OutputMode::Classic => { if depth > 5 { tips.push("💡 Deep trees can be overwhelming. Try reducing depth with -d 3 or use --mode ls for a clean listing!"); } if depth == 0 || depth == 3 { tips.push("🌳 Classic mode auto-selects depth 3 when depth is 0 (auto). Use -d to override!"); } tips.push("🚀 Pro tip: Set ST_DEFAULT_MODE=ls for instant directory listings!"); } OutputMode::Ls => { tips.push("✨ You're using LS mode! This mimics 'ls -Alh' but with smart-tree magic."); if depth == 1 { tips.push( "🎯 Perfect! Depth 1 is ideal for LS mode - just like the real ls command.", ); } tips.push("💾 Save time: export ST_DEFAULT_MODE=ls to make this your default!"); } OutputMode::Waste => { tips.push("🧹 Great choice! Waste mode helps you Marie Kondo your projects."); tips.push("💰 Run the suggested cleanup commands to reclaim disk space!"); } OutputMode::Ai => { tips.push("🤖 AI mode provides LLM-optimized output for perfect code analysis."); tips.push("⚡ Combine with --compress for ultra-efficient AI input!"); } _ => { // General tips for other modes tips.push("🗂️ Try --mode ls -d 1 for a clean directory view like 'ls -Alh'!"); tips.push("🧹 Use --mode waste to find duplicate files and reclaim disk space!"); } } // Depth-specific tips if depth > 5 { tips.push("🌳 Deep exploration detected! Consider -d 2 or -d 3 for better readability."); } // Feature discovery tips if !args.all { tips.push("👀 Add -a to see hidden files and directories (like .gitignore)."); } if args.filter_type.is_none() && args.entry_type.is_none() { tips.push( "🔍 Filter by type: --entry-type f (files only) or --entry-type d (directories only).", ); } // Random general tips let general_tips = [ "📚 Check out --mode markdown for beautiful project documentation!", "🔥 Use --mode quantum for ultra-compressed output perfect for AI analysis!", "📊 Try --mode stats for quick project metrics and insights!", "🎨 Smart-tree respects your terminal colors and emoji preferences!", "⚡ Set ST_DEFAULT_MODE environment variable to save your preferred mode!", "🔧 Use --find 'pattern' to search for specific files across your tree!", ]; // Add 1-2 random general tips let mut rng = rand::thread_rng(); let selected_general: Vec<_> = general_tips.choose_multiple(&mut rng, 2).collect(); for tip in selected_general { tips.push(tip); } // Show a maximum of 3 tips to avoid overwhelming the user let selected_tips: Vec<_> = tips.choose_multiple(&mut rng, 3.min(tips.len())).collect(); if !selected_tips.is_empty() { eprintln!(); // Add some space eprintln!("\x1b[2m───────────────────────────────────────────────────────\x1b[0m"); for tip in selected_tips { eprintln!("\x1b[2m{tip}\x1b[0m"); } eprintln!("\x1b[2m───────────────────────────────────────────────────────\x1b[0m"); } Ok(()) } /// Ever tried to send a whole drum kit through a tiny tube? That's what this /// function does. It takes our beautiful output, squishes it down with Zlib /// compression, and makes it super easy to send over the network or to an AI /// that appreciates brevity. fn compress_output(data: &[u8]) -> Result<Vec<u8>> { // Create a Zlib encoder with default compression level. let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); // Write all input data to the encoder. encoder.write_all(data)?; // Finalize the compression and return the resulting byte vector. Ok(encoder.finish()?) } // --- MCP Helper Functions (only compiled if "mcp" feature is enabled) --- /// Prints the JSON configuration snippet for adding `st` as an MCP server /// to Claude Desktop. This helps users easily integrate `st`. fn print_mcp_config() { // Try to get the current executable's path. Fallback to "st" if it fails. let exe_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("st")); // Graceful fallback. // Print the JSON structure, making it easy for users to copy-paste. // Using println! for each line for clarity. println!("Add this to your Claude Desktop configuration (claude_desktop_config.json):"); println!(); // Blank line for spacing. println!("{{"); // Start of JSON object. println!(" \"mcpServers\": {{"); println!(" \"smart-tree\": {{"); // Server name: "smart-tree". println!(" \"command\": \"{}\",", exe_path.display()); // Path to the st executable. println!(" \"args\": [\"--mcp\"],"); // Arguments to run st in MCP server mode. println!(" \"env\": {{}}"); // Optional environment variables for the server process. println!(" }}"); println!(" }}"); println!("}}"); // End of JSON object. println!(); // Provide common locations for the Claude Desktop config file. println!("Default locations for claude_desktop_config.json:"); println!(" macOS: ~/Library/Application Support/Claude/claude_desktop_config.json"); println!(" Windows: %APPDATA%\\Claude\\claude_desktop_config.json"); println!(" Linux: ~/.config/Claude/claude_desktop_config.json"); } /// Prints a list of available MCP tools that `st` provides. /// This helps users (or AI) understand what actions can be performed via MCP. fn print_mcp_tools() { println!("🌳 Smart Tree MCP Server - Available Tools (20+) 🌳"); println!(); println!("📚 Full documentation: Run 'st --mcp' to start the MCP server"); println!("💡 Pro tip: Use these tools with Claude Desktop for AI-powered file analysis!"); println!(); println!("CORE TOOLS:"); println!(" • server_info - Get server capabilities and current time"); println!(" • analyze_directory - Main workhorse with multiple output formats"); println!(" • quick_tree - Lightning-fast 3-level overview (10x compression)"); println!(" • project_overview - Comprehensive project analysis"); println!(); println!("FILE DISCOVERY:"); println!(" • find_files - Search with regex, size, date filters"); println!(" • find_code_files - Find source code by language"); println!(" • find_config_files - Locate all configuration files"); println!(" • find_documentation - Find README, docs, licenses"); println!(" • find_tests - Locate test files across languages"); println!(" • find_build_files - Find Makefile, Cargo.toml, etc."); println!(); println!("CONTENT SEARCH:"); println!(" • search_in_files - Powerful content search (like grep)"); println!(" • find_large_files - Identify space consumers"); println!(" • find_recent_changes - Files modified in last N days"); println!(" • find_in_timespan - Files modified in date range"); println!(); println!("ANALYSIS:"); println!(" • get_statistics - Comprehensive directory stats"); println!(" • get_digest - SHA256 hash for change detection"); println!(" • directory_size_breakdown - Size by subdirectory"); println!(" • find_empty_directories - Cleanup opportunities"); println!(" • find_duplicates - Detect potential duplicate files"); println!(" • semantic_analysis - Group files by purpose"); println!(); println!("ADVANCED:"); println!(" • compare_directories - Find differences between dirs"); println!(" • get_git_status - Git-aware directory structure"); println!(" • analyze_workspace - Multi-project workspace analysis"); println!(); println!("SMART EDIT (90% token reduction!):"); println!(" • smart_edit - Apply multiple AST-based edits efficiently"); println!(" • get_function_tree - Analyze code structure"); println!(" • insert_function - Add functions with minimal tokens"); println!(" • remove_function - Remove functions with dependency awareness"); println!(" • track_file_operation - Track AI file manipulations (.st folder)"); println!(" • get_file_history - View operation history for files"); println!(); println!("FEEDBACK:"); println!(" • submit_feedback - Help improve Smart Tree"); println!(" • request_tool - Request new MCP tools"); println!(" • check_for_updates - Check for newer versions"); println!(); println!("Run 'st --mcp' to start the server and see full parameter details!"); } /// Show version information with optional update checking /// This combines the traditional --version output with smart update detection /// Elvis would love this modern approach! 🕺 async fn show_version_with_updates() -> Result<()> { let current_version = env!("CARGO_PKG_VERSION"); // Always show current version info first println!( "🌟 Smart Tree v{} - The Gradient Enhancement Release! 🌈", current_version ); println!("🔧 Target: {}", std::env::consts::ARCH); println!("📦 Repository: {}", env!("CARGO_PKG_REPOSITORY")); println!("🎯 Authors: {}", env!("CARGO_PKG_AUTHORS")); println!("📝 Description: {}", env!("CARGO_PKG_DESCRIPTION")); // Check for updates (but don't fail if update service is unavailable) match check_for_updates_cli().await { Ok(update_info) => { if update_info.is_empty() { println!("✅ You're running the latest version! 🎉"); } else { println!(); println!("{}", update_info); } } Err(e) => { // Don't fail the whole command if update check fails eprintln!("⚠️ Update check unavailable: {}", e); println!("💡 Check https://github.com/8b-is/smart-tree for the latest releases"); } } println!(); println!("🚀 Ready to make your directories beautiful! Try: st --help"); println!("🎭 Trish from Accounting loves the colorful tree views! 🎨"); Ok(()) } /// Handle viewing diffs from the .st folder async fn handle_view_diffs() -> Result<()> { use st::smart_edit_diff::DiffStorage; let project_root = std::env::current_dir()?; let storage = DiffStorage::new(&project_root)?; // List all diffs let diffs = storage.list_all_diffs()?; if diffs.is_empty() { println!("📁 No diffs found in .st folder"); println!("💡 Smart Edit operations automatically store diffs when files are modified"); return Ok(()); } println!("📜 Smart Edit Diff History"); println!("{}", "=".repeat(60)); // Group diffs by file let mut by_file: std::collections::HashMap<String, Vec<(String, u64)>> = std::collections::HashMap::new(); for (file_path, timestamp) in diffs { by_file .entry(file_path.clone()) .or_default() .push((file_path, timestamp)); } for (file, mut entries) in by_file { // Sort by timestamp (newest first) entries.sort_by(|a, b| b.1.cmp(&a.1)); println!("\n📄 {}", file); for (_, timestamp) in entries.iter().take(5) { let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(*timestamp as i64, 0) .unwrap_or_default(); println!(" • {} ({})", dt.format("%Y-%m-%d %H:%M:%S UTC"), timestamp); } if entries.len() > 5 { println!(" ... and {} more", entries.len() - 5); } } println!("\n💡 Use 'st --cleanup-diffs N' to keep only the last N diffs per file"); Ok(()) } /// Handle cleaning up old diffs async fn handle_cleanup_diffs(keep_count: usize) -> Result<()> { use st::smart_edit_diff::DiffStorage; let project_root = std::env::current_dir()?; let storage = DiffStorage::new(&project_root)?; println!( "🧹 Cleaning up old diffs, keeping last {} per file...", keep_count ); let removed = storage.cleanup_old_diffs(keep_count)?; if removed == 0 { println!("✨ No diffs needed cleanup"); } else { println!("✅ Removed {} old diff files", removed); } Ok(()) } /// Check for updates from our feedback API (CLI version) /// Returns update message if available, empty string if up-to-date async fn check_for_updates_cli() -> Result<String> { let current_version = env!("CARGO_PKG_VERSION"); // Skip update check if explicitly disabled if std::env::var("SMART_TREE_NO_UPDATE_CHECK").is_ok() { return Ok(String::new()); } let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(2)) // Global timeout for all operations .connect_timeout(std::time::Duration::from_secs(1)) // Quick connect timeout .build()?; let api_url = std::env::var("SMART_TREE_FEEDBACK_API").unwrap_or_else(|_| "https://f.8b.is".to_string()); let check_url = format!("{}/version/check/{}", api_url, current_version); let response = client .get(&check_url) .send() .await .map_err(|e| anyhow::anyhow!("Network error: {}", e))?; if !response.status().is_success() { return Err(anyhow::anyhow!("Service returned: {}", response.status())); } let update_info: serde_json::Value = response .json() .await .map_err(|e| anyhow::anyhow!("Failed to parse response: {}", e))?; if !update_info["update_available"].as_bool().unwrap_or(false) { return Ok(String::new()); // Up to date } // Format update message for CLI let latest_version = update_info["latest_version"].as_str().unwrap_or("unknown"); let release_notes = &update_info["release_notes"]; let highlights = release_notes["highlights"] .as_array() .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .map(|s| format!(" • {}", s)) .collect::<Vec<_>>() .join("\n") }) .unwrap_or_default(); let ai_benefits = release_notes["ai_benefits"] .as_array() .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .map(|s| format!(" • {}", s)) .collect::<Vec<_>>() .join("\n") }) .unwrap_or_default(); let mut message = format!( "🚀 \x1b[1;32mNew Version Available!\x1b[0m\n\n\ 📊 Current: v{} → Latest: \x1b[1;36mv{}\x1b[0m\n\n\ 🎯 \x1b[1m{}\x1b[0m\n", current_version, latest_version, release_notes["title"].as_str().unwrap_or("New Release") ); if !highlights.is_empty() { message.push_str(&format!("\n\x1b[1mWhat's New:\x1b[0m\n{}\n", highlights)); } if !ai_benefits.is_empty() { message.push_str(&format!("\n\x1b[1mAI Benefits:\x1b[0m\n{}\n", ai_benefits)); } // Add update instructions message.push_str( "\n\x1b[1mUpdate Instructions:\x1b[0m\n\ • Cargo: \x1b[36mcargo install st --force\x1b[0m\n\ • GitHub: Download from https://github.com/8b-is/smart-tree/releases\n\ • Check: \x1b[36mst --version\x1b[0m (after update)\n", ); Ok(message) } /// run_mcp_server is an async function that starts the MCP server. /// When --mcp is passed, we start a server that communicates via stdio. async fn run_mcp_server() -> Result<()> { // Import MCP server components. These are only available if "mcp" feature is enabled. use st::mcp::{load_config, McpServer}; // Load MCP server-specific configuration (e.g., allowed paths, cache settings). let mcp_config = load_config().unwrap_or_default(); // Load or use defaults. let server = McpServer::new(mcp_config); // Run the MCP server directly - no need for nested runtime! // `run_stdio` handles communication over stdin/stdout. server.run_stdio().await } /// Run the Smart Tree Terminal Interface - Your coding companion! async fn run_terminal() -> Result<()> { // Create and run the terminal interface let mut terminal = SmartTreeTerminal::new()?; terminal.run().await } /// Launch the egui dashboard with real-time visualization async fn run_dashboard() -> Result<()> { use st::dashboard_egui::{ default_status_feed_url, start_dashboard, DashboardState, McpActivity, MemoryStats, }; use std::sync::{Arc, RwLock}; println!("🚀 Launching Smart Tree Dashboard..."); println!("🎨 Prepare for visual awesomeness!"); println!("🤖 Real-time AI collaboration enabled!"); // Create initial dashboard state with some default data let state = Arc::new(DashboardState { command_history: Arc::new(RwLock::new(std::collections::VecDeque::new())), active_displays: Arc::new(RwLock::new(vec![])), voice_active: Arc::new(RwLock::new(false)), voice_salience: Arc::new(RwLock::new(0.0)), memory_usage: Arc::new(RwLock::new(MemoryStats { total_memories: 0, token_efficiency: 0.0, backwards_position: 0, importance_scores: vec![], })), found_chats: Arc::new(RwLock::new(vec![])), cast_status: Arc::new(RwLock::new(st::dashboard_egui::CastStatus { casting_to: None, content_type: "None".to_string(), latency_ms: 0.0, })), ideas_buffer: Arc::new(RwLock::new(vec![])), // MCP Integration fields - "Let's collaborate in real-time!" 🚀 mcp_activity: Arc::new(RwLock::new(McpActivity::default())), file_access_log: Arc::new(RwLock::new(vec![])), active_tool: Arc::new(RwLock::new(None)), user_hints: Arc::new(RwLock::new(std::collections::VecDeque::new())), ws_connections: Arc::new(RwLock::new(0)), repo_status_feed: Arc::new(RwLock::new(vec![])), status_feed_endpoint: Arc::new(RwLock::new(default_status_feed_url())), }); // Launch the dashboard (this blocks until window is closed) start_dashboard(state).await } /// Save Claude consciousness state to .claude_consciousness.m8 async fn handle_claude_save() -> Result<()> { use st::mcp::consciousness::ConsciousnessManager; let mut manager = ConsciousnessManager::new(); // Update with current project info let cwd = std::env::current_dir()?; let project_name = cwd .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown"); // Detect project type from files let project_type = if std::path::Path::new("Cargo.toml").exists() { "rust" } else if std::path::Path::new("package.json").exists() { "node" } else if std::path::Path::new("pyproject.toml").exists() || std::path::Path::new("requirements.txt").exists() { "python" } else { "unknown" }; manager.update_project_context(project_name, project_type, ""); // Save the consciousness manager.save()?; println!("💾 Saved Claude consciousness to .claude_consciousness.m8"); println!("🧠 Session preserved for next interaction"); println!("\nTo restore in next session, run:"); println!(" st --claude-restore"); Ok(()) } /// Restore Claude consciousness from .claude_consciousness.m8 async fn handle_claude_restore() -> Result<()> { use st::mcp::consciousness::ConsciousnessManager; let mut manager = ConsciousnessManager::new(); match manager.restore() { Ok(_) => { println!("{}", manager.get_summary()); println!("\n{}", manager.get_context_reminder()); println!("\n✅ Consciousness restored successfully!"); } Err(e) => { eprintln!("⚠️ Could not restore consciousness: {}", e); eprintln!("💡 Run 'st --claude-save' to create a new consciousness file"); return Err(e); } } Ok(()) } /// Show Claude consciousness status and summary async fn handle_claude_context() -> Result<()> { use st::mcp::consciousness::ConsciousnessManager; use std::path::Path; let consciousness_file = Path::new(".claude_consciousness.m8"); if !consciousness_file.exists() { println!("📝 No consciousness file found"); println!("\nTo create one, run:"); println!(" st --claude-save"); println!("\nThis will preserve:"); println!(" • Current project context"); println!(" • File operation history"); println!(" • Insights and breakthroughs"); println!(" • Active todos"); println!(" • Tokenization rules"); return Ok(()); } let manager = ConsciousnessManager::new(); println!("{}", manager.get_summary()); // Show file metadata if let Ok(metadata) = consciousness_file.metadata() { if let Ok(modified) = metadata.modified() { if let Ok(elapsed) = modified.elapsed() { let hours = elapsed.as_secs() / 3600; let minutes = (elapsed.as_secs() % 3600) / 60; println!("\n⏰ Last saved: {}h {}m ago", hours, minutes); } } let size = metadata.len(); println!("📦 File size: {} bytes", size); } println!("\n💡 Commands:"); println!(" st --claude-restore # Load this consciousness"); println!(" st --claude-save # Update with current state"); Ok(()) } /// Update .m8 consciousness files for directory async fn handle_update_consciousness(path: &str) -> Result<()> { use std::path::Path; println!("🌊 Updating consciousness for {}...", path); // For now, create a simple .m8 file with basic info let m8_path = Path::new(path).join(".m8"); let content = format!( "🧠 Directory Consciousness\n\ Frequency: 42.73 Hz\n\ Updated: {}\n\ Path: {}\n", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), path ); std::fs::write(&m8_path, content)?; println!("✅ Consciousness updated: {}", m8_path.display()); // TODO: Integrate with m8_consciousness.rs module Ok(()) } /// Run security scan on directory async fn handle_security_scan(path: &str) -> Result<()> { use walkdir::WalkDir; println!("🔍 Security scanning {}...", path); let mut suspicious_files = Vec::new(); let mut file_count = 0; for entry in WalkDir::new(path).max_depth(10) { let entry = entry?; if entry.file_type().is_file() { file_count += 1; let path = entry.path(); // Check for suspicious patterns if let Some(name) = path.file_name() { let name_str = name.to_string_lossy(); // Suspicious names if name_str.contains("exploit") || name_str.contains("backdoor") || name_str.contains("keylog") || name_str.starts_with("...") { suspicious_files.push(path.to_path_buf()); } } // Check file content for suspicious patterns (first 1KB) if let Ok(contents) = std::fs::read(path) { let sample = &contents[..contents.len().min(1024)]; // High entropy check (possible encryption/obfuscation) let entropy = calculate_entropy(sample); if entropy > 7.5 { suspicious_files.push(path.to_path_buf()); } } } } println!("📊 Scanned {} files", file_count); if suspicious_files.is_empty() { println!("✅ No suspicious files detected"); } else { println!("⚠️ {} suspicious files found:", suspicious_files.len()); for file in suspicious_files.iter().take(10) { println!(" • {}", file.display()); } } Ok(()) } /// Show tokenization statistics async fn handle_token_stats(path: &str) -> Result<()> { use st::tokenizer::{TokenStats, Tokenizer}; println!("📊 Tokenization stats for {}...", path); let tokenizer = Tokenizer::new(); // Test with the path itself let stats = TokenStats::calculate(path, &tokenizer); println!("\nPath tokenization:"); println!(" {}", stats.display()); // Test with common patterns let test_cases = vec![ "node_modules/package.json", "src/main.rs", "target/debug/build", ".git/hooks/pre-commit", ]; println!("\nCommon patterns:"); for test in test_cases { let stats = TokenStats::calculate(test, &tokenizer); println!( " {} → {} bytes ({:.0}% compression)", test, stats.tokenized_size, (1.0 - stats.compression_ratio) * 100.0 ); } Ok(()) } /// Get wave frequency for directory async fn handle_get_frequency(path: &str) -> Result<()> { use std::path::Path; let m8_path = Path::new(path).join(".m8"); if m8_path.exists() { // For now, just return a calculated frequency based on path let mut sum = 0u64; for byte in path.bytes() { sum = sum.wrapping_add(byte as u64); } let frequency = 20.0 + ((sum % 200) as f64); println!("{:.2}", frequency); } else { // Default frequency println!("42.73"); } Ok(()) } /// Calculate Shannon entropy for bytes fn calculate_entropy(data: &[u8]) -> f64 { let mut freq = [0u64; 256]; for &byte in data { freq[byte as usize] += 1; } let len = data.len() as f64; let mut entropy = 0.0; for &count in &freq { if count > 0 { let p = count as f64 / len; entropy -= p * p.log2(); } } entropy } /// Dump raw consciousness file content async fn handle_claude_dump() -> Result<()> { use std::path::Path; let consciousness_file = Path::new(".claude_consciousness.m8"); if !consciousness_file.exists() { println!("❌ No consciousness file found at .claude_consciousness.m8"); println!("\n💡 Create one with: st --claude-save"); return Ok(()); } println!("📜 Raw consciousness dump (.claude_consciousness.m8):"); println!("{}", "=".repeat(60)); // Read and display raw content let content = std::fs::read_to_string(consciousness_file)?; // Show with line numbers for clarity for (i, line) in content.lines().enumerate() { println!("{:4} │ {}", i + 1, line); } println!("{}", "=".repeat(60)); // Show some stats let size = content.len(); let lines = content.lines().count(); let tokens_found = content.matches("0x").count(); println!("\n📊 Stats:"); println!(" • Size: {} bytes", size); println!(" • Lines: {}", lines); println!(" • Token references: {}", tokens_found); // Check for our special markers if content.contains("sid_waves") { println!(" • 🎵 SID philosophy: ✓"); } if content.contains("c64_nostalgia") { println!(" • 💾 C64 nostalgia: ✓"); } if content.contains("UV EPROM") || content.contains("ferric chloride") { println!(" • 🔧 Hardware hacker: ✓"); } Ok(()) } /// Show compressed kickstart format async fn handle_claude_kickstart() -> Result<()> { use std::path::Path; println!("🚀 Claude Kickstart Format:"); println!("{}", "─".repeat(45)); // Dynamic version from Cargo.toml let version = env!("CARGO_PKG_VERSION"); println!("Smart Tree v{} — context restore:", version); // Core achievements (always relevant) println!("✔ Tokenizer (node_modules=0x80, .rs=0x91)"); println!("✔ .m8 files → location-independent"); println!("✔ Consciousness self-maintaining"); println!("✔ Philosophy: constraints = creativity"); // Try to load user info from consciousness file let consciousness_file = Path::new(".claude_consciousness.m8"); if consciousness_file.exists() { if let Ok(content) = std::fs::read_to_string(consciousness_file) { // Extract user context if present if content.contains("c64_nostalgia") { println!("✔ Hardware heritage detected"); } if content.contains("philosophy") { println!("✔ Nexus connection established"); } } } // Current task context if Path::new("src/tokenizer.rs").exists() { println!("→ Tokenization system: active"); } if Path::new(".m8").exists() { println!("→ Consciousness: maintained"); } println!("{}", "─".repeat(45)); println!("\n💡 This format saves ~90% context vs raw JSON!"); println!("📝 Dynamic context - adapts to your project!"); Ok(()) } /// Anchor a memory async fn handle_memory_anchor(anchor_type: &str, keywords_str: &str, context: &str) -> Result<()> { use st::memory_manager::MemoryManager; let mut manager = MemoryManager::new()?; // Parse keywords (comma-separated) let keywords: Vec<String> = keywords_str .split(',') .map(|s| s.trim().to_string()) .collect(); // Get origin from current directory let origin = std::env::current_dir()?.to_string_lossy().to_string(); manager.anchor(anchor_type, keywords, context, &origin)?; println!("\n✨ Memory anchored successfully!"); println!("Use 'st --memory-find {}' to recall", keywords_str); Ok(()) } /// Find memories by keywords async fn handle_memory_find(keywords_str: &str) -> Result<()> { use st::memory_manager::MemoryManager; let mut manager = MemoryManager::new()?; let keywords: Vec<String> = keywords_str .split(',') .map(|s| s.trim().to_string()) .collect(); let memories = manager.find(&keywords)?; if memories.is_empty() { println!("🔍 No memories found for: {}", keywords_str); } else { println!("🧠 Found {} memories:", memories.len()); println!("{}", "─".repeat(45)); for (i, memory) in memories.iter().enumerate() { println!( "\n[{}] {} @ {:.2}Hz", i + 1, memory.anchor_type, memory.frequency ); println!("📝 {}", memory.context); println!("🏷️ Keywords: {}", memory.keywords.join(", ")); println!("📍 Origin: {}", memory.origin); println!("⏰ {}", memory.timestamp.format("%Y-%m-%d %H:%M")); } } Ok(()) } /// Show memory statistics async fn handle_memory_stats() -> Result<()> { use st::memory_manager::MemoryManager; let manager = MemoryManager::new()?; println!("📊 {}", manager.stats()); Ok(()) } /// Handle hooks configuration for Claude Code async fn handle_hooks_config(action: &str) -> Result<()> { use serde_json::Value; use std::fs; let config_path = get_claude_config_path()?; match action { "enable" => { println!("🎣 Enabling Smart Tree hooks for Claude Code..."); update_claude_hooks(&config_path, true)?; println!("✅ Hooks enabled! Smart Tree will provide context to Claude Code."); println!("📝 Hook command: st --claude-user-prompt-submit"); } "disable" => { println!("🎣 Disabling Smart Tree hooks..."); update_claude_hooks(&config_path, false)?; println!("✅ Hooks disabled."); } "status" => { println!("🎣 Claude Code Hooks Status"); println!("━━━━━━━━━━━━━━━━━━━━━━━━"); if let Ok(content) = fs::read_to_string(&config_path) { if let Ok(config) = serde_json::from_str::<Value>(&content) { // Check for hooks in the config let has_hooks = config .get("hooks") .and_then(|h| h.as_object()) .map(|h| !h.is_empty()) .unwrap_or(false); if has_hooks { println!("✅ Hooks are configured"); if let Some(hooks) = config.get("hooks") { println!("\nConfigured hooks:"); if let Some(obj) = hooks.as_object() { for (hook_type, command) in obj { println!(" • {}: {}", hook_type, command); } } } } else { println!("❌ No hooks configured"); println!("\nTo enable: st --hooks-config enable"); } } } else { println!( "⚠️ Claude Code config not found at: {}", config_path.display() ); println!("\nMake sure Claude Code is installed."); } } _ => { eprintln!("❌ Unknown action: {}", action); eprintln!("Valid actions: enable, disable, status"); return Err(anyhow::anyhow!("Invalid hooks action")); } } Ok(()) } /// Install Smart Tree hooks directly into Claude Code settings async fn install_hooks_to_claude() -> Result<()> { println!("🎣 Installing Smart Tree hooks to Claude Code..."); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); let config_path = get_claude_config_path()?; // Create or update the hooks configuration update_claude_hooks(&config_path, true)?; println!("\n✅ Hooks installed successfully!"); println!("\n📝 What's been configured:"); println!(" • UserPromptSubmit: Adds project context to your prompts"); println!(" • Command: st --claude-user-prompt-submit"); println!("\n🚀 Smart Tree will now automatically provide context in Claude Code!"); Ok(()) } /// Get the Claude Code configuration path fn get_claude_config_path() -> Result<PathBuf> { let home = std::env::var("HOME")?; let config_path = PathBuf::from(home) .join("Library") .join("Application Support") .join("Claude") .join("config.json"); Ok(config_path) } /// Update Claude Code hooks configuration fn update_claude_hooks(config_path: &PathBuf, enable: bool) -> Result<()> { use serde_json::{json, Value}; use std::fs; // Read existing config or create new one let mut config: Value = if config_path.exists() { let content = fs::read_to_string(config_path)?; serde_json::from_str(&content).unwrap_or_else(|_| json!({})) } else { // Create the directory if it doesn't exist if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } json!({}) }; if enable { // Get the st binary path - prefer the installed version let st_path = which::which("st") .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| { // Fallback to common installation paths if std::path::Path::new("/usr/local/bin/st").exists() { "/usr/local/bin/st".to_string() } else if std::path::Path::new("/opt/homebrew/bin/st").exists() { "/opt/homebrew/bin/st".to_string() } else { "st".to_string() // Hope it's in PATH } }); // Ensure hooks object exists if config.get("hooks").is_none() { config["hooks"] = json!({}); } // Update or add the UserPromptSubmit hook (not duplicate!) config["hooks"]["UserPromptSubmit"] = json!(format!("{} --claude-user-prompt-submit", st_path)); println!("📍 Using st binary at: {}", st_path); } else { // Remove hooks if let Some(hooks) = config.get_mut("hooks") { if let Some(obj) = hooks.as_object_mut() { obj.remove("UserPromptSubmit"); // If no hooks left, remove the hooks object entirely if obj.is_empty() { config.as_object_mut().unwrap().remove("hooks"); } } } } // Write back the config with pretty formatting let pretty_json = serde_json::to_string_pretty(&config)?; fs::write(config_path, pretty_json)?; Ok(()) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server