const EXACT_TOOL_NAME_ALIASES: Record = { shell_command: "bash", "functions.shell_command": "bash", exec_command: "exec_command", "functions.exec_command": "exec_command", "functions.read": "read", "functions.edit": "edit", "functions.write": "write", "functions.apply_patch": "apply_patch", change: "edit", "functions.change": "edit", changes: "edit", write_stdin: "bash", read_file: "read", read_text_file: "read", readfile: "read", "read file": "read", edit_file: "edit", update_file: "edit", write_file: "write", mcp__acp__read: "read", mcp__acp__edit: "edit", mcp__acp__write: "write", todowrite: "todowrite", todo_write: "todowrite", task_update: "taskupdate", task_create: "taskcreate", task_list: "tasklist", enter_plan_mode: "enterplanmode", exit_plan_mode: "exitplanmode", web_fetch: "webfetch", web_search: "websearch", context7_query_docs: "context7_query-docs", context7_resolve_library_id: "context7_resolve-library-id", agent: "agent", // Gemini CLI searchtext: "grep", search_text: "grep", writefile: "write", editfile: "edit", // Cline attempt_completion: "attempt_completion", ask_followup_question: "question", write_to_file: "write", replace_in_file: "edit", execute_command: "bash", list_files: "glob", search_files: "grep", list_code_definition_names: "grep", browser_action: "webfetch", use_mcp_tool: "tool", // Codex update_plan: "task", request_user_input: "question", // OpenCode delegate_task: "task", call_omo_agent: "agent", ast_grep_search: "grep", ast_grep_replace: "edit", background_task: "task", background_cancel: "task", background_output: "task", slashcommand: "skill", question: "question", ask_user_question: "question", askuserquestion: "question", lsp_diagnostics: "lsp", lsp_document_symbols: "lsp", lsp_goto_definition: "lsp", lsp_servers: "lsp", execute: "bash", search: "grep", fetch: "webfetch", think: "task", switch_mode: "switch_mode", other: "tool", } function canonicalizeToolName(input: string): string { return input .trim() .toLowerCase() .replace(/[().]/g, "_") .replace(/[\s-]+/g, "_") } function inferFromFreeformName(input: string): string | null { const normalized = input.trim().toLowerCase() if (!normalized) return null if ( /\b(?:shell|bash|exec(?:ute)?)\s*[_-]?(?:command|cmd)?\b/.test(normalized) ) return "bash" if (/apply\s*[_-]?patch/.test(normalized)) return "apply_patch" if (/^change(?:$|[\s:/_-])/.test(normalized)) return "edit" if (/^read(?:$|[\s:/-])/.test(normalized)) return "read" if (/^edit(?:$|[\s:/-])/.test(normalized)) return "edit" if (/^write(?:$|[\s:/-])/.test(normalized)) return "write" if (/^grep(?:\b|[_\s:-])/.test(normalized)) return "grep" if (/^glob(?:\b|[_\s:-])/.test(normalized)) return "glob" if (/^webfetch(?:\b|[_\s:-])/.test(normalized)) return "webfetch" if (/^websearch(?:\b|[_\s:-])/.test(normalized)) return "websearch" if (/\bweb[_\s-]?search\b/.test(normalized)) return "websearch" if (/\bgrep\b/.test(normalized)) return "grep" if (/\bagent\b/.test(normalized)) return "agent" if (/\blsp\b/.test(normalized)) return "lsp" if (/^todowrite(?:\b|[_\s:-])/.test(normalized)) return "todowrite" if (/^taskupdate(?:\b|[_\s:-])/.test(normalized)) return "taskupdate" if (/^taskcreate(?:\b|[_\s:-])/.test(normalized)) return "taskcreate" if (/^tasklist(?:\b|[_\s:-])/.test(normalized)) return "tasklist" if (/^task(?:\b|[_\s:-])/.test(normalized)) return "task" if (/\bask\s*(?:user)?\s*question\b/.test(normalized)) return "question" return null } function extractToolNameFromLiveCallTitle(input: string): string | null { const match = input.match( /^[::'"`“”‘’\s]*([a-z0-9_.-]+)(?:\s*[::])?\s*call[\w-]*['"`“”‘’\s]*$/i ) return match?.[1] ?? null } function tryParseInputObject(rawInput: string | null | undefined) { if (!rawInput) return null try { const parsed = JSON.parse(rawInput) if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return null } return parsed as Record } catch { return null } } function hasAnyKey(obj: Record, keys: string[]): boolean { return keys.some( (key) => key in obj && obj[key] !== null && obj[key] !== undefined ) } function inferFromInput( rawInput: string | null | undefined, kind: string | null | undefined, title: string | null | undefined ): string | null { if (!rawInput) return null const normalizedKind = normalizeToolName(kind ?? "") const normalizedTitle = normalizeToolName(title ?? "") if (rawInput.includes("*** Begin Patch")) { return "apply_patch" } const trimmed = rawInput.trim() if ( trimmed.length > 0 && !trimmed.startsWith("{") && !trimmed.startsWith("[") && (normalizedKind === "bash" || normalizedKind === "exec_command" || normalizedKind === "tool" || normalizedTitle === "bash" || normalizedTitle === "exec_command") ) { return "bash" } const parsed = tryParseInputObject(rawInput) if (!parsed) return null if ( hasAnyKey(parsed, [ "command", "cmd", "script", "args", "argv", "command_args", ]) ) return "bash" if (hasAnyKey(parsed, ["old_string", "new_string", "replace_all"])) return "edit" if (hasAnyKey(parsed, ["changes"])) return "edit" if (hasAnyKey(parsed, ["todos"])) return "todowrite" if (hasAnyKey(parsed, ["query"])) return "websearch" if (hasAnyKey(parsed, ["url"])) return "webfetch" const hasPattern = hasAnyKey(parsed, ["pattern"]) const hasGlob = hasAnyKey(parsed, ["glob"]) if (hasPattern) return hasGlob ? "glob" : "grep" if (hasGlob) return "glob" if (hasAnyKey(parsed, ["question"])) return "question" if (hasAnyKey(parsed, ["subagent_type", "taskId", "task_id", "subject"])) { return "task" } const hasPath = hasAnyKey(parsed, ["file_path", "notebook_path", "path"]) if (hasPath) { if ( normalizedKind === "read" || normalizedKind === "edit" || normalizedKind === "write" || normalizedKind === "delete" || normalizedKind === "move" ) { return normalizedKind } if ( normalizedTitle === "read" || normalizedTitle === "edit" || normalizedTitle === "write" ) { return normalizedTitle } if ( hasAnyKey(parsed, ["content", "new_source", "cell_type", "edit_mode"]) ) { return "write" } return "read" } return null } export function normalizeToolName(toolName: string): string { const trimmed = toolName .trim() .replace(/^[::'"`“”‘’\s]+/, "") .replace(/['"`“”‘’\s]+$/, "") if (!trimmed) return "tool" const exact = EXACT_TOOL_NAME_ALIASES[trimmed.toLowerCase()] if (exact) return exact const canonical = canonicalizeToolName(trimmed) const alias = EXACT_TOOL_NAME_ALIASES[canonical] if (alias) return alias const freeform = inferFromFreeformName(trimmed) if (freeform) return freeform const liveTitleToolName = extractToolNameFromLiveCallTitle(trimmed) if (liveTitleToolName) { const fromLiveTitle = normalizeToolName(liveTitleToolName) if (fromLiveTitle !== "tool") return fromLiveTitle } return trimmed } export function inferLiveToolName(params: { title?: string | null kind?: string | null rawInput?: string | null }): string { const byInput = inferFromInput(params.rawInput, params.kind, params.title) if (byInput) return byInput const byTitle = normalizeToolName(params.title ?? "") if (byTitle !== "tool") return byTitle const byKind = normalizeToolName(params.kind ?? "") if (byKind !== "tool") return byKind return "tool" }