From eb5cc873eaba045a19bdb2589b8ea3b4d2c40765 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 7 Feb 2026 13:12:47 +0900 Subject: [PATCH] fix: trim whitespace from tool names to prevent invalid tool calls Some models (e.g. kimi-k2.5) return tool names with leading spaces like ' delegate_task', causing tool matching to fail. Add .trim() in transformToolName() and defensive trim in claude-code-hooks. Fixes #1568 --- src/hooks/claude-code-hooks/index.ts | 2 +- src/shared/tool-name.test.ts | 155 +++++++++++++++++++++++++++ src/shared/tool-name.ts | 9 +- 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 src/shared/tool-name.test.ts diff --git a/src/hooks/claude-code-hooks/index.ts b/src/hooks/claude-code-hooks/index.ts index f4b2b1149..9555ea797 100644 --- a/src/hooks/claude-code-hooks/index.ts +++ b/src/hooks/claude-code-hooks/index.ts @@ -175,7 +175,7 @@ export function createClaudeCodeHooksHook( input: { tool: string; sessionID: string; callID: string }, output: { args: Record } ): Promise => { - if (input.tool === "todowrite" && typeof output.args.todos === "string") { + if (input.tool.trim() === "todowrite" && typeof output.args.todos === "string") { let parsed: unknown try { parsed = JSON.parse(output.args.todos) diff --git a/src/shared/tool-name.test.ts b/src/shared/tool-name.test.ts new file mode 100644 index 000000000..3cafe75ab --- /dev/null +++ b/src/shared/tool-name.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect } from "bun:test" +import { transformToolName } from "./tool-name" + +describe("transformToolName", () => { + describe("whitespace trimming", () => { + it("trims leading whitespace from tool name", () => { + // given + const toolName = " delegate_task" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("DelegateTask") + }) + + it("trims trailing whitespace from tool name", () => { + // given + const toolName = "delegate_task " + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("DelegateTask") + }) + + it("trims both leading and trailing whitespace", () => { + // given + const toolName = " delegate_task " + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("DelegateTask") + }) + + it("applies special mapping after trimming whitespace", () => { + // given + const toolName = " webfetch" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("WebFetch") + }) + + it("handles simple case with leading and trailing spaces", () => { + // given + const toolName = " read " + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("Read") + }) + }) + + describe("special tool mappings", () => { + it("maps webfetch to WebFetch", () => { + // given + const toolName = "webfetch" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("WebFetch") + }) + + it("maps websearch to WebSearch", () => { + // given + const toolName = "websearch" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("WebSearch") + }) + + it("maps todoread to TodoRead", () => { + // given + const toolName = "todoread" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("TodoRead") + }) + + it("maps todowrite to TodoWrite", () => { + // given + const toolName = "todowrite" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("TodoWrite") + }) + }) + + describe("kebab-case and snake_case conversion", () => { + it("converts snake_case to PascalCase", () => { + // given + const toolName = "delegate_task" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("DelegateTask") + }) + + it("converts kebab-case to PascalCase", () => { + // given + const toolName = "call-omo-agent" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("CallOmoAgent") + }) + }) + + describe("simple capitalization", () => { + it("capitalizes simple single-word tool names", () => { + // given + const toolName = "read" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("Read") + }) + + it("preserves capitalization of already capitalized names", () => { + // given + const toolName = "Write" + + // when + const result = transformToolName(toolName) + + // then + expect(result).toBe("Write") + }) + }) +}) diff --git a/src/shared/tool-name.ts b/src/shared/tool-name.ts index d94ede6dc..b93b07021 100644 --- a/src/shared/tool-name.ts +++ b/src/shared/tool-name.ts @@ -13,14 +13,15 @@ function toPascalCase(str: string): string { } export function transformToolName(toolName: string): string { - const lower = toolName.toLowerCase() + const trimmed = toolName.trim() + const lower = trimmed.toLowerCase() if (lower in SPECIAL_TOOL_MAPPINGS) { return SPECIAL_TOOL_MAPPINGS[lower] } - if (toolName.includes("-") || toolName.includes("_")) { - return toPascalCase(toolName) + if (trimmed.includes("-") || trimmed.includes("_")) { + return toPascalCase(trimmed) } - return toolName.charAt(0).toUpperCase() + toolName.slice(1) + return trimmed.charAt(0).toUpperCase() + trimmed.slice(1) }