opencode-claude-max-proxy 1.8.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,61 +4,64 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![GitHub stars](https://img.shields.io/github/stars/rynfar/opencode-claude-max-proxy.svg)](https://github.com/rynfar/opencode-claude-max-proxy/stargazers)
6
6
 
7
- A transparent proxy that lets you use your **Claude Max subscription** with [OpenCode](https://opencode.ai) with full multi-model agent delegation.
7
+ A transparent proxy that allows a Claude Max subscription to be used with [OpenCode](https://opencode.ai), preserving multi-model agent routing.
8
8
 
9
- ## The Idea
9
+ OpenCode targets the Anthropic API, while Claude Max provides access to Claude via the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). The proxy forwards tool calls from Claude Max to OpenCode so agent routing works correctly.
10
10
 
11
- OpenCode speaks the Anthropic API. Claude Max gives you unlimited Claude through the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). This proxy bridges the two but the interesting part isn't the bridging, it's **how tool execution and agent delegation work**.
11
+ Tool execution is handled within the Agent SDK, which is responsible for running tools, coordinating sub-agents, and streaming responses. Because the SDK only has access to Claude models, any tool calls or delegated tasks are executed within that scope. In configurations such as [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode), where agents may be assigned to OpenaI, Gemini, or other providers, those assignments are not preserved at execution time, and all work is effectively routed through Claude.
12
12
 
13
- Most proxy approaches try to handle everything internally: the SDK runs tools, manages subagents, and streams back a finished result. The problem? The SDK only knows about Claude models. If you're using [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) with agents routed to GPT-5.4, Gemini, or other providers, the SDK can't reach them. Your carefully configured multi-model agent setup gets flattened to "everything runs on Claude."
13
+ We avoid that limitation by forwarding tool calls instead of executing them. When a `tool_use` event is emitted, the proxy intercepts it, halts the current turn, and sends the raw payload back to OpenCode. OpenCode executes the tool, including any agent routing, and returns the result as a `tool_result`. The proxy then resumes the SDK session.
14
14
 
15
- This proxy takes a different approach: **passthrough delegation**. Instead of handling tools internally, the proxy intercepts every tool call *before the SDK executes it*, stops the turn, and forwards the raw `tool_use` back to OpenCode. OpenCode handles execution including routing `Task` calls through its own agent system with full model routing. The result comes back as a standard `tool_result`, the proxy resumes the SDK session, and Claude continues.
16
-
17
- The effect: Claude thinks it's calling tools normally. OpenCode thinks it's talking to the Anthropic API. But behind the scenes, your oracle agent runs on GPT-5.4, your explore agent runs on Gemini, and the main session runs on Claude Max — exactly as configured.
15
+ From Claude’s perspective, tool usage proceeds normally. From OpenCode’s perspective, it is interacting with the Anthropic API. Execution remains distributed according to the configured agents, allowing different models to handle different roles without being constrained by the SDK.
18
16
 
19
17
  ```
20
18
  ┌──────────┐ ┌───────────────┐ ┌──────────────┐
21
- │ │ 1. Request │ │ SDK Auth │ │
19
+ │ │ │ │ │ │
22
20
  │ OpenCode │ ─────────────► │ Proxy │ ───────────► │ Claude Max │
23
- │ │ │ (localhost) │ │
24
- │ │ 4. tool_use │ │ 2. Generate │ │
25
- │ │ ◄───────────── PreToolUse │ ◄─────────── │ Response
26
- │ │ │ hook blocks │ │ │
27
- │ │ 5. Execute └──────────────┘
28
- │ │ ─── Task ───►
29
- │ │ (routes to ┌──────────────┐
30
- │ │ GPT-5.4, 6. Resume │ │ oh-my- │
31
- │ │ Gemini,etc) ◄──────────── │ │ opencode │
32
- │ │ │ tool_result agents
33
- │ │ 7. Final │ │
34
- │ │ ◄───────────── oracle:
35
- │ │ response │ │ │ GPT-5.4 │
36
- └──────────┘ └───────────────┘ explore:
37
- Gemini
38
- librarian:
39
- Sonnet
40
- └──────────────┘
21
+ │ │ │ (localhost) │ │ (Agent SDK)
22
+ │ │ │ │ │ │
23
+ │ │ │ ◄─────────── │ tool_use
24
+ │ │ │ ◄──────────── │ │ │
25
+ │ │ Intercept │ └──────────────┘
26
+ │ │ (stop turn)
27
+ │ │
28
+ │ │
29
+ │ │ ┌──────────────┐
30
+ │ │ │ │ OpenCode
31
+ │ │ agent
32
+ │ │ system
33
+ │ │ └──────────────┘
34
+ │ │
35
+ │ ▼
36
+ │ ◄────────────
37
+ │ Resume turn
38
+ │ │ │
39
+ │ │ ◄───────────── │
40
+ │ │ response │
41
+ └──────────┘ └───────────────┘
41
42
  ```
42
43
 
43
44
  ## How Passthrough Works
44
45
 
45
- The key insight is the Claude Agent SDK's `PreToolUse` hook — an officially supported callback that fires before any tool executes. Combined with `maxTurns: 1`, it gives us precise control over the execution boundary:
46
+ The Claude Agent SDK exposes a `PreToolUse` hook that fires before any tool executes. Combined with `maxTurns: 1`, it gives us precise control over the execution boundary without monkey-patching or stream rewriting.
46
47
 
47
48
  1. **Claude generates a response** with `tool_use` blocks (Read a file, delegate to an agent, run a command)
48
- 2. **The PreToolUse hook fires** for each tool call we capture the tool name, input, and ID, then return `decision: "block"` to prevent SDK-internal execution
49
+ 2. **The PreToolUse hook fires** for each tool call - we capture the tool name, input, and ID, then return `decision: "block"` to prevent SDK-internal execution
49
50
  3. **The SDK stops** (blocked tool + maxTurns:1 = turn complete) and we have the full tool_use payload
50
51
  4. **The proxy returns it to OpenCode** as a standard Anthropic API response with `stop_reason: "tool_use"`
51
52
  5. **OpenCode handles everything** — file reads, shell commands, and crucially, `Task` delegation through its own agent system with full model routing
52
53
  6. **OpenCode sends `tool_result` back**, the proxy resumes the SDK session, and Claude continues
53
54
 
54
- No monkey-patching. No forked SDKs. No fragile stream rewriting. Just a hook and a turn limit.
55
-
56
55
  ## Quick Start
57
56
 
58
57
  ### Prerequisites
59
58
 
60
- 1. **Claude Max subscription** — [Subscribe here](https://claude.ai/settings/subscription)
59
+ 1. **Claude Max subscription** — [Subscribe here](https://claude.ai/settings/billing)
61
60
  2. **Claude CLI** authenticated: `npm install -g @anthropic-ai/claude-code && claude login`
61
+ 3. **Clear any existing OpenCode Anthropic auth** — if you previously used OpenCode with a real Anthropic API key, log out first or the cached auth will override the proxy:
62
+ ```bash
63
+ opencode auth logout # select "anthropic" when prompted
64
+ ```
62
65
 
63
66
  ### Option A: npm Install
64
67
 
@@ -98,32 +101,45 @@ docker compose exec proxy claude login
98
101
  curl http://127.0.0.1:3456/health
99
102
  ```
100
103
 
101
- > **Note:** On macOS, `claude login` must be run inside the container (keychain credentials can't be mounted). On Linux, volume-mounting `~/.claude` may work without re-login.
104
+ > **Note:** On macOS, use `./bin/docker-auth.sh` to copy host credentials into the container (handles the keychain/scopes format difference). On Linux, volume-mounting `~/.claude` may work directly.
102
105
 
103
106
  ### Connect OpenCode
104
107
 
105
- #### Environment Variables
108
+ #### Per-Terminal Launcher (recommended)
106
109
 
107
110
  ```bash
108
- ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
111
+ ./bin/oc.sh
109
112
  ```
110
113
 
111
- The `ANTHROPIC_API_KEY` can be any non-empty string the proxy doesn't use it. Authentication is handled by your `claude login` session.
114
+ Each terminal gets its own proxy on a random port. Proxy starts automatically, connects OpenCode, and cleans up on exit. Sessions resume across terminals via a shared file store.
112
115
 
113
- #### Shell Alias
116
+ Shell alias:
114
117
 
115
118
  ```bash
116
- # Add to ~/.zshrc or ~/.bashrc or ~/.config/fish/config.fish
117
- alias oc='ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode'
119
+ # ~/.zshrc or ~/.bashrc
120
+ alias oc='/path/to/opencode-claude-max-proxy/bin/oc.sh'
118
121
  ```
119
122
 
120
- #### OpenCode Config File
123
+ #### Shared Proxy
124
+
125
+ For a single long-running proxy:
126
+
127
+ ```bash
128
+ # Terminal 1: start the proxy
129
+ CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
130
+
131
+ # Terminal 2+: connect OpenCode
132
+ ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
133
+ ```
121
134
 
122
- Alternatively, the proxy URL and API key can be set globally in `~/.config/opencode/opencode.json`. This has the benefit of working in OpenCode Desktop as well.
135
+ `ANTHROPIC_API_KEY` can be any non-empty string. Authentication is handled by `claude login`.
136
+
137
+ #### OpenCode Desktop / Config File
138
+
139
+ Set the proxy URL in `~/.config/opencode/opencode.json` to use with Desktop or avoid env vars.
123
140
 
124
141
  ```json
125
142
  {
126
- ...
127
143
  "provider": {
128
144
  "anthropic": {
129
145
  "options": {
@@ -132,10 +148,11 @@ Alternatively, the proxy URL and API key can be set globally in `~/.config/openc
132
148
  }
133
149
  }
134
150
  }
135
- ...
136
151
  }
137
152
  ```
138
153
 
154
+ > Desktop requires the shared proxy on a fixed port. Run `CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy` in the background. Sessions are shared between Desktop and terminal instances.
155
+
139
156
  ## Modes
140
157
 
141
158
  ### Passthrough Mode (recommended)
@@ -158,13 +175,13 @@ bun run proxy
158
175
 
159
176
  Tools execute inside the proxy via MCP. Subagents run on Claude via the SDK's native agent system. Simpler setup, but all agents use Claude regardless of oh-my-opencode config.
160
177
 
161
- | | Passthrough | Internal |
162
- |---|---|---|
163
- | Tool execution | OpenCode | Proxy (MCP) |
164
- | Agent delegation | OpenCode → multi-model | SDK → Claude only |
165
- | oh-my-opencode models | ✅ Respected | ❌ All Claude |
166
- | Agent system prompts | ✅ Full | ⚠️ Description only |
167
- | Setup complexity | Same | Same |
178
+ | | Passthrough | Internal |
179
+ | --------------------- | ---------------------- | ------------------- |
180
+ | Tool execution | OpenCode | Proxy (MCP) |
181
+ | Agent delegation | OpenCode → multi-model | SDK → Claude only |
182
+ | oh-my-opencode models | ✅ Respected | ❌ All Claude |
183
+ | Agent system prompts | ✅ Full | ⚠️ Description only |
184
+ | Setup complexity | Same | Same |
168
185
 
169
186
  ## Works With Any Agent Framework
170
187
 
@@ -178,118 +195,93 @@ In internal mode, a `PreToolUse` hook fuzzy-matches agent names as a safety net
178
195
 
179
196
  ## Session Resume
180
197
 
181
- The proxy tracks SDK session IDs and resumes conversations on follow-up requests:
198
+ The proxy tracks SDK session IDs and resumes conversations on follow-up requests. Sessions are stored in `~/.cache/opencode-claude-max-proxy/sessions.json`, shared across all proxy instances (including per-terminal proxies).
182
199
 
183
- - **Faster responses** — no re-processing of conversation history
184
- - **Better context** — the SDK remembers tool results from previous turns
200
+ Lookup order:
185
201
 
186
- Session tracking works two ways:
202
+ 1. **Header-based** add the included OpenCode plugin to inject session headers:
187
203
 
188
- 1. **Header-based** (recommended) — Add the included OpenCode plugin:
189
204
  ```json
190
- { "plugin": ["./path/to/opencode-claude-max-proxy/src/plugin/claude-max-headers.ts"] }
205
+ {
206
+ "plugin": [
207
+ "./path/to/opencode-claude-max-proxy/src/plugin/claude-max-headers.ts"
208
+ ]
209
+ }
191
210
  ```
192
211
 
193
- 2. **Fingerprint-based** (automatic fallback) — hashes the first user message to identify returning conversations
212
+ 2. **Fingerprint-based** (automatic fallback) — hashes the first user message to match returning conversations
194
213
 
195
- Sessions are cached for 24 hours.
214
+ Sessions expire after 24 hours.
196
215
 
197
216
  ## Configuration
198
217
 
199
- | Variable | Default | Description |
200
- |----------|---------|-------------|
201
- | `CLAUDE_PROXY_PASSTHROUGH` | (unset) | Enable passthrough mode forward all tools to OpenCode |
202
- | `CLAUDE_PROXY_PORT` | 3456 | Proxy server port |
203
- | `CLAUDE_PROXY_HOST` | 127.0.0.1 | Proxy server host |
204
- | `CLAUDE_PROXY_WORKDIR` | (cwd) | Working directory for Claude and tools |
205
- | `CLAUDE_PROXY_MAX_CONCURRENT` | 1 | Max concurrent SDK sessions (increase with caution) |
206
- | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | 120 | Connection idle timeout |
218
+ | Variable | Default | Description |
219
+ | ----------------------------------- | --------- | -------------------------------------------------------- |
220
+ | `CLAUDE_PROXY_PASSTHROUGH` | (unset) | Enable passthrough mode to forward all tools to OpenCode |
221
+ | `CLAUDE_PROXY_PORT` | 3456 | Proxy server port |
222
+ | `CLAUDE_PROXY_HOST` | 127.0.0.1 | Proxy server host |
223
+ | `CLAUDE_PROXY_WORKDIR` | (cwd) | Working directory for Claude and tools |
224
+ | `CLAUDE_PROXY_MAX_CONCURRENT` | 1 | Max concurrent SDK sessions (increase with caution) |
225
+ | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | 120 | Connection idle timeout |
207
226
 
208
227
  ## Concurrency
209
228
 
210
- The proxy supports multiple simultaneous OpenCode instances. Each request spawns its own independent SDK subprocess — run as many terminals as you want. All concurrent responses are delivered correctly.
211
-
212
- **Use the auto-restart supervisor** (recommended):
213
-
214
- ```bash
215
- CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
216
- # or directly:
217
- CLAUDE_PROXY_PASSTHROUGH=1 ./bin/claude-proxy-supervisor.sh
218
- ```
229
+ Per-terminal proxies (`oc.sh`) avoid concurrency issues entirely. Each terminal gets its own proxy.
219
230
 
220
- > **⚠️ Known Issue: Bun SSE Crash ([oven-sh/bun#17947](https://github.com/oven-sh/bun/issues/17947))**
221
- >
222
- > The Claude Agent SDK's `cli.js` subprocess is compiled with Bun, which has a known segfault in `structuredCloneForStream` during cleanup of concurrent streaming responses. This affects all runtimes (Bun, Node.js via tsx) because the crash originates in the SDK's child process, not in the proxy itself.
223
- >
224
- > **What this means in practice:**
225
- > - **Sequential requests (1 terminal):** No impact. Never crashes.
226
- > - **Concurrent requests (2+ terminals):** All responses are delivered correctly. The crash occurs *after* responses complete, during stream cleanup. No work is lost.
227
- > - **After a crash:** The supervisor restarts the proxy in ~1-3 seconds. If a new request arrives during this window, OpenCode shows "Unable to connect" — just retry.
228
- >
229
- > We are monitoring the upstream Bun issue for a fix. Once patched, the supervisor becomes optional.
230
-
231
- ## Model Mapping
232
-
233
- | OpenCode Model | Claude SDK |
234
- |----------------|------------|
235
- | `anthropic/claude-opus-*` | opus |
236
- | `anthropic/claude-sonnet-*` | sonnet (default) |
237
- | `anthropic/claude-haiku-*` | haiku |
238
-
239
- ## Disclaimer
240
-
241
- This project is an **unofficial wrapper** around Anthropic's publicly available [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It is not affiliated with, endorsed by, or supported by Anthropic.
242
-
243
- **Use at your own risk.** The authors make no claims regarding compliance with Anthropic's Terms of Service. It is your responsibility to review and comply with [Anthropic's Terms of Service](https://www.anthropic.com/terms) and any applicable usage policies. Terms may change at any time.
244
-
245
- This project calls `query()` from Anthropic's public npm package using your own authenticated account. No API keys are intercepted, no authentication is bypassed, and no proprietary systems are reverse-engineered.
231
+ The shared proxy supports concurrent requests but the SDK's `cli.js` subprocess (compiled with Bun) can segfault during stream cleanup ([oven-sh/bun#17947](https://github.com/oven-sh/bun/issues/17947)). Responses are always delivered correctly; the crash occurs after completion. The supervisor auto-restarts within a few seconds.
246
232
 
247
233
  ## FAQ
248
234
 
249
235
  <details>
250
236
  <summary><strong>Why passthrough mode instead of handling tools internally?</strong></summary>
251
237
 
252
- Internal tool execution means the SDK handles everything but the SDK only speaks Claude. If your agents are configured for GPT-5.4 or Gemini via oh-my-opencode, that routing gets lost. Passthrough mode preserves the full OpenCode agent pipeline, including multi-model routing, full system prompts, and agent lifecycle management.
238
+ If the Agent SDK executes tools directly, everything runs through Claude. Any agent routing defined in OpenCode is bypassed. Passthrough mode just sends the tool calls to OpenCode to run.
239
+
253
240
  </details>
254
241
 
255
242
  <details>
256
243
  <summary><strong>Does this work without oh-my-opencode?</strong></summary>
257
244
 
258
- Yes. Both modes work with native OpenCode (build + plan agents) and with any custom agents defined in your `opencode.json`. oh-my-opencode just adds more agents and model routing the proxy handles whatever OpenCode sends.
245
+ Yes. Both modes work with native OpenCode (build + plan agents) and with any custom agents defined in your `opencode.json`. oh-my-opencode just adds more agents and model routing. The proxy handles whatever OpenCode sends.
246
+
259
247
  </details>
260
248
 
261
249
  <details>
262
- <summary><strong>Why do I need ANTHROPIC_API_KEY=dummy?</strong></summary>
250
+ <summary><strong>Why do I need `ANTHROPIC_API_KEY=dummy`?</strong></summary>
263
251
 
264
252
  OpenCode requires an API key to be set, but the proxy never uses it. Authentication is handled by your `claude login` session through the Agent SDK.
253
+
265
254
  </details>
266
255
 
267
256
  <details>
268
257
  <summary><strong>What about rate limits?</strong></summary>
269
258
 
270
259
  Your Claude Max subscription has its own usage limits. The proxy doesn't add any additional limits. Concurrent requests are supported.
260
+
271
261
  </details>
272
262
 
273
263
  <details>
274
264
  <summary><strong>Is my data sent anywhere else?</strong></summary>
275
265
 
276
266
  No. The proxy runs locally. Requests go directly to Claude through the official SDK. In passthrough mode, tool execution happens in OpenCode on your machine.
267
+
277
268
  </details>
278
269
 
279
270
  <details>
280
271
  <summary><strong>Why does internal mode use MCP tools?</strong></summary>
281
272
 
282
273
  The Claude Agent SDK uses different parameter names for tools than OpenCode (e.g., `file_path` vs `filePath`). Internal mode provides its own MCP tools with SDK-compatible parameter names. Passthrough mode doesn't need this since OpenCode handles tool execution directly.
274
+
283
275
  </details>
284
276
 
285
277
  ## Troubleshooting
286
278
 
287
- | Problem | Solution |
288
- |---------|----------|
289
- | "Authentication failed" | Run `claude login` to authenticate |
290
- | "Connection refused" | Make sure the proxy is running: `bun run proxy` |
291
- | "Port 3456 is already in use" | `kill $(lsof -ti :3456)` or use `CLAUDE_PROXY_PORT=4567` |
292
- | Title generation fails | Set `"small_model": "anthropic/claude-haiku-4-5"` in your OpenCode config |
279
+ | Problem | Solution |
280
+ | ----------------------------- | ------------------------------------------------------------------------- |
281
+ | "Authentication failed" | Run `claude login` to authenticate |
282
+ | "Connection refused" | Make sure the proxy is running: `bun run proxy` |
283
+ | "Port 3456 is already in use" | `kill $(lsof -ti :3456)` or use `CLAUDE_PROXY_PORT=4567` |
284
+ | Title generation fails | Set `"small_model": "anthropic/claude-haiku-4-5"` in your OpenCode config |
293
285
 
294
286
  ## Auto-start (macOS)
295
287
 
@@ -323,23 +315,14 @@ EOF
323
315
  launchctl load ~/Library/LaunchAgents/com.claude-max-proxy.plist
324
316
  ```
325
317
 
326
- ## Testing
318
+ ## Development
319
+
320
+ ### Run tests
327
321
 
328
322
  ```bash
329
323
  bun test
330
324
  ```
331
325
 
332
- 106 tests across 13 files covering:
333
- - Passthrough tool forwarding and tool_result acceptance
334
- - PreToolUse hook interception and agent name fuzzy matching
335
- - SDK agent definition extraction (native OpenCode + oh-my-opencode)
336
- - MCP tool filtering (internal mode)
337
- - Session resume (header-based and fingerprint-based)
338
- - Streaming message deduplication
339
- - Working directory propagation
340
- - Concurrent request handling
341
- - Error classification (auth, rate limit, billing, timeout)
342
-
343
326
  ### Health Endpoint
344
327
 
345
328
  ```bash
@@ -348,7 +331,7 @@ curl http://127.0.0.1:3456/health
348
331
 
349
332
  Returns auth status, subscription type, and proxy mode. Use this to verify the proxy is running and authenticated before connecting OpenCode.
350
333
 
351
- ## Architecture
334
+ ### Architecture
352
335
 
353
336
  ```
354
337
  src/
@@ -360,24 +343,17 @@ src/
360
343
  ├── mcpTools.ts # MCP tool definitions for internal mode (read, write, edit, bash, glob, grep)
361
344
  ├── logger.ts # Structured logging with AsyncLocalStorage context
362
345
  ├── plugin/
363
- └── claude-max-headers.ts # OpenCode plugin for session header injection
364
- └── __tests__/ # 106 tests across 13 files
365
- ├── helpers.ts
366
- ├── integration.test.ts
367
- ├── proxy-agent-definitions.test.ts
368
- ├── proxy-agent-fuzzy-match.test.ts
369
- ├── proxy-error-handling.test.ts
370
- ├── proxy-mcp-filtering.test.ts
371
- ├── proxy-passthrough-concept.test.ts
372
- ├── proxy-pretooluse-hook.test.ts
373
- ├── proxy-session-resume.test.ts
374
- ├── proxy-streaming-message.test.ts
375
- ├── proxy-subagent-support.test.ts
376
- ├── proxy-tool-forwarding.test.ts
377
- ├── proxy-transparent-tools.test.ts
378
- └── proxy-working-directory.test.ts
346
+ └── claude-max-headers.ts # OpenCode plugin for session header injection
379
347
  ```
380
348
 
349
+ ## Disclaimer
350
+
351
+ This project is an **unofficial wrapper** around Anthropic's publicly available [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It is not affiliated with, endorsed by, or supported by Anthropic.
352
+
353
+ **Use at your own risk.** The authors make no claims regarding compliance with Anthropic's Terms of Service. It is your responsibility to review and comply with [Anthropic's Terms of Service](https://www.anthropic.com/legal/consumer-terms) and [Authorized Usage Policy](https://www.anthropic.com/legal/aup). Terms may change at any time.
354
+
355
+ This project calls `query()` from Anthropic's public npm package using your own authenticated account. No API keys are intercepted, no authentication is bypassed, and no proprietary systems are reverse-engineered.
356
+
381
357
  ## License
382
358
 
383
359
  MIT
package/bin/oc.sh ADDED
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # Per-terminal proxy launcher for OpenCode.
3
+ #
4
+ # Starts a dedicated proxy on a random port, launches OpenCode pointed at it,
5
+ # and cleans up when OpenCode exits. Each terminal gets its own proxy — no
6
+ # concurrent request issues, no shared port conflicts.
7
+ #
8
+ # Session resume works across terminals via the shared session file store.
9
+ #
10
+ # Usage: ./bin/oc.sh [opencode args...]
11
+
12
+ set -e
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ PROXY_SCRIPT="$SCRIPT_DIR/claude-proxy.ts"
16
+
17
+ if [ ! -f "$PROXY_SCRIPT" ]; then
18
+ echo "❌ Proxy script not found: $PROXY_SCRIPT" >&2
19
+ exit 1
20
+ fi
21
+
22
+ # Pick a random free port
23
+ PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()' 2>/dev/null \
24
+ || ruby -e 'require "socket"; s = TCPServer.new("127.0.0.1", 0); puts s.addr[1]; s.close' 2>/dev/null \
25
+ || echo $((RANDOM + 10000)))
26
+
27
+ # Start proxy in background
28
+ CLAUDE_PROXY_PORT=$PORT \
29
+ CLAUDE_PROXY_WORKDIR="$PWD" \
30
+ CLAUDE_PROXY_PASSTHROUGH="${CLAUDE_PROXY_PASSTHROUGH:-1}" \
31
+ bun run "$PROXY_SCRIPT" > /dev/null 2>&1 &
32
+ PROXY_PID=$!
33
+
34
+ # Ensure proxy is cleaned up on exit
35
+ cleanup() {
36
+ kill $PROXY_PID 2>/dev/null
37
+ wait $PROXY_PID 2>/dev/null
38
+ }
39
+ trap cleanup EXIT INT TERM
40
+
41
+ # Wait for proxy to be ready (up to 10 seconds)
42
+ for i in $(seq 1 100); do
43
+ if curl -sf "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
44
+ break
45
+ fi
46
+ if ! kill -0 $PROXY_PID 2>/dev/null; then
47
+ echo "❌ Proxy failed to start" >&2
48
+ exit 1
49
+ fi
50
+ sleep 0.1
51
+ done
52
+
53
+ # Verify proxy is healthy
54
+ if ! curl -sf "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
55
+ echo "❌ Proxy didn't become healthy within 10 seconds" >&2
56
+ exit 1
57
+ fi
58
+
59
+ # Launch OpenCode
60
+ ANTHROPIC_API_KEY=dummy \
61
+ ANTHROPIC_BASE_URL="http://127.0.0.1:$PORT" \
62
+ opencode "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-max-proxy",
3
- "version": "1.8.1",
3
+ "version": "1.10.1",
4
4
  "description": "Use your Claude Max subscription with OpenCode via proxy server",
5
5
  "type": "module",
6
6
  "main": "./src/proxy/server.ts",
@@ -15,6 +15,7 @@ import { withClaudeLogContext } from "../logger"
15
15
  import { fuzzyMatchAgentName } from "./agentMatch"
16
16
  import { buildAgentDefinitions } from "./agentDefs"
17
17
  import { createPassthroughMcpServer, stripMcpPrefix, PASSTHROUGH_MCP_NAME, PASSTHROUGH_MCP_PREFIX } from "./passthroughTools"
18
+ import { lookupSharedSession, storeSharedSession, clearSharedSessions } from "./sessionStore"
18
19
 
19
20
  // --- Session Tracking ---
20
21
  // Maps OpenCode session ID (or fingerprint) → Claude SDK session ID
@@ -31,6 +32,8 @@ const fingerprintCache = new Map<string, SessionState>()
31
32
  export function clearSessionCache() {
32
33
  sessionCache.clear()
33
34
  fingerprintCache.clear()
35
+ // Also clear shared file store
36
+ try { clearSharedSessions() } catch {}
34
37
  }
35
38
 
36
39
  // Clean stale sessions every hour — sessions survive a full workday
@@ -63,13 +66,41 @@ function lookupSession(
63
66
  opencodeSessionId: string | undefined,
64
67
  messages: Array<{ role: string; content: any }>
65
68
  ): SessionState | undefined {
66
- // Primary: use x-opencode-session header
69
+ // When a session ID is provided, only match by that ID — don't fall through
70
+ // to fingerprint. A different session ID means a different session.
67
71
  if (opencodeSessionId) {
68
- return sessionCache.get(opencodeSessionId)
72
+ const cached = sessionCache.get(opencodeSessionId)
73
+ if (cached) return cached
74
+ // Check shared file store
75
+ const shared = lookupSharedSession(opencodeSessionId)
76
+ if (shared) {
77
+ const state: SessionState = {
78
+ claudeSessionId: shared.claudeSessionId,
79
+ lastAccess: shared.lastUsedAt,
80
+ messageCount: 0,
81
+ }
82
+ sessionCache.set(opencodeSessionId, state)
83
+ return state
84
+ }
85
+ return undefined
69
86
  }
70
- // Fallback: fingerprint (only when no header is present)
87
+
88
+ // No session ID — use fingerprint fallback
71
89
  const fp = getConversationFingerprint(messages)
72
- if (fp) return fingerprintCache.get(fp)
90
+ if (fp) {
91
+ const cached = fingerprintCache.get(fp)
92
+ if (cached) return cached
93
+ const shared = lookupSharedSession(fp)
94
+ if (shared) {
95
+ const state: SessionState = {
96
+ claudeSessionId: shared.claudeSessionId,
97
+ lastAccess: shared.lastUsedAt,
98
+ messageCount: 0,
99
+ }
100
+ fingerprintCache.set(fp, state)
101
+ return state
102
+ }
103
+ }
73
104
  return undefined
74
105
  }
75
106
 
@@ -81,9 +112,13 @@ function storeSession(
81
112
  ) {
82
113
  if (!claudeSessionId) return
83
114
  const state: SessionState = { claudeSessionId, lastAccess: Date.now(), messageCount: messages?.length || 0 }
115
+ // In-memory cache
84
116
  if (opencodeSessionId) sessionCache.set(opencodeSessionId, state)
85
117
  const fp = getConversationFingerprint(messages)
86
118
  if (fp) fingerprintCache.set(fp, state)
119
+ // Shared file store (cross-proxy resume)
120
+ const key = opencodeSessionId || fp
121
+ if (key) storeSharedSession(key, claudeSessionId, state.messageCount)
87
122
  }
88
123
 
89
124
  /** Extract only the last user message (for resume — SDK already has history) */
@@ -0,0 +1,94 @@
1
+ /**
2
+ * File-based session store for cross-proxy session resume.
3
+ *
4
+ * When running per-terminal proxies (each on a different port),
5
+ * sessions need to be shared so you can resume a conversation
6
+ * started in one terminal from another. This stores session
7
+ * mappings in a JSON file that all proxy instances read/write.
8
+ *
9
+ * Format: { [key]: { claudeSessionId, createdAt, lastUsedAt } }
10
+ * Keys are either OpenCode session IDs or conversation fingerprints.
11
+ */
12
+
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs"
14
+ import { join, dirname } from "path"
15
+ import { homedir } from "os"
16
+
17
+ export interface StoredSession {
18
+ claudeSessionId: string
19
+ createdAt: number
20
+ lastUsedAt: number
21
+ messageCount: number
22
+ }
23
+
24
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
25
+
26
+ function getStorePath(): string {
27
+ const dir = process.env.CLAUDE_PROXY_SESSION_DIR
28
+ || join(homedir(), ".cache", "opencode-claude-max-proxy")
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true })
31
+ }
32
+ return join(dir, "sessions.json")
33
+ }
34
+
35
+ function readStore(): Record<string, StoredSession> {
36
+ const path = getStorePath()
37
+ if (!existsSync(path)) return {}
38
+ try {
39
+ const data = readFileSync(path, "utf-8")
40
+ const store = JSON.parse(data) as Record<string, StoredSession>
41
+ // Prune expired entries
42
+ const now = Date.now()
43
+ const pruned: Record<string, StoredSession> = {}
44
+ for (const [key, session] of Object.entries(store)) {
45
+ if (now - session.lastUsedAt < SESSION_TTL_MS) {
46
+ pruned[key] = session
47
+ }
48
+ }
49
+ return pruned
50
+ } catch {
51
+ return {}
52
+ }
53
+ }
54
+
55
+ function writeStore(store: Record<string, StoredSession>): void {
56
+ const path = getStorePath()
57
+ const tmp = path + ".tmp"
58
+ try {
59
+ writeFileSync(tmp, JSON.stringify(store, null, 2))
60
+ renameSync(tmp, path) // atomic write
61
+ } catch {
62
+ // If rename fails, try direct write
63
+ try {
64
+ writeFileSync(path, JSON.stringify(store, null, 2))
65
+ } catch {}
66
+ }
67
+ }
68
+
69
+ export function lookupSharedSession(key: string): StoredSession | undefined {
70
+ const store = readStore()
71
+ const session = store[key]
72
+ if (!session) return undefined
73
+ if (Date.now() - session.lastUsedAt >= SESSION_TTL_MS) return undefined
74
+ return session
75
+ }
76
+
77
+ export function storeSharedSession(key: string, claudeSessionId: string, messageCount?: number): void {
78
+ const store = readStore()
79
+ const existing = store[key]
80
+ store[key] = {
81
+ claudeSessionId,
82
+ createdAt: existing?.createdAt || Date.now(),
83
+ lastUsedAt: Date.now(),
84
+ messageCount: messageCount ?? existing?.messageCount ?? 0,
85
+ }
86
+ writeStore(store)
87
+ }
88
+
89
+ export function clearSharedSessions(): void {
90
+ const path = getStorePath()
91
+ try {
92
+ writeFileSync(path, "{}")
93
+ } catch {}
94
+ }