opencode-with-claude 1.0.0 → 1.1.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
@@ -7,11 +7,11 @@ Use [OpenCode](https://opencode.ai) with your [Claude Max](https://claude.ai) su
7
7
  ## How It Works
8
8
 
9
9
  ```
10
- ┌─────────────┐ ┌────────────────────┐ ┌─────────────────┐
11
- │ OpenCode │──────▶│ Claude Max Proxy │──────▶│ Anthropic │
12
- │ (TUI/Web) │ :3456 │ (local server) │ SDK │ Claude Max │
13
- │◀──────│ │◀──────│ │
14
- └─────────────┘ └────────────────────┘ └─────────────────┘
10
+ ┌─────────────┐ ┌────────────────────┐ ┌─────────────────┐
11
+ │ OpenCode │─────────────▶│ Claude Max Proxy │──────▶│ Anthropic │
12
+ │ (TUI/Web) │ :3456 / auto │ (local server) │ SDK │ Claude Max │
13
+ │◀─────────────│ │◀──────│ │
14
+ └─────────────┘ └────────────────────┘ └─────────────────┘
15
15
  ```
16
16
 
17
17
  [OpenCode](https://opencode.ai) speaks the Anthropic REST API. Claude Max provides access via the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) (not the REST API). The [opencode-claude-max-proxy](https://github.com/rynfar/opencode-claude-max-proxy) bridges the gap — it accepts API requests from OpenCode and translates them into Agent SDK calls using your Claude Max session.
@@ -22,7 +22,7 @@ There are three ways to get started: the **plugin** (recommended), the **standal
22
22
 
23
23
  ### Option A: OpenCode Plugin (recommended)
24
24
 
25
- The plugin manages the proxy lifecycle automatically — it starts the proxy when OpenCode launches, health-checks it, and cleans up on exit.
25
+ The plugin manages the proxy lifecycle automatically — it starts the proxy when OpenCode launches, configures the Anthropic provider, and cleans up on exit. Each OpenCode instance gets its own proxy on an OS-assigned port, so multiple instances can run simultaneously without conflicts.
26
26
 
27
27
  **1. Authenticate with Claude (one-time)**
28
28
 
@@ -50,14 +50,14 @@ Global (`~/.config/opencode/opencode.json`) or project-level:
50
50
  }
51
51
  ```
52
52
 
53
+ The `apiKey` is a dummy value — authentication goes through your Claude Max session, not an API key. The `baseURL` points to the default proxy port (3456). If that port is already in use (e.g. another opencode instance), the plugin automatically starts the proxy on a different port and overrides the `baseURL` at runtime.
54
+
53
55
  **3. Run OpenCode**
54
56
 
55
57
  ```bash
56
58
  opencode
57
59
  ```
58
60
 
59
- That's it. The plugin handles everything.
60
-
61
61
  ### Option B: Standalone Installer (`oc` launcher)
62
62
 
63
63
  A one-liner that installs all dependencies and gives you the `oc` command — no config files to edit.
@@ -153,7 +153,7 @@ npm uninstall -g @anthropic-ai/claude-code opencode-ai opencode-claude-max-proxy
153
153
 
154
154
  | Variable | Default | Description |
155
155
  |----------|---------|-------------|
156
- | `CLAUDE_PROXY_PORT` | `3456` (plugin/Docker) / random (`oc`) | Port for the proxy server |
156
+ | `CLAUDE_PROXY_PORT` | `3456` | Preferred port for the proxy (falls back to a random port if in use) |
157
157
  | `CLAUDE_PROXY_WORKDIR` | `$PWD` | Working directory for the proxy |
158
158
  | `OC_SKIP_AUTH_CHECK` | unset | Set to `1` to skip Claude auth check on `oc` launch |
159
159
  | `OC_AUTO_UPDATE` | unset | Set to `true` or `1` to auto-update components on Docker container start |
@@ -177,14 +177,8 @@ This opens a browser for OAuth. Your Claude Max subscription credentials are nee
177
177
  ### "Proxy failed to start"
178
178
 
179
179
  1. Check Claude auth: `claude auth status`
180
- 2. Check if the port is in use: `lsof -i :3456`
181
- 3. Try a different port: set `CLAUDE_PROXY_PORT=4567` and update `baseURL` in `opencode.json` to match
182
-
183
- ### "Proxy didn't become healthy within 10 seconds"
184
-
185
- The proxy takes a moment to initialize. If this persists:
186
- - Ensure `claude auth status` shows `loggedIn: true`
187
- - Check your internet connection
180
+ 2. Ensure your internet connection is working
181
+ 3. If using a manual port override, check if it's in use: `lsof -i :$CLAUDE_PROXY_PORT`
188
182
 
189
183
  ### Updating components
190
184
 
@@ -206,7 +200,9 @@ docker compose -f docker/docker-compose.yml build --no-cache && docker compose -
206
200
  ```
207
201
  opencode-with-claude/
208
202
  ├── src/
209
- └── index.ts # Plugin entry point
203
+ ├── index.ts # Plugin entry point
204
+ │ ├── proxy.ts # Proxy lifecycle management
205
+ │ └── logger.ts # Plugin logger
210
206
  ├── bin/
211
207
  │ └── oc # Standalone launcher
212
208
  ├── docker/
@@ -239,7 +235,7 @@ npm run build
239
235
 
240
236
  **Do I need an Anthropic API key?**
241
237
 
242
- No. The proxy authenticates through your Claude Max subscription via `claude login`. The `ANTHROPIC_API_KEY=dummy` value is just a placeholder that OpenCode requires — it's never actually used.
238
+ No. The proxy authenticates through your Claude Max subscription via `claude login`. The plugin automatically sets a dummy API key — it's never actually used for authentication.
243
239
 
244
240
  **What happens if my Claude Max subscription expires?**
245
241
 
@@ -251,7 +247,7 @@ The **plugin** is recommended if you already use OpenCode — it integrates with
251
247
 
252
248
  **Can I use this with multiple projects at the same time?**
253
249
 
254
- Yes. The `oc` launcher assigns a random port for each terminal session. The plugin uses a fixed port (`3456` by default), so configure `CLAUDE_PROXY_PORT` if running multiple instances.
250
+ Yes. The first instance uses port 3456 by default. Additional instances automatically fall back to a random OS-assigned port, so they all work simultaneously without any extra configuration.
255
251
 
256
252
  **Is this the same as using the Anthropic API?**
257
253
 
package/dist/index.d.ts CHANGED
@@ -4,12 +4,9 @@ import type { Plugin } from "@opencode-ai/plugin";
4
4
  *
5
5
  * On init:
6
6
  * 1. Verifies the Claude CLI is installed and authenticated
7
- * 2. Resolves the bundled claude-max-proxy binary
8
- * 3. Spawns the proxy on a local port
9
- * 4. Waits for the proxy to become healthy
10
- * 5. Registers cleanup handlers to kill the proxy on exit
11
- *
12
- * Requires provider config in opencode.json to route API traffic through the proxy:
13
- * "provider": { "anthropic": { "options": { "baseURL": "http://127.0.0.1:3456", "apiKey": "dummy" } } }
7
+ * 2. Starts the proxy (port 3456, or falls back to a random port if in use)
8
+ * 3. Registers cleanup handlers to stop the proxy on exit
9
+ * 4. Returns a `config` hook that injects the proxy's baseURL into
10
+ * the Anthropic provider so each opencode instance gets its own proxy.
14
11
  */
15
12
  export declare const ClaudeMaxPlugin: Plugin;
package/dist/index.js CHANGED
@@ -1,73 +1,25 @@
1
- import { spawn } from "child_process";
2
- import { createRequire } from "module";
3
- const DEFAULT_PORT = 3456;
4
- const HEALTH_TIMEOUT_MS = 10_000;
5
- const HEALTH_INTERVAL_MS = 100;
6
- /**
7
- * Resolve the claude-max-proxy binary from this package's bundled dependency.
8
- */
9
- function resolveProxyBin() {
10
- const require = createRequire(import.meta.url);
11
- const proxyPkgPath = require.resolve("opencode-claude-max-proxy/package.json");
12
- const proxyDir = proxyPkgPath.replace(/\/package\.json$/, "");
13
- const proxyPkg = require(proxyPkgPath);
14
- const binEntries = proxyPkg.bin;
15
- if (!binEntries || typeof binEntries !== "object") {
16
- throw new Error("Could not find claude-max-proxy binary in opencode-claude-max-proxy package");
17
- }
18
- const binPath = Object.values(binEntries)[0];
19
- if (!binPath) {
20
- throw new Error("claude-max-proxy package has no bin entry");
21
- }
22
- return `${proxyDir}/${binPath}`;
23
- }
24
- /**
25
- * Poll the proxy health endpoint until it responds OK or timeout.
26
- */
27
- async function waitForHealth(port, timeoutMs, proxy) {
28
- const start = Date.now();
29
- while (Date.now() - start < timeoutMs) {
30
- // Check if proxy process died
31
- if (proxy.exitCode !== null) {
32
- throw new Error("Claude Max proxy process exited unexpectedly. Is Claude authenticated? Run: claude login");
33
- }
34
- try {
35
- const res = await fetch(`http://127.0.0.1:${port}/health`);
36
- if (res.ok)
37
- return;
38
- }
39
- catch {
40
- // Not ready yet
41
- }
42
- await new Promise((r) => setTimeout(r, HEALTH_INTERVAL_MS));
43
- }
44
- throw new Error(`Claude Max proxy didn't become healthy within ${timeoutMs / 1000}s. Check: claude auth status`);
45
- }
1
+ import { createLogger } from "./logger.js";
2
+ import { registerCleanup, startProxy } from "./proxy.js";
46
3
  /**
47
4
  * OpenCode plugin that manages the Claude Max proxy lifecycle.
48
5
  *
49
6
  * On init:
50
7
  * 1. Verifies the Claude CLI is installed and authenticated
51
- * 2. Resolves the bundled claude-max-proxy binary
52
- * 3. Spawns the proxy on a local port
53
- * 4. Waits for the proxy to become healthy
54
- * 5. Registers cleanup handlers to kill the proxy on exit
55
- *
56
- * Requires provider config in opencode.json to route API traffic through the proxy:
57
- * "provider": { "anthropic": { "options": { "baseURL": "http://127.0.0.1:3456", "apiKey": "dummy" } } }
8
+ * 2. Starts the proxy (port 3456, or falls back to a random port if in use)
9
+ * 3. Registers cleanup handlers to stop the proxy on exit
10
+ * 4. Returns a `config` hook that injects the proxy's baseURL into
11
+ * the Anthropic provider so each opencode instance gets its own proxy.
58
12
  */
59
13
  export const ClaudeMaxPlugin = async ({ client, $, directory }) => {
60
- const log = (level, message) => client.app.log({
61
- body: { service: "opencode-with-claude", level, message },
62
- });
63
- // 1. Check claude CLI exists
14
+ const log = createLogger(client);
15
+ // 1. Verify Claude CLI is installed
64
16
  try {
65
- await $ `which claude`;
17
+ await $ `claude --version`;
66
18
  }
67
19
  catch {
68
20
  throw new Error("Claude Code CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
69
21
  }
70
- // 2. Check authentication
22
+ // 2. Verify authentication
71
23
  let authOutput;
72
24
  try {
73
25
  authOutput = await $ `claude auth status`.text();
@@ -79,59 +31,22 @@ export const ClaudeMaxPlugin = async ({ client, $, directory }) => {
79
31
  throw new Error("Claude not authenticated. Run: claude login");
80
32
  }
81
33
  await log("info", "Claude authentication verified");
82
- // 3. Resolve proxy binary (bundled dependency)
83
- let proxyBin;
84
- try {
85
- proxyBin = resolveProxyBin();
86
- }
87
- catch (err) {
88
- throw new Error(`Failed to resolve claude-max-proxy binary: ${err instanceof Error ? err.message : err}`);
89
- }
90
- // 4. Pick port
91
- const port = parseInt(process.env.CLAUDE_PROXY_PORT || "", 10) || DEFAULT_PORT;
92
- // 5. Spawn proxy
93
- await log("info", `Starting Claude Max proxy on port ${port}...`);
94
- const proxy = spawn(proxyBin, [], {
95
- env: {
96
- ...process.env,
97
- CLAUDE_PROXY_PORT: String(port),
98
- CLAUDE_PROXY_PASSTHROUGH: "1",
99
- CLAUDE_PROXY_WORKDIR: directory,
34
+ // 3. Start the proxy
35
+ const port = parseInt(process.env.CLAUDE_PROXY_PORT || "", 10) || undefined;
36
+ await log("info", "Starting Claude Max proxy...");
37
+ const proxy = await startProxy({ port, log });
38
+ const baseURL = `http://127.0.0.1:${proxy.port}`;
39
+ await log("info", `Claude Max proxy ready at ${baseURL}`);
40
+ // 4. Register cleanup handlers
41
+ registerCleanup(proxy);
42
+ // 5. Configure the Anthropic provider to route through the proxy
43
+ return {
44
+ async config(input) {
45
+ input.provider ??= {};
46
+ input.provider.anthropic ??= {};
47
+ input.provider.anthropic.options ??= {};
48
+ input.provider.anthropic.options.baseURL = baseURL;
49
+ input.provider.anthropic.options.apiKey = "claude-max-proxy";
100
50
  },
101
- stdio: "ignore",
102
- detached: false,
103
- });
104
- proxy.on("error", (err) => {
105
- log("error", `Proxy process error: ${err.message}`);
106
- });
107
- // 6. Wait for health
108
- try {
109
- await waitForHealth(port, HEALTH_TIMEOUT_MS, proxy);
110
- }
111
- catch (err) {
112
- // Kill the proxy if health check fails
113
- try {
114
- proxy.kill();
115
- }
116
- catch { }
117
- throw err;
118
- }
119
- await log("info", `Claude Max proxy ready on port ${port}`);
120
- // 7. Cleanup on exit
121
- let cleaned = false;
122
- const cleanup = () => {
123
- if (cleaned)
124
- return;
125
- cleaned = true;
126
- try {
127
- proxy.kill();
128
- }
129
- catch { }
130
51
  };
131
- process.on("exit", cleanup);
132
- process.on("SIGINT", cleanup);
133
- process.on("SIGTERM", cleanup);
134
- // No hooks needed -- proxy runs as a sidecar process.
135
- // Provider config in opencode.json routes API traffic through the proxy.
136
- return {};
137
52
  };
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export type LogLevel = "debug" | "info" | "warn" | "error";
3
+ export type LogFn = (level: LogLevel, message: string) => Promise<unknown>;
4
+ /**
5
+ * Create a logger bound to the plugin's client.
6
+ */
7
+ export declare function createLogger(client: Parameters<Plugin>[0]["client"]): LogFn;
package/dist/logger.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Create a logger bound to the plugin's client.
3
+ */
4
+ export function createLogger(client) {
5
+ return (level, message) => client.app.log({
6
+ body: { service: "opencode-with-claude", level, message },
7
+ });
8
+ }
@@ -0,0 +1,29 @@
1
+ import type { LogFn } from "./logger.js";
2
+ export interface StartProxyOptions {
3
+ port?: number;
4
+ log: LogFn;
5
+ }
6
+ export interface ProxyHandle {
7
+ port: number;
8
+ close(): Promise<void>;
9
+ }
10
+ /**
11
+ * Start the Claude Max proxy using the programmatic API.
12
+ *
13
+ * Tries the preferred port first (default 3456). If that port is already
14
+ * in use, falls back to port 0 so the OS assigns a free port. This keeps
15
+ * the port predictable for single-instance users while allowing multiple
16
+ * opencode instances to coexist without conflicts.
17
+ *
18
+ * The upstream proxy unconditionally writes `[PROXY]` lines to
19
+ * console.error, so we patch it for the duration of the call and
20
+ * redirect those messages through the plugin logger instead.
21
+ */
22
+ export declare function startProxy(opts: StartProxyOptions): Promise<ProxyHandle>;
23
+ /**
24
+ * Register cross-platform cleanup handlers that stop the proxy on exit.
25
+ *
26
+ * - `exit` and `SIGINT` work on all platforms.
27
+ * - `SIGTERM` is only available on POSIX systems.
28
+ */
29
+ export declare function registerCleanup(proxy: ProxyHandle): void;
package/dist/proxy.js ADDED
@@ -0,0 +1,91 @@
1
+ import { startProxyServer } from "opencode-claude-max-proxy";
2
+ const IS_WINDOWS = process.platform === "win32";
3
+ const DEFAULT_PORT = 3456;
4
+ /**
5
+ * Start the Claude Max proxy using the programmatic API.
6
+ *
7
+ * Tries the preferred port first (default 3456). If that port is already
8
+ * in use, falls back to port 0 so the OS assigns a free port. This keeps
9
+ * the port predictable for single-instance users while allowing multiple
10
+ * opencode instances to coexist without conflicts.
11
+ *
12
+ * The upstream proxy unconditionally writes `[PROXY]` lines to
13
+ * console.error, so we patch it for the duration of the call and
14
+ * redirect those messages through the plugin logger instead.
15
+ */
16
+ export async function startProxy(opts) {
17
+ const { port = DEFAULT_PORT, log } = opts;
18
+ const origError = console.error;
19
+ console.error = (...args) => {
20
+ const msg = args.map(String).join(" ");
21
+ if (msg.startsWith("[PROXY]")) {
22
+ void log("debug", msg);
23
+ return;
24
+ }
25
+ origError.apply(console, args);
26
+ };
27
+ const attempt = async (p) => {
28
+ try {
29
+ return await startProxyServer({
30
+ port: p,
31
+ host: "127.0.0.1",
32
+ silent: true,
33
+ });
34
+ }
35
+ catch (err) {
36
+ if (p !== 0 &&
37
+ err instanceof Error &&
38
+ "code" in err &&
39
+ err.code === "EADDRINUSE") {
40
+ await log("info", `Port ${p} in use, starting on a random port instead...`);
41
+ return startProxyServer({
42
+ port: 0,
43
+ host: "127.0.0.1",
44
+ silent: true,
45
+ });
46
+ }
47
+ throw err;
48
+ }
49
+ };
50
+ let proxy;
51
+ try {
52
+ proxy = await attempt(port);
53
+ }
54
+ catch (err) {
55
+ console.error = origError;
56
+ throw err;
57
+ }
58
+ const addr = proxy.server.address();
59
+ const actualPort = addr.port;
60
+ await log("info", `Claude Max proxy running on port ${actualPort}`);
61
+ return {
62
+ port: actualPort,
63
+ close: async () => {
64
+ console.error = origError;
65
+ await proxy.close();
66
+ },
67
+ };
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Process cleanup
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * Register cross-platform cleanup handlers that stop the proxy on exit.
74
+ *
75
+ * - `exit` and `SIGINT` work on all platforms.
76
+ * - `SIGTERM` is only available on POSIX systems.
77
+ */
78
+ export function registerCleanup(proxy) {
79
+ let cleaned = false;
80
+ const cleanup = () => {
81
+ if (cleaned)
82
+ return;
83
+ cleaned = true;
84
+ void proxy.close();
85
+ };
86
+ process.on("exit", cleanup);
87
+ process.on("SIGINT", cleanup);
88
+ if (!IS_WINDOWS) {
89
+ process.on("SIGTERM", cleanup);
90
+ }
91
+ }
package/package.json CHANGED
@@ -1,27 +1,20 @@
1
1
  {
2
2
  "name": "opencode-with-claude",
3
- "version": "1.0.0",
4
- "description": "OpenCode plugin to use your Claude Max subscription via local proxy",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "type": "module",
8
- "files": [
9
- "dist"
10
- ],
11
- "scripts": {
12
- "build": "tsc",
13
- "test": "./test/run.sh",
14
- "test:clean": "./test/run.sh --clean",
15
- "prepublishOnly": "npm run build"
16
- },
17
- "dependencies": {
18
- "opencode-claude-max-proxy": "latest"
3
+ "version": "1.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/ianjwhite99/opencode-with-claude.git"
19
7
  },
8
+ "main": "dist/index.js",
20
9
  "devDependencies": {
21
- "@opencode-ai/plugin": "latest",
10
+ "@opencode-ai/plugin": "^1.3.0",
22
11
  "@types/node": "^25.5.0",
23
- "typescript": "^5.0.0"
12
+ "typescript": "^5.9.3"
24
13
  },
14
+ "description": "OpenCode plugin to use your Claude Max subscription via local proxy",
15
+ "files": [
16
+ "dist"
17
+ ],
25
18
  "keywords": [
26
19
  "opencode",
27
20
  "opencode-plugin",
@@ -30,8 +23,15 @@
30
23
  "proxy"
31
24
  ],
32
25
  "license": "MIT",
33
- "repository": {
34
- "type": "git",
35
- "url": "https://github.com/ianjwhite99/opencode-with-claude.git"
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "test": "./test/run.sh",
29
+ "test:clean": "./test/run.sh --clean",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "type": "module",
33
+ "types": "dist/index.d.ts",
34
+ "dependencies": {
35
+ "opencode-claude-max-proxy": "^1.15.0"
36
36
  }
37
37
  }