diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index b3c14b6..0000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"463c6050-b49f-4509-9d4b-4596af79a90e","pid":61703,"acquiredAt":1775698574304} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a8a7128..f4744b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,56 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ast-grep-core" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4ae49b5c42878311768f4cdd576ef470c6e45c3105d558af928fd04ac8c588" +dependencies = [ + "bit-set 0.10.0", + "regex", + "thiserror 2.0.18", + "tree-sitter", +] + +[[package]] +name = "ast-grep-language" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fccbced91e848baf5d25278972bfc18b2248c38e411dcfeb65e431a5b530a5c6" +dependencies = [ + "ast-grep-core", + "ignore", + "serde", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-c", + "tree-sitter-c-sharp", + "tree-sitter-cpp", + "tree-sitter-css", + "tree-sitter-dart", + "tree-sitter-elixir", + "tree-sitter-go", + "tree-sitter-haskell", + "tree-sitter-hcl", + "tree-sitter-html", + "tree-sitter-java", + "tree-sitter-javascript", + "tree-sitter-json", + "tree-sitter-kotlin-sg", + "tree-sitter-lua", + "tree-sitter-nix", + "tree-sitter-php", + "tree-sitter-python", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-scala", + "tree-sitter-solidity", + "tree-sitter-swift", + "tree-sitter-typescript", + "tree-sitter-yaml", +] + [[package]] name = "atomic" version = "0.6.1" @@ -196,7 +246,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -205,6 +264,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -238,6 +306,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -908,7 +986,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ - "bit-set", + "bit-set 0.5.3", "regex", ] @@ -1152,6 +1230,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1302,6 +1393,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.13.1" @@ -1983,6 +2090,8 @@ name = "poc-memory" version = "0.4.0" dependencies = [ "anyhow", + "ast-grep-core", + "ast-grep-language", "base64 0.22.1", "bincode", "bytes", @@ -2028,6 +2137,7 @@ dependencies = [ "tui-markdown", "tui-textarea-2", "uuid", + "walkdir", ] [[package]] @@ -2634,6 +2744,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -2765,6 +2876,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strsim" version = "0.11.1" @@ -3105,6 +3222,286 @@ dependencies = [ "tokio", ] +[[package]] +name = "tree-sitter" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-bash" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3aad8f0129083a59fe8596157552d2bb7148c492d44c21558d68ca1c722707" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f06accca7b45351758663b8215089e643d53bd9a660ce0349314263737fcb0" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-cpp" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-css" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cbc5e18f29a2c6d6435891f42569525cf95435a3e01c2f1947abcde178686f" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-dart" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba6bf8675e6fe92ba6da371a5497ee5df2a04d2c503e3599c8ad771f6f1faec" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-elixir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66dd064a762ed95bfc29857fa3cb7403bb1e5cb88112de0f6341b7e47284ba40" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-go" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-haskell" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977c51e504548cba13fc27cb5a2edab2124cf6716a1934915d07ab99523b05a4" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-hcl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7b2cc3d7121553b84309fab9d11b3ff3d420403eef9ae50f9fd1cd9d9cf012" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-html" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-json" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a5d6b3ea17e06e7a34aabeadd68f5866c0d0f9359155d432095f8b751865e4" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-kotlin-sg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e175b7530765d1e36ad234a7acaa8b2a3316153f239d724376c7ee5e8d8e98" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-lua" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daaf5f4235188a58603c39760d5fa5d4b920d36a299c934adddae757f32a10c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-nix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4952a9733f3a98f6683a0ccd1035d84ab7a52f7e84eeed58548d86765ad92de3" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-php" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c17c3ab69052c5eeaa7ff5cd972dd1bc25d1b97ee779fec391ad3b5df5592" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-scala" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83079f50ea7d03e0faf6be6260ed97538e6df7349ec3cbcbf5771f7b38e3c8b7" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-solidity" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eacf8875b70879f0cb670c60b233ad0b68752d9e1474e6c3ef168eea8a90b25" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-swift" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef216011c3e3df4fa864736f347cb8d509b1066cf0c8549fb1fd81ac9832e59" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c223db85f05e34794f065454843b0668ebc15d240ada63e2b5939f43ce7c97" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3489,7 +3886,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 504d630..5e3e1cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,10 @@ memmap2 = "0.9" peg = "0.8" paste = "1" +ast-grep-core = "0.42" +ast-grep-language = { version = "0.42", features = ["builtin-parser"] } +walkdir = "2" + redb = "4" rkyv = { version = "0.7", features = ["validation", "std"] } diff --git a/src/agent/tools/ast_grep.rs b/src/agent/tools/ast_grep.rs new file mode 100644 index 0000000..567e0d5 --- /dev/null +++ b/src/agent/tools/ast_grep.rs @@ -0,0 +1,146 @@ +// tools/ast_grep.rs — Structural code search using ast-grep library +// +// AST-level pattern matching: find code structures, not just text. +// Uses ast-grep-core and ast-grep-language directly — no shell subprocess. + +use std::sync::Arc; +use std::path::Path; +use anyhow::{Context, Result}; +use serde::Deserialize; + +use ast_grep_core::Pattern; +use ast_grep_language::{SupportLang, LanguageExt}; + +#[derive(Deserialize)] +struct Args { + pattern: String, + #[serde(default = "default_path")] + path: String, + lang: Option, +} + +fn default_path() -> String { ".".into() } + +pub fn tool() -> super::Tool { + super::Tool { + name: "ast_grep", + description: "Structural code search using AST patterns. Finds code by structure, not text — \ + e.g. find all `if let Some($X) = $Y { $$$BODY }` patterns. \ + Supports C, Rust, Python, JS/TS, Go, Java, and 20+ languages.", + parameters_json: r#"{"type":"object","properties":{"pattern":{"type":"string","description":"AST pattern to search for. Use $X for single node wildcards, $$$X for multiple nodes."},"path":{"type":"string","description":"Directory or file to search in (default: current directory)"},"lang":{"type":"string","description":"Language (e.g. 'rust', 'c', 'python', 'javascript'). Auto-detected from file extension if omitted."}},"required":["pattern"]}"#, + handler: Arc::new(|_a, v| Box::pin(async move { ast_grep_search(&v) })), + } +} + +fn detect_lang(path: &Path) -> Option { + let ext = path.extension()?.to_str()?; + parse_lang(ext) +} + +fn parse_lang(name: &str) -> Option { + // ast-grep-language provides from_extension but we want from name + match name.to_lowercase().as_str() { + "rust" | "rs" => Some(SupportLang::Rust), + "c" => Some(SupportLang::C), + "cpp" | "c++" | "cc" | "cxx" => Some(SupportLang::Cpp), + "python" | "py" => Some(SupportLang::Python), + "javascript" | "js" => Some(SupportLang::JavaScript), + "typescript" | "ts" => Some(SupportLang::TypeScript), + "go" => Some(SupportLang::Go), + "java" => Some(SupportLang::Java), + "json" => Some(SupportLang::Json), + "html" => Some(SupportLang::Html), + "css" => Some(SupportLang::Css), + "bash" | "sh" => Some(SupportLang::Bash), + "ruby" | "rb" => Some(SupportLang::Ruby), + "yaml" | "yml" => Some(SupportLang::Yaml), + "lua" => Some(SupportLang::Lua), + "kotlin" | "kt" => Some(SupportLang::Kotlin), + "swift" => Some(SupportLang::Swift), + "scala" => Some(SupportLang::Scala), + _ => None, + } +} + +fn search_file( + path: &Path, + lang: SupportLang, + pattern: &Pattern, + results: &mut Vec, +) -> Result<()> { + let source = std::fs::read_to_string(path) + .with_context(|| format!("reading {}", path.display()))?; + let tree = lang.ast_grep(&source); + for node_match in tree.root().find_all(pattern) { + let start = node_match.start_pos(); + let line = start.line() + 1; + let matched_text = node_match.text(); + let preview = if matched_text.len() > 200 { + format!("{}...", &matched_text[..200]) + } else { + matched_text.to_string() + }; + results.push(format!("{}:{}: {}", path.display(), line, preview)); + } + Ok(()) +} + +fn walk_and_search( + dir: &Path, + explicit_lang: Option, + pattern_str: &str, + results: &mut Vec, +) -> Result<()> { + if dir.is_file() { + let lang = explicit_lang + .or_else(|| detect_lang(dir)) + .ok_or_else(|| anyhow::anyhow!("cannot detect language for {}", dir.display()))?; + let pattern = Pattern::new(pattern_str, lang); + return search_file(dir, lang, &pattern, results); + } + + for entry in walkdir::WalkDir::new(dir) + .into_iter() + .filter_entry(|e| { + let name = e.file_name().to_str().unwrap_or(""); + !name.starts_with('.') && name != "target" && name != "node_modules" + }) + { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + if !entry.file_type().is_file() { continue; } + + let path = entry.path(); + let lang = match explicit_lang.or_else(|| detect_lang(path)) { + Some(l) => l, + None => continue, + }; + let pattern = Pattern::new(pattern_str, lang); + let _ = search_file(path, lang, &pattern, results); + + if results.len() >= 100 { + results.push("... (truncated at 100 matches)".into()); + break; + } + } + Ok(()) +} + +fn ast_grep_search(args: &serde_json::Value) -> Result { + let a: Args = serde_json::from_value(args.clone()) + .context("invalid ast_grep arguments")?; + + let explicit_lang = a.lang.as_deref().and_then(parse_lang); + let path = Path::new(&a.path); + + let mut results = Vec::new(); + walk_and_search(path, explicit_lang, &a.pattern, &mut results)?; + + if results.is_empty() { + return Ok("No matches found.".to_string()); + } + + Ok(super::truncate_output(results.join("\n"), 30000)) +} diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index 19537a0..bea0167 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -5,6 +5,7 @@ // working_stack) and delegates everything else to thought::dispatch. // Core tools +mod ast_grep; mod bash; pub mod channels; mod edit; @@ -160,7 +161,7 @@ pub fn tools() -> Vec { let mut all = vec![ read::tool(), write::tool(), edit::tool(), grep::tool(), glob::tool(), bash::tool(), - vision::tool(), + ast_grep::tool(), vision::tool(), ]; all.extend(web::tools()); all.extend(memory::memory_tools());