use std::sync::Arc;
use crate::tool::{param_string, ToolSpec};
pub fn self_tools() -> Vec<ToolSpec> {
vec![
self_read_tool(),
self_patch_tool(),
self_inspect_tool(),
self_run_tests_tool(),
]
}
fn find_project_root() -> String {
let cwd = std::env::current_dir().unwrap_or_default();
let mut path = Some(cwd.as_path());
while let Some(p) = path {
if p.join("Cargo.toml").exists() {
return p.to_string_lossy().to_string();
}
path = p.parent();
}
cwd.to_string_lossy().to_string()
}
lazy_static::lazy_static! {
static ref CORTEX_ROOT: String = find_project_root();
}
fn is_allowed(path: &std::path::Path) -> bool {
let canonical = match path.canonicalize() {
Ok(p) => p,
Err(_) => return false,
};
let canonical_str = canonical.to_string_lossy().to_string();
let root = std::path::Path::new(&*CORTEX_ROOT);
let src_dir = root.join("src").canonicalize().ok();
let root_config = root.join("config.yaml").canonicalize().ok();
let root_cargo = root.join("Cargo.toml").canonicalize().ok();
if let Some(ref src) = src_dir {
if canonical_str.starts_with(&src.to_string_lossy().to_string()) {
return true;
}
}
if let Some(ref f) = root_config {
if &canonical == f { return true; }
}
if let Some(ref f) = root_cargo {
if &canonical == f { return true; }
}
false
}
fn resolve_path(path_str: &str) -> Option<String> {
let path = if path_str.starts_with('~') {
let home = std::env::var("HOME").unwrap_or_default();
path_str.replacen('~', &home, 1)
} else if !std::path::Path::new(path_str).is_absolute() {
std::path::Path::new(&*CORTEX_ROOT).join(path_str).to_string_lossy().to_string()
} else {
path_str.to_string()
};
let p = std::path::Path::new(&path);
if p.exists() && p.is_file() && is_allowed(p) {
p.canonicalize().ok().map(|c| c.to_string_lossy().to_string())
} else {
None
}
}
fn self_read_tool() -> ToolSpec {
let (params, _) = crate::tool::required_params(&[
("path", param_string("Path to the file (relative to project root, or absolute)")),
("limit", serde_json::json!({"type": "integer", "description": "Max lines to read", "default": 50})),
]);
ToolSpec::new("self_read", "Read lines from a file in the Cortex project", params,
Arc::new(|args| {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(50).min(200) as usize;
let resolved = resolve_path(path).ok_or_else(|| format!("Path not allowed or not found: {}. Must be within the Cortex project.", path))?;
let content = std::fs::read_to_string(&resolved).map_err(|e| format!("Error reading file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let selected = &lines[..std::cmp::min(limit, total)];
let mut result = format!("File: {} ({} lines)\n", resolved, total);
for (i, line) in selected.iter().enumerate() { result.push_str(&format!("{:4}|{}\n", i + 1, line)); }
if selected.len() < total { result.push_str(&format!("... ({} more lines)\n", total - selected.len())); }
Ok(result)
}),
)
}
fn self_patch_tool() -> ToolSpec {
let (params, _) = crate::tool::required_params(&[
("path", param_string("Path to the file (relative to project root)")),
("old_string", param_string("Exact text to find and replace")),
("new_string", param_string("Replacement text")),
]);
ToolSpec::new("self_patch", "Make a targeted edit to a file in the Cortex project", params,
Arc::new(|args| {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
let old_str = args.get("old_string").and_then(|v| v.as_str()).unwrap_or("");
let new_str = args.get("new_string").and_then(|v| v.as_str()).unwrap_or("");
let resolved = resolve_path(path).ok_or_else(|| format!("Path not allowed: {}. Must be within the Cortex project.", path))?;
if old_str.trim().is_empty() { return Err("old_string cannot be empty.".into()); }
let content = std::fs::read_to_string(&resolved).map_err(|e| format!("Error reading {}: {}", path, e))?;
if !content.contains(old_str) { return Err(format!("Could not find the specified text in {}. Use self_read to verify.", path)); }
let new_content = content.replacen(old_str, new_str, 1);
if new_content == content { return Ok("No changes made (old_string not found).".into()); }
std::fs::write(&resolved, &new_content).map_err(|e| format!("Error writing {}: {}", path, e))?;
Ok("Patched successfully.".into())
}),
)
}
fn self_inspect_tool() -> ToolSpec {
let (params, _) = crate::tool::required_params(&[("module", serde_json::json!({"type": "string", "description": "Module path to inspect", "default": "cortex"}))]);
ToolSpec::new("self_inspect", "List the modules and files in the Cortex project", params,
Arc::new(|args| {
let module = args.get("module").and_then(|v| v.as_str()).unwrap_or("cortex");
let module_path = std::path::Path::new(&*CORTEX_ROOT).join("src").join(module.replace('.', "/"));
if !module_path.exists() || !module_path.is_dir() { return Err(format!("Module path not found: {}", module)); }
let mut result = vec![format!("Module: {}", module)];
let mut entries: Vec<_> = std::fs::read_dir(&module_path).map_err(|e| format!("Error: {}", e))?.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());
for entry in &entries {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') { continue; }
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let rs_count = std::fs::read_dir(entry.path()).map(|d| d.filter_map(|e| e.ok()).filter(|e| e.file_name().to_string_lossy().ends_with(".rs")).count()).unwrap_or(0);
result.push(format!(" 📁 {}/ ({} files)", name, rs_count));
} else if name.ends_with(".rs") {
let size = std::fs::metadata(entry.path()).map(|m| m.len()).unwrap_or(0);
result.push(format!(" 📄 {} ({} bytes)", name, size));
}
}
Ok(result.join("\n"))
}),
)
}
fn self_run_tests_tool() -> ToolSpec {
let (params, _) = crate::tool::required_params(&[]);
ToolSpec::new("self_run_tests", "Run the Cortex test suite using cargo test", params,
Arc::new(|_args| {
let manifest = std::path::Path::new(&*CORTEX_ROOT).join("Cargo.toml");
let output = std::process::Command::new("cargo")
.args(["test", "--manifest-path"])
.arg(&manifest)
.output()
.map_err(|e| format!("Failed to run tests: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let mut out = String::new();
for line in stdout.lines().chain(stderr.lines()) {
if line.contains("test result") || line.contains("running") || line.contains("FAILED") || line.contains("error") {
out.push_str(line); out.push('\n');
}
}
if out.is_empty() { out = stdout; }
Ok(format!("Test results:\n{}", out))
}),
)
}