use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::agent::Agent;
use crate::ast;
use crate::lsp::LspProvider;
use crate::tools::RecoverableError;
pub(crate) struct LspTimer {
start: std::time::Instant,
}
impl LspTimer {
pub(crate) fn start() -> Self {
Self {
start: std::time::Instant::now(),
}
}
pub(crate) async fn record(self, lsp: &dyn LspProvider, lang: &str, root: &Path) {
lsp.record_first_response(lang, root, self.start.elapsed().as_millis() as i64)
.await;
}
}
pub(crate) fn is_glob(path: &str) -> bool {
path.contains(['*', '?', '['])
}
pub(crate) async fn resolve_read_path(
agent: &Agent,
relative_path: &str,
) -> anyhow::Result<PathBuf> {
if relative_path == "." || relative_path.is_empty() {
return agent.require_project_root().await;
}
let project_root = agent.project_root().await;
let security = agent.security_config().await;
let full = crate::util::path_security::validate_read_path(
relative_path,
project_root.as_deref(),
&security,
)?;
if !full.exists() {
return Err(RecoverableError::with_hint(
format!("path not found: {}", full.display()),
"Use tree to explore the directory structure, \
or symbols(path) to list symbols in a file or directory.",
)
.into());
}
Ok(full)
}
pub(crate) async fn resolve_write_path(
agent: &Agent,
relative_path: &str,
) -> anyhow::Result<PathBuf> {
let root = agent.require_project_root().await?;
let security = agent.security_config().await;
let session_roots = agent.session_write_roots_snapshot().await;
crate::util::path_security::validate_write_path(relative_path, &root, &security, &session_roots)
}
pub(crate) async fn resolve_library_roots(
scope: &crate::library::scope::Scope,
agent: &crate::agent::Agent,
) -> anyhow::Result<Vec<(String, PathBuf)>> {
let registry = match agent.library_registry().await {
Some(r) => r,
None => return Ok(vec![]),
};
let matched: Vec<&crate::library::registry::LibraryEntry> = registry
.all()
.iter()
.filter(|entry| scope.includes_library(&entry.name))
.collect();
if let crate::library::scope::Scope::Library(_) = scope {
let unavailable: Vec<&str> = matched
.iter()
.filter(|e| !e.source_available)
.map(|e| e.name.as_str())
.collect();
if !unavailable.is_empty() {
let names = unavailable.join(", ");
return Err(RecoverableError::with_hint(
format!(
"Library source code is not available locally for: {}",
names,
),
"To browse library source, download it using the project's build tool \
(e.g. ./gradlew dependencies, mvn dependency:sources), then call \
library(action='register', path=\"/path/to/source\", name, language) and retry.",
)
.into());
}
}
Ok(matched
.iter()
.filter(|entry| entry.source_available)
.map(|entry| (entry.name.clone(), entry.path.clone()))
.collect())
}
pub(crate) fn format_library_path(lib_name: &str, lib_root: &Path, file_path: &Path) -> String {
file_path
.strip_prefix(lib_root)
.map(|rel| format!("lib:{}/{}", lib_name, rel.display()))
.unwrap_or_else(|_| file_path.display().to_string())
}
pub(crate) fn classify_reference_path(
path: &Path,
project_root: &Path,
library_roots: &[(String, PathBuf)],
) -> (String, String) {
if path.starts_with(project_root) {
let rel = path.strip_prefix(project_root).unwrap_or(path);
("project".to_string(), rel.display().to_string())
} else if let Some((name, lib_root)) = library_roots.iter().find(|(_, r)| path.starts_with(r)) {
(
"lib:".to_string() + name,
format_library_path(name, lib_root, path),
)
} else {
("external".to_string(), path.display().to_string())
}
}
pub(crate) async fn resolve_glob(
agent: &Agent,
path_or_glob: &str,
) -> anyhow::Result<Vec<PathBuf>> {
let root = agent.require_project_root().await?;
if !is_glob(path_or_glob) {
let full = resolve_read_path(agent, path_or_glob).await?;
return Ok(vec![full]);
}
let glob = globset::GlobBuilder::new(path_or_glob)
.literal_separator(false)
.build()
.map_err(|e| {
RecoverableError::with_hint(
format!("invalid glob pattern '{}': {}", path_or_glob, e),
"Check glob syntax: use * for any segment, ** for recursive, ? for single char.",
)
})?;
let matcher = glob.compile_matcher();
let mut matches = vec![];
let walker = ignore::WalkBuilder::new(&root)
.hidden(true)
.git_ignore(true)
.build();
for entry in walker.flatten() {
if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
continue;
}
if let Ok(rel) = entry.path().strip_prefix(&root) {
if matcher.is_match(rel) {
matches.push(entry.path().to_path_buf());
}
}
}
if matches.is_empty() {
return Err(RecoverableError::with_hint(
format!("no files matched glob pattern: {}", path_or_glob),
"Try a broader pattern or use tree to verify the path exists.",
)
.into());
}
matches.sort();
Ok(matches)
}
pub(crate) fn get_path_param(input: &Value, required: bool) -> anyhow::Result<Option<&str>> {
match input["path"]
.as_str()
.or_else(|| input["relative_path"].as_str())
.or_else(|| input["file"].as_str())
{
Some(p) => Ok(Some(p)),
None if required => Err(RecoverableError::with_hint(
"missing 'path' parameter",
"Add the required 'path' parameter to the tool call.",
)
.into()),
None => Ok(None),
}
}
pub(crate) fn require_path_param(input: &Value) -> anyhow::Result<&str> {
input["path"]
.as_str()
.or_else(|| input["relative_path"].as_str())
.or_else(|| input["file"].as_str())
.ok_or_else(|| {
RecoverableError::with_hint(
"missing 'path' parameter",
"Add the required 'path' parameter to the tool call.",
)
.into()
})
}
pub(crate) fn guard_not_markdown(path: &Path) -> anyhow::Result<()> {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext.eq_ignore_ascii_case("md") || ext.eq_ignore_ascii_case("markdown") {
return Err(RecoverableError::with_hint(
"symbol tools do not support markdown files",
"Use edit_markdown(path, heading, action, content) for section-level edits, \
or edit_file for literal string replacements in markdown.",
)
.into());
}
}
Ok(())
}
pub(crate) async fn get_lsp_client(
agent: &Agent,
lsp: &dyn LspProvider,
path: &Path,
) -> anyhow::Result<(std::sync::Arc<dyn crate::lsp::LspClientOps>, String)> {
let lang = ast::detect_language(path).ok_or_else(|| {
RecoverableError::with_hint(
format!("unsupported file type: {:?}", path),
"LSP symbol analysis supports: rust, python, typescript, tsx, \
javascript, jsx, go, java, kotlin, c, cpp, csharp, ruby. \
Use list_functions for a tree-sitter fallback on other file types.",
)
})?;
let root = agent.require_project_root().await?;
let mux_override = agent.lsp_mux_override(lang).await;
let client = lsp.get_or_start(lang, &root, mux_override).await?;
let language_id = crate::lsp::servers::lsp_language_id(lang);
Ok((client, language_id.to_string()))
}
fn is_mux_disconnect(e: &anyhow::Error) -> bool {
let s = e.to_string();
s.contains("Mux connection lost") || s.contains("Failed to spawn mux process")
}
pub(crate) async fn retry_on_mux_disconnect<F, Fut, T>(
agent: &Agent,
lsp: &dyn LspProvider,
path: &Path,
initial_client: std::sync::Arc<dyn crate::lsp::LspClientOps>,
initial_lang: String,
op: F,
) -> anyhow::Result<T>
where
F: Fn(std::sync::Arc<dyn crate::lsp::LspClientOps>, String) -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
match op(initial_client, initial_lang).await {
Err(e) if is_mux_disconnect(&e) => {
tracing::warn!("LSP mux disconnect, retrying once: {}", e);
let (client, lang) = get_lsp_client(agent, lsp, path).await?;
op(client, lang).await
}
other => other,
}
}
pub(crate) fn uri_to_path(uri: &str) -> Option<PathBuf> {
crate::util::file_address::FileAddress::from_uri_str(uri)
.map(crate::util::file_address::FileAddress::into_path)
}
pub(crate) fn path_in_excluded_dir(path: &std::path::Path) -> bool {
const EXCLUDED: &[&str] = &[
"target",
"node_modules",
".git",
"dist",
"build",
"out",
"__pycache__",
".mypy_cache",
".pytest_cache",
"vendor",
".gradle",
".idea",
".vscode",
];
path.components().any(|c| {
if let std::path::Component::Normal(name) = c {
EXCLUDED.iter().any(|&ex| name == std::ffi::OsStr::new(ex))
} else {
false
}
})
}
pub(crate) async fn tag_external_path(
path: &std::path::Path,
project_root: &std::path::Path,
agent: &crate::agent::Agent,
) -> String {
if path.starts_with(project_root) {
return "project".to_string();
}
if let Some(registry) = agent.library_registry().await {
if let Some(entry) = registry.is_library_path(path) {
return format!("lib:{}", entry.name);
}
}
if let Some(discovered) = crate::library::discovery::discover_library_root(path) {
let name = discovered.name.clone();
let mut inner = agent.inner.write().await;
if let Some(project) = inner.active_project_mut() {
project.library_registry.register(
discovered.name,
discovered.path,
discovered.language,
crate::library::registry::DiscoveryMethod::LspFollowThrough,
true,
);
let registry_path = project.root.join(".codescout").join("libraries.json");
let _ = project.library_registry.save(®istry_path);
}
format!("lib:{}", name)
} else {
"external".to_string()
}
}