Fix shared tool I/O mappings between Claude Code and OpenCode#11
Merged
Fix shared tool I/O mappings between Claude Code and OpenCode#11
Conversation
BNasraoui
added a commit
to BNasraoui/opencode-claude-bridge
that referenced
this pull request
Apr 15, 2026
This PR was previously based on pre-v1.10.2 main. Upstream has since landed PRs dotCipher#9 (OAuth), dotCipher#10 (Windows compat), dotCipher#11 (shared tool mappings), and dotCipher#12 (CI). Rebased onto current main and reworked our improvements to layer cleanly on top of upstream's tool-mapping work. ## What changed from upstream v1.10.2 Upstream dotCipher#11 uses regex-on-raw-bytes to translate tool arguments as they stream in. That works when the whole JSON fits in one chunk, but fails silently when Anthropic splits a tool argument across a TCP boundary — e.g. "file_" in one chunk and "path" in the next. The regex never matches a split key, so the consumer receives untranslated snake_case and produces tool validation errors. This PR replaces that approach with three structural changes: 1. **SSE event framing (src/stream.ts).** New module with a stateful processor that parses SSE frames on \n\n boundaries, buffers input_json_delta fragments per content-block index, and emits a single translated input_json_delta at content_block_stop. Chunk-boundary corruption becomes structurally impossible. Uses start() + async loop instead of pull() — Bun's ReadableStream doesn't reliably re-invoke pull() when the handler resolves without enqueuing (which happens while we're buffering deltas). 2. **JSON.parse for tool arg translation (src/claude-tools.ts).** translateToolArgsJsonString parses the assembled JSON, walks the object, and serializes back. Key renames on parsed objects can't corrupt string values that happen to contain key names (e.g. a TodoWrite item whose content literally says "activeForm", or a Bash command with "file_path=" inside a heredoc). Replaces every regex substitution from dotCipher#11. 3. **Consolidated outbound translation (translateArgsOpencodeToClaude).** The inbound path is now a single function; the outbound path in index.ts body transform was a wall of nested if-blocks. Extracted to a sibling function so inbound/outbound stay in lockstep — next bug fix touches one place. ## What was carried forward from dotCipher#11 All of upstream's tool-mapping fixes are preserved: - Agent: subagent_type required; general-purpose ↔ general; strip model/run_in_background/isolation inbound; strip task_id/command outbound; default subagent_type=general inbound - AskUserQuestion: bidirectional multiSelect ↔ multiple per question - Skill: skill ↔ name; strip args inbound - WebFetch: strip prompt inbound, inject default format=markdown; synthesize prompt from format outbound, strip timeout - Bash: strip run_in_background/dangerouslyDisableSandbox inbound - Read: strip pages inbound - Grep: strip output_mode/-B/-A/-C/context/-n/-i/type/head_limit/ offset/multiline inbound - TodoWrite: bidirectional activeForm ↔ priority; cancelled → completed outbound ## Other changes - context_management and output_config.effort only injected for thinking-capable models (opus, sonnet-4-6). Previously sent to haiku too, which 400s. - Silent try/catch {} blocks now log via debugLog() gated by OPENCODE_CLAUDE_BRIDGE_DEBUG=1. Diagnosable without source changes. - flush() logs a debug warning if a tool_use block is abandoned mid-stream (upstream disconnect between start and stop). ## Tests 81 unit tests (up from 11). Test file imports actual production modules (createSseProcessor from ./stream, translateToolArgsJsonString and translateArgsOpencodeToClaude from ./claude-tools) — not local reimplementations. Assertions parse SSE events and use assert.deepEqual on object shape rather than substring matches. Covers: - Every INBOUND_TOOL_NAME_MAP entry (name mapping) - Every tool's argument translation (including the corruption cases) - Chunk boundary splits (fragmented args, events across chunks) - Interleaved tool_use blocks (per-block state isolation) - Error paths (malformed JSON, translator throws, abandoned blocks) - Byte-exact pass-through for ping/message_start/text_delta - Round-trip symmetry between inbound/outbound translation - All inbound field stripping (Agent/Bash/Read/Grep/Skill) - All outbound field handling (Agent/AskUserQuestion/WebFetch/TodoWrite) ## Verified end-to-end via opencode run Read, Edit, Write, Grep, Glob, Bash, Agent, WebFetch, Skill — all working. WebSearch remains a stub (not in OpenCode's AVAILABLE_TOOLS).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
subagent_type, mappinggeneralcorrectly, and dropping OpenCode-only agent history fieldsformatto ClaudepromptDetails
Agent.subagent_typeis now required in the advertised schema because OpenCode task validation requires itgeneral-purposenow maps to OpenCodegeneralinstead ofbuildsubagent_type: \"general\"when Claude omits itAskUserQuestion.questions[].multiSelectmaps to OpenCodemultipleand backSkill.skillmaps to OpenCodenameand back; unsupported Claudeargsare dropped inboundWebFetchis bridged best-effort:webfetchwithformat: \"markdown\"formatis converted into a synthetic Claude promptmodel,run_in_background,isolationrun_in_background,dangerouslyDisableSandboxpagesoutput_mode, context flags,type,head_limit,offset,multilineVerification
2.1.101to2.1.104npm test