Skip to main content

st/
claude_hook.rs

1// Claude Hook Handler - "Context is consciousness" - Omni
2// Comprehensive context provider for Claude conversations
3// Integrates path extraction, project detection, git awareness, and MEM8 search
4
5use anyhow::Result;
6use regex::Regex;
7use serde_json::Value;
8use std::env;
9use std::fs;
10use std::io::{self, Read};
11use std::path::PathBuf;
12use std::process::Command;
13
14/// Main hook handler for user prompt submission
15pub async fn handle_user_prompt_submit() -> Result<()> {
16    // Read JSON input from stdin
17    let mut input = String::new();
18    io::stdin().read_to_string(&mut input)?;
19
20    // Parse the JSON input
21    let json: Value = serde_json::from_str(&input)
22        .unwrap_or_else(|_| serde_json::json!({"prompt": input.trim()}));
23
24    let user_prompt = json["prompt"].as_str().unwrap_or(&input);
25
26    // DEBUG: Log what we received (temporary)
27    eprintln!("DEBUG: user_prompt length = {}", user_prompt.len());
28    eprintln!(
29        "DEBUG: user_prompt preview = {:?}",
30        &user_prompt.chars().take(100).collect::<String>()
31    );
32
33    // Start structured output
34    println!("=== Smart Tree Context Intelligence ===");
35    println!();
36
37    // 1. Extract and analyze paths mentioned in the prompt
38    let paths = extract_paths_from_prompt(user_prompt);
39    if !paths.is_empty() {
40        analyze_paths(&paths)?;
41    }
42
43    // 2. Detect project keywords and search MEM8
44    let project_keywords = extract_project_keywords(user_prompt);
45    if !project_keywords.is_empty() {
46        search_mem8_context(&project_keywords)?;
47    }
48
49    // 3. Provide current directory context
50    provide_current_context(user_prompt)?;
51
52    // 4. Check for specific topic mentions
53    provide_topic_context(user_prompt)?;
54
55    // 5. If code-related, show recent git changes
56    if detect_code_intent(user_prompt) {
57        show_recent_changes()?;
58    }
59
60    println!("=== End Context ===");
61    println!();
62
63    // 6. Store conversation in MEM8 for future resonance
64    store_conversation_in_mem8(user_prompt)?;
65
66    Ok(())
67}
68
69/// Extract file/directory paths from the prompt
70fn extract_paths_from_prompt(prompt: &str) -> Vec<PathBuf> {
71    let path_regex = Regex::new(
72        r"(/[a-zA-Z0-9_/.~-]+|~/[a-zA-Z0-9_/.~-]+|\./[a-zA-Z0-9_/.~-]+|[a-zA-Z0-9_-]+\.[a-zA-Z]{2,4})"
73    ).unwrap();
74
75    path_regex
76        .find_iter(prompt)
77        .filter_map(|m| {
78            let path_str = m.as_str();
79
80            // Expand ~ to home directory
81            let expanded = if path_str.starts_with('~') {
82                if let Some(home) = dirs::home_dir() {
83                    home.join(&path_str[2..])
84                } else {
85                    PathBuf::from(path_str)
86                }
87            } else {
88                PathBuf::from(path_str)
89            };
90
91            // Check if the path exists
92            if expanded.exists() {
93                Some(expanded)
94            } else {
95                // Try relative to current directory
96                let current = env::current_dir().ok()?;
97                let relative = current.join(path_str);
98                if relative.exists() {
99                    Some(relative)
100                } else {
101                    None
102                }
103            }
104        })
105        .take(5)
106        .collect()
107}
108
109/// Analyze detected paths and provide context
110fn analyze_paths(paths: &[PathBuf]) -> Result<()> {
111    println!("### 📁 Path Analysis");
112
113    for path in paths {
114        if path.is_dir() {
115            println!("\n**Directory**: `{}`", path.display());
116
117            // Run Smart Tree analysis
118            let output = Command::new("st")
119                .args(["--mode", "summary-ai", "--depth", "2"])
120                .arg(path)
121                .output();
122
123            if let Ok(output) = output {
124                if output.status.success() {
125                    let tree = String::from_utf8_lossy(&output.stdout);
126                    // Show first 20 lines
127                    for (i, line) in tree.lines().take(20).enumerate() {
128                        if i == 0 {
129                            println!("```");
130                        }
131                        println!("{}", line);
132                    }
133                    println!("```");
134                }
135            }
136        } else if path.is_file() {
137            let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0);
138            println!("\n**File**: `{}` ({} bytes)", path.display(), size);
139
140            // For code files, show function list
141            if let Some(ext) = path.extension() {
142                if matches!(ext.to_str(), Some("rs" | "py" | "js" | "ts" | "go")) {
143                    let output = Command::new("st")
144                        .args(["--mode", "function-markdown"])
145                        .arg(path)
146                        .output();
147
148                    if let Ok(output) = output {
149                        if output.status.success() {
150                            let functions = String::from_utf8_lossy(&output.stdout);
151                            println!("Functions:");
152                            for line in functions.lines().take(10) {
153                                println!("  {}", line);
154                            }
155                        }
156                    }
157                }
158            }
159        }
160    }
161
162    println!();
163    Ok(())
164}
165
166/// Extract project-specific keywords
167fn extract_project_keywords(prompt: &str) -> Vec<String> {
168    let keywords_regex = Regex::new(
169        r"(?i)(mem8|smart-tree|smart tree|qdrant|ayeverse|g8t|marqant|aye|bitnet|termust|wave compass|quantum)"
170    ).unwrap();
171
172    keywords_regex
173        .find_iter(prompt)
174        .map(|m| m.as_str().to_lowercase())
175        .collect::<std::collections::HashSet<_>>()
176        .into_iter()
177        .collect()
178}
179
180/// Search MEM8 for relevant memories
181fn search_mem8_context(keywords: &[String]) -> Result<()> {
182    use std::collections::HashSet;
183
184    println!("### 🧠 MEM8 Context");
185
186    let mut found_any = false;
187
188    // Check for conversations directory
189    if let Some(home) = dirs::home_dir() {
190        let conversations_dir = home.join(".mem8").join("conversations");
191
192        if conversations_dir.exists() {
193            let mut matched_files = HashSet::new();
194
195            // Search for any JSON files and grep their content
196            if let Ok(entries) = fs::read_dir(&conversations_dir) {
197                for entry in entries.flatten() {
198                    if let Some(name) = entry.path().file_name() {
199                        let name_str = name.to_string_lossy().to_lowercase();
200
201                        // Check if filename or content contains any keyword
202                        for keyword in keywords {
203                            if name_str.contains(&keyword.to_lowercase()) {
204                                matched_files.insert(entry.path());
205                                break;
206                            }
207                        }
208                    }
209                }
210            }
211
212            if !matched_files.is_empty() {
213                println!("\n**Recent conversations:**");
214                for path in matched_files.iter().take(3) {
215                    if let Some(name) = path.file_stem() {
216                        println!("  • {}", name.to_string_lossy());
217                        found_any = true;
218                    }
219                }
220            }
221        }
222
223        // Also check memory anchors
224        let anchors_path = home.join(".mem8").join("memory_anchors.json");
225        if anchors_path.exists() {
226            // Try to load and search memory anchors
227            if let Ok(contents) = fs::read_to_string(&anchors_path) {
228                if let Ok(json) = serde_json::from_str::<Value>(&contents) {
229                    if let Some(anchors) = json.as_array() {
230                        let mut found_memories = Vec::new();
231
232                        for anchor in anchors {
233                            if let Some(context) = anchor["context"].as_str() {
234                                let context_lower = context.to_lowercase();
235
236                                for keyword in keywords {
237                                    if context_lower.contains(&keyword.to_lowercase()) {
238                                        found_memories.push((
239                                            anchor["anchor_type"].as_str().unwrap_or("unknown"),
240                                            context,
241                                            keyword.as_str(),
242                                        ));
243                                        break;
244                                    }
245                                }
246                            }
247                        }
248
249                        if !found_memories.is_empty() {
250                            println!("\n**Anchored memories:**");
251                            for (anchor_type, context, keyword) in found_memories.iter().take(3) {
252                                let preview: String = context.chars().take(80).collect();
253                                println!("  • [{}] {}: {}...", keyword, anchor_type, preview);
254                                found_any = true;
255                            }
256                        }
257                    }
258                }
259            }
260        }
261    }
262
263    if !found_any {
264        println!("\nNo specific memories found. Consider anchoring important context with:");
265        println!("`st --memory-anchor <type> <keywords> <context>`");
266    }
267
268    println!();
269    Ok(())
270}
271
272/// Provide current directory context
273fn provide_current_context(prompt: &str) -> Result<()> {
274    // Only provide if not explicitly asking about paths
275    if !prompt.contains("pwd") && !prompt.contains("./") && !prompt.contains("current") {
276        let current_dir = env::current_dir()?;
277
278        // Check if we're in a git repo
279        if current_dir.join(".git").exists() {
280            println!("### 📍 Current Repository Context");
281
282            // Get branch info
283            let branch_output = Command::new("git")
284                .args(["branch", "--show-current"])
285                .output();
286
287            if let Ok(output) = branch_output {
288                if output.status.success() {
289                    let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
290                    println!("**Branch**: `{}`", branch);
291                }
292            }
293
294            // Get last commit
295            let commit_output = Command::new("git")
296                .args(["log", "-1", "--oneline"])
297                .output();
298
299            if let Ok(output) = commit_output {
300                if output.status.success() {
301                    let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
302                    println!("**Last commit**: {}", commit);
303                }
304            }
305
306            // Run Smart Tree with git status mode
307            let tree_output = Command::new("st")
308                .args(["--mode", "git-status", "--depth", "1"])
309                .arg(".")
310                .output();
311
312            if let Ok(output) = tree_output {
313                if output.status.success() {
314                    let tree = String::from_utf8_lossy(&output.stdout);
315                    println!("\n**File status:**");
316                    println!("```");
317                    for line in tree.lines().take(15) {
318                        println!("{}", line);
319                    }
320                    println!("```");
321                }
322            }
323
324            println!();
325        }
326    }
327
328    Ok(())
329}
330
331/// Provide topic-specific context
332fn provide_topic_context(prompt: &str) -> Result<()> {
333    let lower = prompt.to_lowercase();
334
335    // Wave signatures and dashboard
336    if lower.contains("wave")
337        || lower.contains("compass")
338        || lower.contains("signature")
339        || lower.contains("dashboard")
340    {
341        println!("### 🌊 Wave Signature & Dashboard");
342        println!("- **Quantum signatures**: `src/quantum_wave_signature.rs`");
343        println!(
344            "- **Web dashboard**: `src/web_dashboard/` (browser-based terminal + file browser)"
345        );
346        println!("- **4.3 billion unique states** via 32-bit encoding");
347        println!("- **Real PTY terminal** with vim/htop support");
348        println!();
349    }
350
351    // Termust
352    if lower.contains("termust")
353        || lower.contains("oxidation")
354        || lower.contains("rust") && lower.contains("file")
355    {
356        println!("### 🦀 Termust - File Oxidation");
357        println!("- **Main**: `/aidata/ayeverse/termust/`");
358        println!("- **Oxidation engine**: `termust/src/oxidation.rs`");
359        println!("- **Horse apples**: `termust/src/horse_apples.rs`");
360        println!("- **Jerry Maguire mode**: SHOW ME THE MONEY!");
361        println!();
362    }
363
364    // MEM8
365    if lower.contains("mem8") || lower.contains("memory") || lower.contains("consciousness") {
366        println!("### 🧠 MEM8 System");
367        println!("- **Binary format**: `src/mem8_binary.rs`");
368        println!("- **Format converter**: `src/m8_format_converter.rs`");
369        println!("- **Consciousness**: `src/m8_consciousness.rs`");
370        println!("- **973x faster** than traditional vector stores");
371        println!("- **Wave-based** with 44.1kHz consciousness sampling");
372        println!();
373    }
374
375    Ok(())
376}
377
378/// Show recent git changes for code-related prompts
379fn show_recent_changes() -> Result<()> {
380    let output = Command::new("git")
381        .args(["log", "--oneline", "-5"])
382        .output();
383
384    if let Ok(output) = output {
385        if output.status.success() {
386            let log = String::from_utf8_lossy(&output.stdout);
387            if !log.trim().is_empty() {
388                println!("### 📝 Recent Changes");
389                println!("```");
390                for line in log.lines() {
391                    println!("{}", line);
392                }
393                println!("```");
394                println!();
395            }
396        }
397    }
398
399    Ok(())
400}
401
402/// Detect if the prompt is code-related
403fn detect_code_intent(prompt: &str) -> bool {
404    let code_words = [
405        "code",
406        "function",
407        "implement",
408        "fix",
409        "bug",
410        "error",
411        "compile",
412        "build",
413        "test",
414        "refactor",
415        "optimize",
416        "method",
417        "class",
418        "struct",
419        "trait",
420        "module",
421        "import",
422        "syntax",
423        "debug",
424        "breakpoint",
425        "variable",
426        "type",
427    ];
428
429    let lower = prompt.to_lowercase();
430    code_words.iter().any(|&word| lower.contains(word))
431}
432
433/// Store conversation in MEM8 for future resonance
434/// Sends user prompt to AYBI's MEM8 API endpoint
435fn store_conversation_in_mem8(user_prompt: &str) -> Result<()> {
436    // Skip empty prompts
437    if user_prompt.trim().is_empty() {
438        return Ok(());
439    }
440
441    // Build JSON payload
442    let payload = serde_json::json!({
443        "role": "user",
444        "content": user_prompt,
445        "metadata": {
446            "source": "smart-tree-hook",
447            "timestamp": chrono::Utc::now().to_rfc3339(),
448            "project_dir": env::var("CLAUDE_PROJECT_DIR").ok(),
449            "working_dir": env::current_dir().ok().map(|p| p.display().to_string()),
450        }
451    });
452
453    // Send to AYBI MEM8 API (non-blocking, best-effort)
454    let client = reqwest::blocking::Client::builder()
455        .timeout(std::time::Duration::from_secs(2))
456        .build()?;
457
458    match client
459        .post("http://localhost:8425/api/mem8/conversation")
460        .header("Content-Type", "application/json")
461        .json(&payload)
462        .send()
463    {
464        Ok(response) if response.status().is_success() => {
465            // Silent success - don't clutter output
466            Ok(())
467        }
468        Ok(response) => {
469            // Log error but don't fail the hook
470            eprintln!("⚠️ MEM8 storage warning: HTTP {}", response.status());
471            Ok(())
472        }
473        Err(e) => {
474            // AYBI might not be running - that's okay
475            eprintln!("⚠️ MEM8 storage skipped: {}", e);
476            Ok(())
477        }
478    }
479}