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
This commit is contained in:
parent
1c0b41aa65
commit
eb5cc873ea
@ -175,7 +175,7 @@ export function createClaudeCodeHooksHook(
|
|||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
output: { args: Record<string, unknown> }
|
output: { args: Record<string, unknown> }
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (input.tool === "todowrite" && typeof output.args.todos === "string") {
|
if (input.tool.trim() === "todowrite" && typeof output.args.todos === "string") {
|
||||||
let parsed: unknown
|
let parsed: unknown
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(output.args.todos)
|
parsed = JSON.parse(output.args.todos)
|
||||||
|
|||||||
155
src/shared/tool-name.test.ts
Normal file
155
src/shared/tool-name.test.ts
Normal file
@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -13,14 +13,15 @@ function toPascalCase(str: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transformToolName(toolName: 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) {
|
if (lower in SPECIAL_TOOL_MAPPINGS) {
|
||||||
return SPECIAL_TOOL_MAPPINGS[lower]
|
return SPECIAL_TOOL_MAPPINGS[lower]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolName.includes("-") || toolName.includes("_")) {
|
if (trimmed.includes("-") || trimmed.includes("_")) {
|
||||||
return toPascalCase(toolName)
|
return toPascalCase(trimmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return toolName.charAt(0).toUpperCase() + toolName.slice(1)
|
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user