pub struct NormalizedTool {
pub name: String,
pub status: ToolProjectionStatus,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ToolProjectionStatus {
Canonical,
Normalized,
Unknown,
}
pub fn normalize_tool_for_harness(raw: &str, target_harness: &str) -> NormalizedTool {
let trimmed = raw.trim();
if trimmed.is_empty() {
return NormalizedTool {
name: String::new(),
status: ToolProjectionStatus::Unknown,
};
}
let (head, payload) = match trimmed.find('(') {
Some(index) => (&trimmed[..index], &trimmed[index..]),
None => (trimmed, ""),
};
let canonical = canonical_tool_name(head, target_harness);
match canonical {
Some(value) => {
let status = if head == value {
ToolProjectionStatus::Canonical
} else {
ToolProjectionStatus::Normalized
};
NormalizedTool {
name: format!("{value}{payload}"),
status,
}
}
None => NormalizedTool {
name: trimmed.to_string(),
status: ToolProjectionStatus::Unknown,
},
}
}
pub fn is_first_class_harness(harness: &str) -> bool {
matches!(
harness.trim().to_ascii_lowercase().as_str(),
"claude" | "codex" | "opencode"
)
}
fn canonical_tool_name(head: &str, target_harness: &str) -> Option<&'static str> {
let key = head.trim().to_ascii_lowercase();
if key.is_empty() {
return None;
}
match target_harness.trim().to_ascii_lowercase().as_str() {
"claude" => canonical_claude_tool(&key),
"codex" => canonical_codex_tool(&key),
"opencode" => canonical_opencode_tool(&key),
"cursor" | "pi" => canonical_generic_tool(&key),
_ => canonical_generic_tool(&key),
}
}
fn canonical_claude_tool(key: &str) -> Option<&'static str> {
match key {
"bash" | "shell" | "terminal" => Some("Bash"),
"read" | "cat" | "view" => Some("Read"),
"write" => Some("Write"),
"edit" | "sed" => Some("Edit"),
"agent" | "subagent" => Some("Agent"),
"glob" | "find" => Some("Glob"),
"grep" | "search" | "rg" => Some("Grep"),
"notebook" | "jupyter" => Some("Notebook"),
"task" | "task_tool" | "taskcreate" | "task_create" | "taskget" | "task_get"
| "tasklist" | "task_list" | "taskoutput" | "task_output" | "taskstop" | "task_stop"
| "taskupdate" | "task_update" => Some("Task"),
"web_search" | "websearch" => Some("WebSearch"),
"web_fetch" | "webfetch" => Some("WebFetch"),
"todo_read" | "todoread" => Some("TodoRead"),
"todo_write" | "todowrite" => Some("TodoWrite"),
"cron" | "croncreate" | "cron_create" | "crondelete" | "cron_delete" | "cronlist"
| "cron_list" => Some("Cron"),
"ask_user" | "askuser" | "askuserquestion" | "ask_user_question" => Some("AskUser"),
"notifications" | "pushnotification" | "push_notification" => Some("Notifications"),
"plan_mode" | "planmode" | "enterplanmode" | "enter_plan_mode" | "exitplanmode"
| "exit_plan_mode" => Some("PlanMode"),
"worktree" | "enterworktree" | "enter_worktree" | "exitworktree" | "exit_worktree" => {
Some("Worktree")
}
"lsp" => Some("LSP"),
"monitor" => Some("Monitor"),
"send_user_file" | "senduserfile" => Some("SendUserFile"),
"schedule_wakeup" | "schedulewakeup" => Some("ScheduleWakeup"),
"remote_trigger" | "remotetrigger" => Some("RemoteTrigger"),
"tool_search" | "toolsearch" => Some("ToolSearch"),
_ => None,
}
}
fn canonical_codex_tool(key: &str) -> Option<&'static str> {
match key {
"shell" | "bash" | "terminal" => Some("shell"),
"file_read" | "read" | "cat" => Some("file_read"),
"file_write" | "write" | "edit" => Some("file_write"),
"agent" | "subagent" => Some("agent"),
_ => None,
}
}
fn canonical_opencode_tool(key: &str) -> Option<&'static str> {
match key {
"bash" | "shell" | "terminal" => Some("bash"),
"read" | "cat" => Some("read"),
"write" => Some("write"),
"edit" => Some("edit"),
"agent" | "subagent" => Some("agent"),
"browser" | "web_search" | "websearch" => Some("browser"),
"fetch" | "web_fetch" | "webfetch" => Some("fetch"),
_ => None,
}
}
fn canonical_generic_tool(key: &str) -> Option<&'static str> {
match key {
"bash" => Some("Bash"),
"read" => Some("Read"),
"write" => Some("Write"),
"edit" => Some("Edit"),
"agent" => Some("Agent"),
_ => None,
}
}