use super::*;
pub fn normalize_paths(calls: &mut [ToolCall], cwd: &str) {
const PATH_KEYS: &[&str] = &["file_path", "filePath", "path", "file", "workdir"];
let cwd_slash = if cwd.ends_with('/') {
cwd.to_string()
} else {
format!("{cwd}/")
};
for call in calls.iter_mut() {
let Ok(mut args) = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(
&call.function.arguments,
) else {
continue;
};
let mut changed = false;
if call.function.name.eq_ignore_ascii_case("bash")
&& let Some(serde_json::Value::String(command)) = args.get("command")
&& let Some(repaired) = repair_punctuation_drifted_cwd_in_command(command, cwd)
{
args.insert("command".to_string(), serde_json::Value::String(repaired));
changed = true;
}
for key in PATH_KEYS {
if let Some(serde_json::Value::String(path)) = args.get(*key) {
let trimmed = path.trim();
let mut sanitized: &str = trimmed;
if let Some(rest) = sanitized.strip_prefix('=') {
sanitized = rest.trim_start();
}
sanitized = sanitized.trim_end_matches([',', ' ', '\t']);
if sanitized.len() >= 2 && sanitized.starts_with('"') && sanitized.ends_with('"') {
sanitized = &sanitized[1..sanitized.len() - 1];
}
if sanitized != path.as_str() {
args.insert(
key.to_string(),
serde_json::Value::String(sanitized.to_string()),
);
changed = true;
}
let Some(serde_json::Value::String(path)) = args.get(*key) else {
continue;
};
if *key == "workdir" && punctuation_equivalent_basename(path, cwd) {
if path != cwd {
args.insert(key.to_string(), serde_json::Value::String(cwd.to_string()));
changed = true;
}
continue;
}
if !path.starts_with('/') {
continue;
}
if !path.starts_with(&cwd_slash) {
continue;
}
let new_path = path[cwd_slash.len()..].to_string();
if new_path != *path && !new_path.is_empty() {
args.insert(key.to_string(), serde_json::Value::String(new_path));
changed = true;
}
}
}
if changed && let Ok(new_args) = serde_json::to_string(&serde_json::Value::Object(args)) {
call.function.arguments = new_args;
}
}
}
fn fold_path_component(name: &str) -> String {
name.chars()
.filter(|c| c.is_ascii_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect()
}
fn punctuation_equivalent_basename(candidate: &str, cwd: &str) -> bool {
let candidate = std::path::Path::new(candidate);
let cwd = std::path::Path::new(cwd);
if candidate.parent() != cwd.parent() {
return false;
}
match (candidate.file_name(), cwd.file_name()) {
(Some(candidate), Some(cwd)) => {
fold_path_component(&candidate.to_string_lossy())
== fold_path_component(&cwd.to_string_lossy())
}
_ => false,
}
}
fn repair_punctuation_drifted_cwd_in_command(command: &str, cwd: &str) -> Option<String> {
let cwd = std::path::Path::new(cwd);
let parent = cwd.parent()?.to_str()?;
let basename = cwd.file_name()?.to_str()?;
let parent_prefix = if parent == "/" {
"/".to_string()
} else {
format!("{parent}/")
};
let canonical_fold = fold_path_component(basename);
let mut repaired = String::with_capacity(command.len());
let mut cursor = 0;
let mut changed = false;
while let Some(offset) = command[cursor..].find(&parent_prefix) {
let component_start = cursor + offset + parent_prefix.len();
let component_end = command[component_start..]
.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')))
.map_or(command.len(), |end| component_start + end);
let candidate = &command[component_start..component_end];
repaired.push_str(&command[cursor..component_start]);
if candidate != basename
&& !candidate.is_empty()
&& fold_path_component(candidate) == canonical_fold
{
repaired.push_str(basename);
changed = true;
} else {
repaired.push_str(candidate);
}
cursor = component_end;
}
if !changed {
return None;
}
repaired.push_str(&command[cursor..]);
Some(repaired)
}