Reviewed PR description, comments, commits, and changed files.
UnderstandIdentified and read linked issues.
UnderstandAnalyzed PR diff and relevant code files.
UnderstandApplied fixes and merged changes to main branch.
EditYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/807/changes
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
The user wants me to review a GitHub PR. Let me start by reading the PR details.
>gh pr view 807 --repo badlogic/pi-mono --json title,body,comments,commits,files,author,labels,state,baseRefName,headRefName>gh pr diff 807 --repo badlogic/pi-monoLet me read the current anthropic.ts file to understand the full context of the change.
Let me read the current main branch file and check for linked issues:
Let me check the current state of the CHANGELOG on main to see if there's already an entry, and check for linked issues in the PR.
I see the CHANGELOG on main already has a different entry under [Unreleased]. Let me check if there are any linked issues and check the documentation files:
>gh issue view 812 --repo badlogic/pi-mono --json title,body,comments,labels,state 2>/dev/null || echo "Issue 812 not found or not accessible">ls -la /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/ 2>/dev/null | head -20Now I have a good understanding of the PR. Let me analyze it:
The PR description mentions this is about:
Let me check the diff more carefully and analyze the changes:
Changes in anthropic.ts:
claudeCodeToolNames → piToolNames (clearer naming)ls: "Ls" from pi tool mappings (ls was removed in CC 2.x)ccExtraTools array with CC 2.x builtin tool names that pi doesn't provideccToolLookup Map combining both mappingstoClaudeCodeName to do case-insensitive lookupChangelog:
### Fixed in [Unreleased] sectionThe current main branch CHANGELOG has:
## [Unreleased]
### Fixed
- Fixed orphaned tool results after errored assistant messages causing Codex API errors...The PR adds:
## [Unreleased]
### Fixed
- Fixed Claude Code OAuth errors when user-defined tools collide with CC builtin names by normalizing tool names case-insensitively to CC canonical formThere's a merge conflict waiting to happen - the changelog entry needs to be appended to the existing section, not replace it. The fix itself looks solid: case-insensitive lookup prevents tool name collisions, the deprecated ls mapping is correctly removed for CC 2.x compatibility, and the approach of combining pi's tools with CC's extra tools is clean. However, the CHANGELOG will need both entries combined, and there's no PR attribution in the changelog entry.
Now let me check for any tests related to this functionality and verify the current state of the CHANGELOG:
>cd /Users/badlogic/workspaces/pi-mono && git log --oneline -5>grep -r "toClaudeCodeName\|fromClaudeCodeName\|claudeCodeToolNames" /Users/badlogic/workspaces/pi-mono/packages/ai/test/ 2>/dev/null || echo "No test files found for these functions"I can see main has a recent commit 0f3a0f78 that fixes issue #812. The current CHANGELOG on main reflects that.
Now I have all the information I need for the review.
PR: https://github.com/badlogic/pi-mono/pull/807
Changelog:
### Fixed in [Unreleased]### Fixed entry for issue #812, but the PR replaces it instead of appendingFixed foo ([#807](https://github.com/badlogic/pi-mono/pull/807) by [@joshp123](https://github.com/joshp123))Good:
todowrite, TodoWrite) will no longer trigger OAuth credential errorsccToolLookup.get(name.toLowerCase()) is the right approachls: "Ls" mapping (CC 2.x removed the ls tool)Bad:
### Fixed sectiontoClaudeCodeName / fromClaudeCodeName normalization (existing tests may cover via integration, but explicit unit tests would be valuable for this edge case)Ugly:
Questions or Assumptions:
ccExtraTools list was derived from CC 2.1.11. If Anthropic adds new tool names to Claude Code that collide with user custom tools, this will break again (acknowledged as acceptable risk in PR description)fromClaudeCodeName was not modified and continues to rely on matching against context.tools by lowercase name, which should still work correctly since the user's original tool name is preserved thereChange summary:
claudeCodeToolNames → piToolNames for clarityccExtraTools array with 11 additional CC 2.x builtin tool names not provided by pi: AskUserQuestion, EnterPlanMode, ExitPlanMode, KillShell, NotebookEdit, Skill, Task, TaskOutput, TodoWrite, WebFetch, WebSearchccToolLookup Map for case-insensitive normalizationtoClaudeCodeName to perform case-insensitive lookup: ccToolLookup.get(name.toLowerCase()) ?? namels: "Ls" mappingTests:
todowrite (lowercase) and verify no OAuth credential errors with Claude Max subscriptionDocumentation:
packages/coding-agent/README.md does not require modification (OAuth stealth mode is an internal implementation detail)packages/coding-agent/docs/*.md do not require modificationbut the implemnetation is fucking stupid, no? what we need to do is know all CC tools, if a pi tool matches ignoring case, we convert to the CC tool name which is send to anthropic, and upon receiving shit, we convert to the pi tool name, we just need to extend the list of tools we already have, no?
The user is asking if the implementation is overly complicated. Let me think about this...
Currently the code has:
piToolNames - maps pi tool names to CC names (read → Read, write → Write, etc.)ccExtraTools - extra CC tools that pi doesn't provideccToolLookup - combined Map for case-insensitive lookupThe user's point: we just need to know all CC tool names. If ANY tool (pi-provided OR user-defined) matches a CC tool name (case-insensitive), convert it to the CC canonical form on the way out, and convert back on the way in.
So instead of having two separate concepts (pi tools vs extra CC tools), we just need ONE list of all CC tool names with their canonical casing, and:
toClaudeCodeName(name): if name.toLowerCase() matches any CC tool name (case-insensitive), return the canonical CC namefromClaudeCodeName(name, tools): already works correctly - it finds the tool in context.tools by case-insensitive matchThe current implementation does exactly this! But the way it's structured is confusing:
piToolNames is a Record<string, string> mapping lowercase pi names to CC namesccExtraTools is an array of canonical CC namesccToolLookupA simpler approach would be:
// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", // pi calls this "find" "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
// Pi tool name aliases (pi name → CC name)const piToolAliases: Record<string, string> = { find: "Glob", // pi's "find" tool is CC's "Glob"};
const ccToolLookup = new Map<string, string>();for (const name of claudeCodeTools) { ccToolLookup.set(name.toLowerCase(), name);}for (const [piName, ccName] of Object.entries(piToolAliases)) { ccToolLookup.set(piName.toLowerCase(), ccName);}This consolidates everything into one canonical list of CC tools plus a mapping for pi-specific aliases. The lookup map handles both the standard CC tools and any pi name variations, making the intent clearer and the code more maintainable. d. The current approach does work, but the structure is unnecessarily convoluted. What the user is really asking for is simpler: maintain one canonical list of CC tool names, then handle case-insensitive matching and conversion in both directions. The existing implementation achieves this goal, just through a more roundabout path with the artificial separation between piToolNames and ccExtraTools.
You're right. The implementation is overengineered.
What we actually need:
// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
// Pi aliases (pi uses "find", CC uses "Glob")const piAliases: Record<string, string> = { find: "Glob" };
const ccLookup = new Map([ ...claudeCodeTools.map(t => [t.toLowerCase(), t] as const), ...Object.entries(piAliases).map(([k, v]) => [k.toLowerCase(), v] as const),]);
const toClaudeCodeName = (name: string) => ccLookup.get(name.toLowerCase()) ?? name;The current PR splits this into:
piToolNames (pi tools mapped to CC names)ccExtraTools (CC tools pi doesn't provide)That's backwards. The primary concept is "CC tool names" and the secondary concept is "pi aliases for some of them". The current structure makes it look like there are two different kinds of tools when there's really just one list with one alias.
Functionally it works, but the code structure doesn't match the mental model. Should I suggest this simplification to the contributor, or do you want to just fix it during merge?
no, we don't need aliases, wtf where does that come from? if a pi tool matches a cc tool name ignoring csae, convert to cc tool name casing (camel case), and the reverse.
Oh, the user is right. I was overcomplicating it too.
Looking at the current code:
const piToolNames: Record<string, string> = { read: "Read", write: "Write", edit: "Edit", bash: "Bash", grep: "Grep", find: "Glob", // <-- this is the only "alias"};Wait, find → Glob is an alias because pi's tool is called "find" but CC calls it "Glob". That's a real mapping that's needed.
But the user is saying we don't need aliases... Let me re-read what they're saying.
"if a pi tool matches a cc tool name ignoring case, convert to cc tool name casing"
So if pi has a tool called "read", and CC has a tool called "Read", we convert "read" → "Read".
But what about pi's "find" tool? CC doesn't have a "Find" tool, it has "Glob". So "find" wouldn't match any CC tool name, and wouldn't be converted. That's fine - it's not a CC tool, so no conversion needed.
OH. I see the point now. The user is saying:
But wait, the current code has find: "Glob" - why? Let me check the original code again...
Looking at the original claudeCodeToolNames:
const claudeCodeToolNames: Record<string, string> = { read: "Read", write: "Write", edit: "Edit", bash: "Bash", grep: "Grep", find: "Glob", ls: "Ls",};So pi's "find" tool is intentionally mapped to CC's "Glob" tool. This suggests that pi's "find" functionality is equivalent to CC's "Glob" functionality, and they want to masquerade as the same tool. But if pi has a "find" tool and CC has a "Glob" tool, they're different tools. Why would we need to map them? There must be something about OAuth credentials or tool identity that makes this necessary.
The mapping appears to be about tool NAMES matching CC's hardcoded list. If Anthropic's OAuth system only allows certain tool names when using Claude Code OAuth, then names like "Read", "Write", "Edit", "Bash", "Grep", and "Glob" are allowed, while other names might not be. The find: "Glob" mapping ensures pi's "find" tool gets sent as "Glob" to match what CC expects, since they're functionally similar and we're presenting ourselves as CC.
But the user is suggesting we don't need explicit aliases. Instead, if a pi tool name matches a CC tool name ignoring case, we should convert it to CC's casing (camel case), and vice versa. So "read" becomes "Read" to match CC's tool, "find" stays as "find" since there's no CC equivalent, and "todowrite" becomes "TodoWrite" if CC has that tool. The deliberate find: "Glob" mapping was for stealth mode to look more like CC, but for the OAuth issue itself, simple case normalization might be sufficient.
The approach would be creating a lookup map from CC's tool names in lowercase to their proper casing, then using that to normalize any incoming tool name. This handles the OAuth problem without needing explicit aliases—just matching on the canonical CC tool names.
But I need to reconsider the existing find: "Glob" mapping that was already in the original code. That mapping has been there to maintain stealth mode compatibility, converting pi's "find" tool to Claude Code's "Glob" so Anthropic doesn't detect the difference. If I remove that mapping, pi would send "find" directly to Anthropic, which could expose the stealth mode and potentially trigger OAuth restrictions. The core issue is that Claude Code actually has these specific tools—Read, Write, Edit, Bash, Grep, Glob—and the mapping ensures pi's tools align with what Claude Code exposes. like a CC tool but has different casing, the OAuth system might reject it. So pi's "find" tool needs to map to "Glob" to match CC's exact tool name and casing.
The core issue seems to be that OAuth restricts tools to specific CC tool names, but custom tools outside that set are permitted. This means pi's built-in tools need exact name matches with CC's tools to avoid authorization errors, while truly custom tools can exist freely.
Looking at the original code, the find: "Glob" mapping serves two purposes: it satisfies OAuth's tool name restrictions and provides stealth by hiding pi's actual tool names. The PR appears to conflate these concerns—the fundamental fix is case normalization to match CC's tools exactly, while the mapping itself is a separate stealth mechanism that should remain distinct.
The user's approach suggests a cleaner separation: maintain a canonical list of CC tool names with proper casing, then perform case-insensitive matching to convert any pi tool that corresponds to a CC tool. This way, find would naturally map to Glob through the matching logic rather than through an explicit alias, and tools like example or todowrite would either pass through unchanged or be rejected based on whether they match any CC tool. 1. List all CC tool names
2. Case-insensitive match → convert to CC casing
3. No match → keep original name
The find: "Glob" mapping seems like it belongs elsewhere—perhaps as a dedicated tool aliasing feature rather than part of the OAuth normalization logic.
You're right, I'm overcomplicating it.
The only thing needed:
// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
const ccLookup = new Map(claudeCodeTools.map(t => [t.toLowerCase(), t]));
const toClaudeCodeName = (name: string) => ccLookup.get(name.toLowerCase()) ?? name;That's it. Tool name matches CC tool (ignoring case) → use CC casing. Doesn't match → leave it alone.
The existing find: "Glob" mapping is a separate concern (pi's "find" masquerading as CC's "Glob" for stealth). That's not about the OAuth collision fix. The PR conflates the two.
Should I write up this feedback for the contributor, or are you going to just fix it when merging?
the existing find -> glob is bullshit. set up a minimal test in ai package (follow th pattern you see there) to prove that.
The user wants me to create a minimal test in the ai package to prove that the find -> Glob mapping is unnecessary. Let me first look at the existing test patterns in the ai package.
>ls -la /Users/badlogic/workspaces/pi-mono/packages/ai/test/also, we need the reverse as well, no? did you read the full @packages/ai/src/providers/anthropic.ts file? you should
The user is right - I need to look at the full anthropic.ts file to understand the reverse mapping (fromClaudeCodeName) as well. Let me re-read it carefully.
Now I see the full picture. Let me trace the flow:
toClaudeCodeName(name) - used when SENDING to Anthropic:
convertTools() - converts tool definitionsconvertMessages() - converts tool_use blocks in assistant messages (history replay)fromClaudeCodeName(name, tools) - used when RECEIVING from Anthropic:
event.content_block.type === "tool_use" - converts the tool name backThe current fromClaudeCodeName logic:
const fromClaudeCodeName = (name: string, tools?: Tool[]) => { if (tools && tools.length > 0) { const lowerName = name.toLowerCase(); const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName); if (matchedTool) return matchedTool.name; } return name;};This finds the tool in context.tools by case-insensitive match and returns the original tool name. This is correct - it maps "Read" back to "read" (or whatever the original tool name was).
BUT there's a problem with find -> Glob:
toClaudeCodeName("find") returns "Glob"fromClaudeCodeName("Glob", tools) looks for a tool with name matching "glob" (case-insensitive)context.tools is named "find", not "glob"Wait, that can't be right. If this was broken, pi wouldn't work at all with OAuth. Let me think again...
Actually, when we receive "Glob" back and search for a case-insensitive match, we're looking for a tool where tool.name.toLowerCase() === "glob". But pi's tool is registered as "find", so that comparison fails. There's no reverse mapping to get back to the original "find" tool name, which means the tool call would reference a non-existent tool.
This suggests the mapping might be broken or unused in practice. The real issue is that only tools matching Claude Code's standard names need mapping—tools like "find" that don't correspond to any CC tool shouldn't require special handling. The solution would be to check if a tool name already matches a CC tool (case-insensitive) before applying any mapping, avoiding unnecessary transformations. todowrite" (received, matched to tool named "todowrite")
The approach is simpler and avoids the earlier mistake of mapping tool names to unrelated alternatives. The find -> Glob mapping was problematic because it renamed the tool to something the model wouldn't recognize, and the reverse lookup would fail since no tool named "glob" exists in the available tools. I should write a test to validate this behavior works correctly across the transformation cycle.
I see now. The flow is:
toClaudeCodeName): Used in convertTools() and convertMessages() when sending tool definitions and tool_use history to AnthropicfromClaudeCodeName): Used when receiving tool_use events from Anthropic's streamThe current fromClaudeCodeName does a case-insensitive lookup against context.tools to find the original tool name. This is correct.
But here's the problem with find -> Glob:
"find" in context.toolstoClaudeCodeName("find") returns "Glob" → sent to Anthropic"Glob" toolfromClaudeCodeName("Glob", tools) looks for tool where name.toLowerCase() === "glob""find", so "find".toLowerCase() !== "glob""Glob""Glob" but no tool named "Glob"!The find -> Glob mapping is actually broken. Let me look at existing test patterns and create a test:
Now let me look at a simpler test file to understand the pattern better, and then create a unit test for the tool name normalization functions.
Now I understand the test patterns. Let me create a unit test for the Anthropic tool name normalization that proves:
find -> Glob mapping is broken (reverse mapping fails)I'll create a test that:
toClaudeCodeName and fromClaudeCodeName functions directlyfind -> Glob breaks the round-tripBut wait - these functions are not exported from anthropic.ts. They're internal to the module. I have a few options:
I think the best approach is to create a test file that imports the anthropic module and tests via an actual OAuth request. But that requires OAuth credentials.
Actually, looking at the test file, they do use OAuth tokens for testing. Let me create a minimal test that:
If find -> Glob is broken, the test should fail because the tool name won't match.
Let me also create a test for the case the PR is trying to fix: a user-defined tool named "todowrite" (lowercase) should work correctly.
Now let me create a test to prove the issue. I'll create a test that:
todowrite) works with case-insensitive normalizationfind -> Glob mapping is broken (the round-trip fails)Now let me run this test to see if it fails with the current implementation (which has the find -> Glob mapping).
>cd /Users/badlogic/workspaces/pi-mono/packages/ai && npm test -- test/anthropic-tool-name-normalization.test.ts 2>&1The tests prove exactly what we suspected:
Test 1 failed: "todowrite" tool triggers OAuth error "This credential is only authorized for use with Claude Code" - this is the exact bug the PR is trying to fix. The current code doesn't normalize "todowrite" to "TodoWrite" because it's not in the hardcoded claudeCodeToolNames map.
Test 3 failed: expected 'Glob' to be 'find' - this proves the find -> Glob mapping is broken! The tool was sent as "Glob" but when it came back, fromClaudeCodeName("Glob", tools) couldn't find a tool named "glob" in the tools list (the tool is named "find"), so it returned "Glob" instead of "find".
Test 2 passed: "read" works because it's in the claudeCodeToolNames map.
Test 4 passed: Custom tools that don't match CC names pass through unchanged.
This is great evidence! Now I have proof that:
find -> Glob mapping is broken (test 3 fails)Let me clean up the test and report to the user.
The tests prove exactly what we suspected:
Test results:
todowrite test FAILED - OAuth error "This credential is only authorized for use with Claude Code" - this is the bug the PR tries to fix. Current code doesn't normalize "todowrite" → "TodoWrite".
read test PASSED - Pi's built-in tools work because they're in the hardcoded map.
find -> Glob test FAILED - expected 'Glob' to be 'find' - proves the mapping is broken! The tool was sent as "Glob", came back as "Glob", and fromClaudeCodeName("Glob", tools) couldn't find a tool named "glob" (only "find" exists), so it returned "Glob".
Custom tool test PASSED - Tools not matching CC names pass through unchanged.
Summary:
find -> Glob mapping is demonstrably broken (test 3)The test file is at packages/ai/test/anthropic-tool-name-normalization.test.ts. Want me to:
anthropic.tsimplement the correct, concise fix, ensure test coverage is there, then return to me
The user wants me to implement the correct fix. Let me:
anthropic.ts file with the correct, simple implementationnpm run check to make sure everything is goodThe correct fix is:
toClaudeCodeName: case-insensitive lookup, return CC name if match, otherwise originalfromClaudeCodeName: already correct - finds tool in context.tools by case-insensitive matchNo find -> Glob mapping needed. No separate "pi tools" vs "extra CC tools" distinction.
Now I'll implement the correct fix. The fix is simple:1. One array of all CC tool names with canonical casing2. A Map for case-insensitive lookup
3. toClaudeCodeName does case-insensitive lookup, returns CC name or original4. fromClaudeCodeName stays the same (already correct)
Now let me run the tests to see if they pass.
Now let's run the tests:
>cd /Users/badlogic/workspaces/pi-mono/packages/ai && npm test -- test/anthropic-tool-name-normalization.test.ts 2>&1All 4 tests pass now. Let me run npm run check to make sure everything compiles correctly.
All 4 tests pass. Now let's run the full check:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1All checks pass. Now let me verify the fix is correct by reading the updated file to make sure it looks good.
Let me verify the final state of the fix:
The fix looks good and clean. Let me summarize what was done:
The fix:
claudeCodeToolNames mapping with a simple claudeCodeTools array of all CC tool names with canonical casingccToolLookup Map for case-insensitive lookuptoClaudeCodeName now does a simple case-insensitive lookup and returns the CC canonical name if found, otherwise the original namefromClaudeCodeName already worked correctly (finds tool in context.tools by case-insensitive match)Key fixes:
find: "Glob" mappingtodowrite) get normalized correctlyTest coverage:
todowrite → TodoWrite → todowrite) - round-trip worksread → Read → read) - round-trip worksfind is NOT mapped to Glob - it stays as find since there's no "Find" in CC toolsAll tests pass, all checks pass.
Done. Here's the summary:
The fix (13 lines of actual code):
// Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;What changed:
find: "Glob" mapping (and ls: "Ls")fromClaudeCodeName unchanged (already correct)Test coverage (test/anthropic-tool-name-normalization.test.ts):
| Test | Result |
|---|---|
todowrite → TodoWrite → todowrite | ✓ Pass |
read → Read → read | ✓ Pass |
find stays find (not mapped to Glob) | ✓ Pass |
| Custom tools pass through unchanged | ✓ Pass |
All checks pass:
npm run check ✓The PR's approach was overengineered (separate piToolNames + ccExtraTools merged into lookup). This is simpler: one list, one lookup.
commit and push, i'll comment on the pr myself
The user wants me to commit and push. I need to:
Files I changed:
I should NOT add the changelog yet since the user will handle the PR themselves.
>cd /Users/badlogic/workspaces/pi-mono && git statusI see there are other changes in the working directory that are not mine. I need to only commit:
Let me first pull to get up to date, then add only my files and commit.
>cd /Users/badlogic/workspaces/pi-mono && git pull --rebaseThere are unstaged changes that aren't mine. I need to stash them first, but AGENTS.md says I shouldn't use git stash because it could stash other agents' work. Let me just add my specific files and commit without pulling first, then do a pull --rebase after.
Actually, looking at AGENTS.md more carefully:
git stash - stashes ALL changes including other agents' work (forbidden)But I can still commit my own changes first, then pull --rebase. Let me try that.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/ai/src/providers/anthropic.ts packages/ai/test/anthropic-tool-name-normalization.test.tsGood, now let me commit these files.
>cd /Users/badlogic/workspaces/pi-mono && git commit -m "fix(ai): normalize tool names case-insensitively against CC tool list
- Replace hardcoded pi->CC tool mappings with single CC tool name list
- Case-insensitive lookup: if tool name matches CC tool, use CC casing
- Remove broken find->Glob mapping (round-trip failed)
- Add test coverage for tool name normalization"Now let me pull --rebase and push.
>cd /Users/badlogic/workspaces/pi-mono && git pull --rebase && git pushHmm, there are still unstaged changes from other work. I committed my changes, but git won't let me pull because of the unstaged changes. Let me check what the status is now.
>cd /Users/badlogic/workspaces/pi-mono && git statusThere are unstaged changes from another agent. According to AGENTS.md, I should not use git stash because it would stash other agents' work. But I need to pull to push my changes.
Let me try using git fetch + git rebase instead, which should work with unstaged changes if they don't conflict.
>cd /Users/badlogic/workspaces/pi-mono && git fetch origin && git rebase origin/mainGit won't let me rebase with unstaged changes. Let me check if the unstaged changes conflict with what I need to rebase. Actually, let me just try a regular merge instead of rebase.
>cd /Users/badlogic/workspaces/pi-mono && git merge origin/main --no-editThe merge worked. Now let me push.
>cd /Users/badlogic/workspaces/pi-mono && git pushDone. The commit with the fix has been pushed. Let me give the user a summary.
Done. Pushed to main:
a5f1016d fix(ai): normalize tool names case-insensitively against CC tool list6b4b4f4f Merge remote-tracking branch 'origin/main'Files committed:
packages/ai/src/providers/anthropic.tspackages/ai/test/anthropic-tool-name-normalization.test.ts## [Unreleased]
### Fixed
- Fixed orphaned tool results after errored assistant messages causing Codex API errors...## [Unreleased]
### Fixed
- Fixed Claude Code OAuth errors when user-defined tools collide with CC builtin names by normalizing tool names case-insensitively to CC canonical form// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", // pi calls this "find" "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
// Pi tool name aliases (pi name → CC name)const piToolAliases: Record<string, string> = { find: "Glob", // pi's "find" tool is CC's "Glob"};
const ccToolLookup = new Map<string, string>();for (const name of claudeCodeTools) { ccToolLookup.set(name.toLowerCase(), name);}for (const [piName, ccName] of Object.entries(piToolAliases)) { ccToolLookup.set(piName.toLowerCase(), ccName);}// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
// Pi aliases (pi uses "find", CC uses "Glob")const piAliases: Record<string, string> = { find: "Glob" };
const ccLookup = new Map([ ...claudeCodeTools.map(t => [t.toLowerCase(), t] as const), ...Object.entries(piAliases).map(([k, v]) => [k.toLowerCase(), v] as const),]);
const toClaudeCodeName = (name: string) => ccLookup.get(name.toLowerCase()) ?? name;const piToolNames: Record<string, string> = { read: "Read", write: "Write", edit: "Edit", bash: "Bash", grep: "Grep", find: "Glob", // <-- this is the only "alias"};const claudeCodeToolNames: Record<string, string> = { read: "Read", write: "Write", edit: "Edit", bash: "Bash", grep: "Grep", find: "Glob", ls: "Ls",};// All Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
const ccLookup = new Map(claudeCodeTools.map(t => [t.toLowerCase(), t]));
const toClaudeCodeName = (name: string) => ccLookup.get(name.toLowerCase()) ?? name;const fromClaudeCodeName = (name: string, tools?: Tool[]) => { if (tools && tools.length > 0) { const lowerName = name.toLowerCase(); const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName); if (matchedTool) return matchedTool.name; } return name;};// Claude Code 2.x tool names (canonical casing)const claudeCodeTools = [ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "KillShell", "NotebookEdit", "Skill", "Task", "TaskOutput", "TodoWrite", "WebFetch", "WebSearch",];
const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;a5f1016d fix(ai): normalize tool names case-insensitively against CC tool list6b4b4f4f Merge remote-tracking branch 'origin/main'