mod analyzers;
pub(crate) mod internal;
mod parser;
mod scanner;
mod storage;
mod types;
#[doc(hidden)]
pub mod cli_main;
pub mod core;
mod loregrep;
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use crate::loregrep::{LoreGrep, LoreGrepBuilder};
pub use crate::core::types::{ScanResult, ToolResult, ToolSchema};
pub use crate::core::errors::{LoreGrepError, Result};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "python")]
#[pymodule]
#[pyo3(name = "loregrep")]
fn loregrep_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<python_bindings::PyLoreGrep>()?;
m.add_class::<python_bindings::PyLoreGrepBuilder>()?;
m.add_class::<python_bindings::PyScanResult>()?;
m.add_class::<python_bindings::PyToolResult>()?;
m.add_class::<python_bindings::PyToolSchema>()?;
m.add_class::<python_bindings::PyIndexCoverage>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}
#[cfg(feature = "python")]
pub mod python_bindings {
use super::*;
use crate::core::types::ScanResult;
use crate::loregrep::{LoreGrep, LoreGrepBuilder};
use pyo3::types::PyDict;
use serde_json::Value;
use std::sync::{Arc, Mutex};
#[pyclass(name = "LoreGrep")]
pub struct PyLoreGrep {
inner: Arc<Mutex<LoreGrep>>,
}
#[pymethods]
impl PyLoreGrep {
#[staticmethod]
fn builder() -> PyLoreGrepBuilder {
PyLoreGrepBuilder {
inner: LoreGrep::builder(),
}
}
#[staticmethod]
fn auto_discover(path: &str) -> PyResult<PyLoreGrep> {
let loregrep = LoreGrep::auto_discover(path).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Auto-discovery failed: {}",
e
))
})?;
Ok(PyLoreGrep {
inner: Arc::new(Mutex::new(loregrep)),
})
}
#[staticmethod]
fn rust_project(path: &str) -> PyResult<PyLoreGrep> {
let loregrep = LoreGrep::rust_project(path).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Rust project setup failed: {}",
e
))
})?;
Ok(PyLoreGrep {
inner: Arc::new(Mutex::new(loregrep)),
})
}
#[staticmethod]
fn python_project(path: &str) -> PyResult<PyLoreGrep> {
let loregrep = LoreGrep::python_project(path).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Python project setup failed: {}",
e
))
})?;
Ok(PyLoreGrep {
inner: Arc::new(Mutex::new(loregrep)),
})
}
#[staticmethod]
fn polyglot_project(path: &str) -> PyResult<PyLoreGrep> {
let loregrep = LoreGrep::polyglot_project(path).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Polyglot project setup failed: {}",
e
))
})?;
Ok(PyLoreGrep {
inner: Arc::new(Mutex::new(loregrep)),
})
}
fn scan<'py>(&self, py: Python<'py>, path: &str) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
let path = path.to_string();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let mut loregrep = {
let guard = inner.lock().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to acquire lock: {}",
e
))
})?;
guard.clone()
};
let result = loregrep.scan(&path).await.map_err(|e| match e {
crate::LoreGrepError::IoError(io_err) => {
PyErr::new::<pyo3::exceptions::PyOSError, _>(format!(
"IO error during scan: {}",
io_err
))
}
crate::LoreGrepError::AnalysisError(analysis_err) => {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Analysis error: {}",
analysis_err
))
}
_ => PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Scan failed: {}",
e
)),
})?;
{
let mut guard = inner.lock().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to acquire lock for update: {}",
e
))
})?;
*guard = loregrep;
}
Ok(PyScanResult::from_scan_result(result))
})
}
fn execute_tool<'py>(
&self,
py: Python<'py>,
tool_name: &str,
args: &Bound<'py, PyDict>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self.inner.clone();
let tool_name = tool_name.to_string();
let args_json: Value = pythonize::depythonize(args).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid tool arguments - could not convert to JSON: {}",
e
))
})?;
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let loregrep = {
let guard = inner.lock().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to acquire lock: {}",
e
))
})?;
guard.clone()
};
let result =
loregrep
.execute_tool(&tool_name, args_json)
.await
.map_err(|e| match e {
crate::LoreGrepError::ToolError(tool_err) => {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Tool '{}' execution failed: {}",
tool_name, tool_err
))
}
crate::LoreGrepError::JsonError(json_err) => {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Tool JSON error: {}",
json_err
))
}
crate::LoreGrepError::IoError(io_err) => {
PyErr::new::<pyo3::exceptions::PyOSError, _>(format!(
"Tool IO error: {}",
io_err
))
}
_ => PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Tool execution failed: {}",
e
)),
})?;
let metadata_str = serde_json::to_string(&result.data).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to serialize tool result metadata: {}",
e
))
})?;
let content = if result.success {
serde_json::to_string(&result.data).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to serialize tool result data: {}",
e
))
})?
} else {
result
.error
.unwrap_or_else(|| "Unknown tool error".to_string())
};
Ok(PyToolResult {
content,
metadata: metadata_str,
})
})
}
#[staticmethod]
fn get_tool_definitions() -> Vec<PyToolSchema> {
LoreGrep::get_tool_definitions()
.iter()
.map(|schema| PyToolSchema {
name: schema.name.clone(),
description: schema.description.clone(),
parameters: serde_json::to_string(&schema.input_schema)
.unwrap_or_else(|_| "{}".to_string()),
})
.collect()
}
#[staticmethod]
fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn get_stats(&self) -> PyResult<PyScanResult> {
let loregrep = self.lock()?;
let stats = loregrep.get_stats().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to read repository stats: {}",
e
))
})?;
Ok(PyScanResult::from_scan_result(stats))
}
fn index_coverage(&self) -> PyResult<PyIndexCoverage> {
let coverage = self.lock()?.index_coverage();
Ok(PyIndexCoverage {
files_indexed: coverage.files_indexed,
files_discovered: coverage.files_discovered,
truncated: coverage.truncated,
note: coverage.note(),
})
}
fn is_scanned(&self) -> PyResult<bool> {
Ok(self.lock()?.is_scanned())
}
fn set_scan_root(&self, root: &str) -> PyResult<()> {
self.lock()?.set_scan_root(root);
Ok(())
}
fn first_missing_indexed_path(&self) -> PyResult<Option<String>> {
Ok(self.lock()?.first_missing_indexed_path())
}
fn clear_index(&self) -> PyResult<()> {
self.lock()?.clear_index();
Ok(())
}
fn __repr__(&self) -> String {
"LoreGrep(configured and ready for repository analysis)".to_string()
}
}
impl PyLoreGrep {
fn lock(&self) -> PyResult<std::sync::MutexGuard<'_, LoreGrep>> {
self.inner.lock().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
"Failed to acquire lock: {}",
e
))
})
}
}
#[pyclass(name = "LoreGrepBuilder")]
pub struct PyLoreGrepBuilder {
inner: LoreGrepBuilder,
}
#[pymethods]
impl PyLoreGrepBuilder {
#[new]
fn new() -> Self {
PyLoreGrepBuilder {
inner: LoreGrepBuilder::new(),
}
}
fn max_file_size(mut slf: PyRefMut<Self>, size: u64) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().max_file_size(size);
slf
}
fn max_depth(mut slf: PyRefMut<Self>, depth: u32) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().max_depth(depth);
slf
}
fn unlimited_depth(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().unlimited_depth();
slf
}
fn max_files(mut slf: PyRefMut<Self>, limit: usize) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().max_files(limit);
slf
}
fn file_patterns(mut slf: PyRefMut<Self>, patterns: Vec<String>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().file_patterns(patterns);
slf
}
fn include_patterns(mut slf: PyRefMut<Self>, patterns: Vec<String>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().include_patterns(patterns);
slf
}
fn follow_symlinks(mut slf: PyRefMut<Self>, follow: bool) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().follow_symlinks(follow);
slf
}
fn configure_patterns_for_languages(
mut slf: PyRefMut<Self>,
languages: Vec<String>,
) -> PyRefMut<Self> {
slf.inner = slf
.inner
.clone()
.configure_patterns_for_languages(&languages);
slf
}
fn exclude_patterns(mut slf: PyRefMut<Self>, patterns: Vec<String>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().exclude_patterns(patterns);
slf
}
fn respect_gitignore(mut slf: PyRefMut<Self>, respect: bool) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().respect_gitignore(respect);
slf
}
fn with_rust_analyzer(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().with_rust_analyzer();
slf
}
fn with_python_analyzer(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().with_python_analyzer();
slf
}
fn with_typescript_analyzer(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().with_typescript_analyzer();
slf
}
fn with_all_analyzers(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().with_all_analyzers();
slf
}
fn optimize_for_performance(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().optimize_for_performance();
slf
}
fn comprehensive_analysis(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().comprehensive_analysis();
slf
}
fn exclude_common_build_dirs(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().exclude_common_build_dirs();
slf
}
fn exclude_test_dirs(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().exclude_test_dirs();
slf
}
fn exclude_vendor_dirs(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().exclude_vendor_dirs();
slf
}
fn include_source_files(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().include_source_files();
slf
}
fn include_config_files(mut slf: PyRefMut<Self>) -> PyRefMut<Self> {
slf.inner = slf.inner.clone().include_config_files();
slf
}
fn build(slf: PyRefMut<Self>) -> PyResult<PyLoreGrep> {
let loregrep = slf.inner.clone().build().map_err(|e| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Build failed: {}", e))
})?;
Ok(PyLoreGrep {
inner: Arc::new(Mutex::new(loregrep)),
})
}
fn __repr__(&self) -> String {
"LoreGrepBuilder(configurable repository analyzer)".to_string()
}
}
#[pyclass(name = "ScanResult")]
pub struct PyScanResult {
#[pyo3(get)]
pub files_scanned: usize,
#[pyo3(get)]
pub functions_found: usize,
#[pyo3(get)]
pub structs_found: usize,
#[pyo3(get)]
pub errors: Vec<String>,
#[pyo3(get)]
pub duration_ms: u64,
#[pyo3(get)]
pub languages: Vec<String>,
}
impl PyScanResult {
fn from_scan_result(result: ScanResult) -> Self {
PyScanResult {
files_scanned: result.files_scanned,
functions_found: result.functions_found,
structs_found: result.structs_found,
errors: Vec::new(), duration_ms: result.duration_ms,
languages: result.languages,
}
}
}
#[pymethods]
impl PyScanResult {
fn __repr__(&self) -> String {
format!(
"ScanResult(files={}, functions={}, structs={}, duration={}ms)",
self.files_scanned, self.functions_found, self.structs_found, self.duration_ms
)
}
}
#[pyclass(name = "IndexCoverage")]
pub struct PyIndexCoverage {
#[pyo3(get)]
pub files_indexed: usize,
#[pyo3(get)]
pub files_discovered: usize,
#[pyo3(get)]
pub truncated: bool,
#[pyo3(get)]
pub note: Option<String>,
}
#[pymethods]
impl PyIndexCoverage {
fn __repr__(&self) -> String {
format!(
"IndexCoverage(indexed={}, discovered={}, truncated={})",
self.files_indexed,
self.files_discovered,
if self.truncated { "True" } else { "False" }
)
}
}
#[pyclass(name = "ToolResult")]
pub struct PyToolResult {
#[pyo3(get)]
pub content: String,
#[pyo3(get)]
pub metadata: String,
}
#[pymethods]
impl PyToolResult {
fn __repr__(&self) -> String {
format!("ToolResult(content_len={})", self.content.len())
}
}
#[pyclass(name = "ToolSchema")]
pub struct PyToolSchema {
#[pyo3(get)]
pub name: String,
#[pyo3(get)]
pub description: String,
#[pyo3(get)]
pub parameters: String,
}
#[pymethods]
impl PyToolSchema {
fn __repr__(&self) -> String {
format!("ToolSchema(name='{}')", self.name)
}
}
}
#[cfg(feature = "python")]
pub use python_bindings::*;