Skip to main content

polyhook_core/
tools.rs

1use crate::types::CallerKind;
2
3/// Normalize a vendor-specific tool name to the canonical polyhook tool name.
4///
5/// The lookup is case-insensitive. If no mapping exists the original name is
6/// returned unchanged.
7pub fn normalize_tool(vendor: &str, caller: &CallerKind) -> String {
8    let lower = vendor.to_lowercase();
9
10    let canonical = match caller {
11        CallerKind::ClaudeCode => normalize_claude_code(&lower),
12        CallerKind::Cursor => normalize_cursor(&lower),
13        CallerKind::Windsurf => normalize_windsurf(&lower),
14        CallerKind::Cline => normalize_cline(&lower),
15        CallerKind::Amp => normalize_amp(&lower),
16        CallerKind::GeminiCli => normalize_gemini_cli(&lower),
17        CallerKind::Hermes => normalize_hermes(&lower),
18        CallerKind::Pi | CallerKind::Codex => normalize_claude_code(&lower),
19        CallerKind::Unknown => None,
20    };
21
22    canonical
23        .map(str::to_owned)
24        .unwrap_or_else(|| vendor.to_owned())
25}
26
27fn normalize_claude_code(lower: &str) -> Option<&'static str> {
28    match lower {
29        "bash" => Some("bash"),
30        "read" => Some("read_file"),
31        "write" => Some("write_file"),
32        "edit" => Some("edit_file"),
33        "ls" => Some("list_dir"),
34        "grep" => Some("grep"),
35        "glob" => Some("glob"),
36        "websearch" => Some("web_search"),
37        "webfetch" => Some("web_fetch"),
38        "task" => Some("spawn_agent"),
39        "mcp__ide__getdiagnostics" => Some("diagnostics"),
40        _ => None,
41    }
42}
43
44fn normalize_cursor(lower: &str) -> Option<&'static str> {
45    match lower {
46        "run_terminal_cmd" => Some("bash"),
47        "read_file" => Some("read_file"),
48        "edit_file" => Some("write_file"),
49        "apply_edit" => Some("edit_file"),
50        "list_dir" => Some("list_dir"),
51        "grep_search" => Some("grep"),
52        "file_search" => Some("glob"),
53        "web_search" => Some("web_search"),
54        "fetch_url" => Some("web_fetch"),
55        "spawn_agent" => Some("spawn_agent"),
56        "get_diagnostics" => Some("diagnostics"),
57        "move_file" => Some("move_file"),
58        "delete_file" => Some("delete_file"),
59        "create_dir" => Some("create_dir"),
60        _ => None,
61    }
62}
63
64fn normalize_windsurf(lower: &str) -> Option<&'static str> {
65    match lower {
66        "run_command" => Some("bash"),
67        "read_file" => Some("read_file"),
68        "write_file" => Some("write_file"),
69        "edit_file" => Some("edit_file"),
70        "list_directory" => Some("list_dir"),
71        "search_files" => Some("grep"),
72        "find_files" => Some("glob"),
73        "search_web" => Some("web_search"),
74        "fetch_page" => Some("web_fetch"),
75        "spawn_agent" => Some("spawn_agent"),
76        "get_diagnostics" => Some("diagnostics"),
77        "move_file" => Some("move_file"),
78        "delete_file" => Some("delete_file"),
79        "create_directory" => Some("create_dir"),
80        _ => None,
81    }
82}
83
84fn normalize_cline(lower: &str) -> Option<&'static str> {
85    match lower {
86        "execute_command" => Some("bash"),
87        "read_file" => Some("read_file"),
88        "write_to_file" => Some("write_file"),
89        "apply_diff" => Some("edit_file"),
90        "list_files" => Some("list_dir"),
91        "search_files" => Some("grep"),
92        "search" => Some("web_search"),
93        "fetch" => Some("web_fetch"),
94        "rename_file" => Some("move_file"),
95        "delete_file" => Some("delete_file"),
96        "create_directory" => Some("create_dir"),
97        "get_diagnostics" => Some("diagnostics"),
98        _ => None,
99    }
100}
101
102fn normalize_amp(lower: &str) -> Option<&'static str> {
103    match lower {
104        "shell" => Some("bash"),
105        "file.read" => Some("read_file"),
106        "file.write" => Some("write_file"),
107        "file.edit" => Some("edit_file"),
108        "fs.list" => Some("list_dir"),
109        "search.grep" => Some("grep"),
110        "search.glob" => Some("glob"),
111        "web.search" => Some("web_search"),
112        "web.fetch" => Some("web_fetch"),
113        "agent.spawn" => Some("spawn_agent"),
114        "lsp.diagnostics" => Some("diagnostics"),
115        "fs.move" => Some("move_file"),
116        "fs.delete" => Some("delete_file"),
117        "fs.mkdir" => Some("create_dir"),
118        _ => None,
119    }
120}
121
122fn normalize_gemini_cli(lower: &str) -> Option<&'static str> {
123    match lower {
124        "run_shell_command" => Some("bash"),
125        "read_file" => Some("read_file"),
126        "write_file" => Some("write_file"),
127        "replace" => Some("edit_file"),
128        "list_directory" => Some("list_dir"),
129        "glob" => Some("glob"),
130        "grep" => Some("grep"),
131        "google_web_search" => Some("web_search"),
132        "fetch" => Some("web_fetch"),
133        "move_file" => Some("move_file"),
134        "delete_file" => Some("delete_file"),
135        "make_directory" => Some("create_dir"),
136        _ => None,
137    }
138}
139
140fn normalize_hermes(lower: &str) -> Option<&'static str> {
141    match lower {
142        "terminal" => Some("bash"),
143        "read_file" => Some("read_file"),
144        "write_file" => Some("write_file"),
145        "patch" => Some("edit_file"),
146        "search_files" => Some("grep"),
147        "delegate_task" => Some("spawn_agent"),
148        "browser_back" | "browser_click" | "browser_console" | "browser_get_images"
149        | "browser_navigate" | "browser_press" | "browser_scroll" | "browser_snapshot"
150        | "browser_type" | "browser_vision" => Some("browser"),
151        _ => None,
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Tests
157// ---------------------------------------------------------------------------
158
159#[cfg(test)]
160#[path = "tools_tests.rs"]
161mod tests;