opencode-claude-max-proxy 1.11.2 → 1.12.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 +26 -24
- package/dist/cli-s4sk7eks.js +13102 -0
- package/dist/cli.js +30 -0
- package/dist/server.js +10 -0
- package/package.json +20 -17
- package/bin/claude-proxy-supervisor.sh +0 -62
- package/bin/claude-proxy.ts +0 -33
- package/bin/docker-auth.sh +0 -77
- package/bin/docker-entrypoint.sh +0 -24
- package/bin/oc.sh +0 -62
- package/src/logger.ts +0 -72
- package/src/mcpTools.ts +0 -185
- package/src/plugin/claude-max-headers.ts +0 -52
- package/src/proxy/agentDefs.ts +0 -102
- package/src/proxy/agentMatch.ts +0 -93
- package/src/proxy/passthroughTools.ts +0 -108
- package/src/proxy/server.ts +0 -1172
- package/src/proxy/sessionStore.ts +0 -94
- package/src/proxy/types.ts +0 -13
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
startProxyServer
|
|
4
|
+
} from "./cli-s4sk7eks.js";
|
|
5
|
+
|
|
6
|
+
// bin/cli.ts
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
process.on("uncaughtException", (err) => {
|
|
9
|
+
console.error(`[PROXY] Uncaught exception (recovered): ${err.message}`);
|
|
10
|
+
});
|
|
11
|
+
process.on("unhandledRejection", (reason) => {
|
|
12
|
+
console.error(`[PROXY] Unhandled rejection (recovered): ${reason instanceof Error ? reason.message : reason}`);
|
|
13
|
+
});
|
|
14
|
+
var port = parseInt(process.env.CLAUDE_PROXY_PORT || "3456", 10);
|
|
15
|
+
var host = process.env.CLAUDE_PROXY_HOST || "127.0.0.1";
|
|
16
|
+
var idleTimeoutSeconds = parseInt(process.env.CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS || "120", 10);
|
|
17
|
+
try {
|
|
18
|
+
const authJson = execSync("claude auth status", { encoding: "utf-8", timeout: 5000 });
|
|
19
|
+
const auth = JSON.parse(authJson);
|
|
20
|
+
if (!auth.loggedIn) {
|
|
21
|
+
console.error("\x1B[31m✗ Not logged in to Claude.\x1B[0m Run: claude login");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (auth.subscriptionType !== "max") {
|
|
25
|
+
console.error(`\x1B[33m⚠ Claude subscription: ${auth.subscriptionType || "unknown"} (Max recommended)\x1B[0m`);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
console.error("\x1B[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1B[0m");
|
|
29
|
+
}
|
|
30
|
+
await startProxyServer({ port, host, idleTimeoutSeconds });
|
package/dist/server.js
ADDED
package/package.json
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-max-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "Use your Claude Max subscription with OpenCode via proxy server",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"claude-max-proxy": "./
|
|
8
|
+
"claude-max-proxy": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".": "./
|
|
11
|
+
".": "./dist/server.js"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
12
15
|
},
|
|
13
16
|
"scripts": {
|
|
14
17
|
"start": "./bin/claude-proxy-supervisor.sh",
|
|
15
18
|
"proxy": "./bin/claude-proxy-supervisor.sh",
|
|
19
|
+
"build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js'",
|
|
20
|
+
"postbuild": "node --check dist/cli.js && node --check dist/server.js",
|
|
21
|
+
"prepublishOnly": "bun run build",
|
|
16
22
|
"test": "bun test",
|
|
17
23
|
"typecheck": "tsc --noEmit",
|
|
18
|
-
"proxy:direct": "bun run ./bin/
|
|
24
|
+
"proxy:direct": "bun run ./bin/cli.ts"
|
|
19
25
|
},
|
|
20
26
|
"dependencies": {
|
|
21
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.80"
|
|
22
|
-
"glob": "^13.0.0",
|
|
23
|
-
"hono": "^4.11.4",
|
|
24
|
-
"p-queue": "^8.0.1"
|
|
27
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.80"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
27
|
-
"@
|
|
30
|
+
"@hono/node-server": "^1.19.11",
|
|
31
|
+
"@types/bun": "^1.3.11",
|
|
28
32
|
"@types/node": "^22.0.0",
|
|
33
|
+
"glob": "^13.0.0",
|
|
34
|
+
"hono": "^4.11.4",
|
|
29
35
|
"typescript": "^5.8.2"
|
|
30
36
|
},
|
|
31
37
|
"files": [
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"src/logger.ts",
|
|
35
|
-
"src/mcpTools.ts",
|
|
36
|
-
"README.md",
|
|
37
|
-
"src/plugin/"
|
|
38
|
+
"dist/",
|
|
39
|
+
"README.md"
|
|
38
40
|
],
|
|
39
41
|
"keywords": [
|
|
40
42
|
"opencode",
|
|
@@ -61,5 +63,6 @@
|
|
|
61
63
|
},
|
|
62
64
|
"author": "rynfar",
|
|
63
65
|
"license": "MIT",
|
|
64
|
-
"private": false
|
|
66
|
+
"private": false,
|
|
67
|
+
"packageManager": "bun@1.3.11"
|
|
65
68
|
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Auto-restart supervisor for claude-max-proxy
|
|
3
|
-
#
|
|
4
|
-
# The Claude Agent SDK's cli.js subprocess (compiled with Bun) can crash
|
|
5
|
-
# during cleanup of concurrent streaming responses — a known Bun bug
|
|
6
|
-
# (oven-sh/bun#17947). All responses are delivered correctly; the crash
|
|
7
|
-
# only occurs after response completion.
|
|
8
|
-
#
|
|
9
|
-
# This supervisor runs the proxy in a subshell with signal isolation,
|
|
10
|
-
# detects crashes, and restarts in ~1 second.
|
|
11
|
-
|
|
12
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
-
cd "$SCRIPT_DIR/.."
|
|
14
|
-
|
|
15
|
-
# Ignore signals that the child's crash might propagate
|
|
16
|
-
trap '' SIGPIPE
|
|
17
|
-
|
|
18
|
-
RESTART_COUNT=0
|
|
19
|
-
MAX_RAPID_RESTARTS=50
|
|
20
|
-
RAPID_WINDOW=60
|
|
21
|
-
LAST_START=0
|
|
22
|
-
|
|
23
|
-
while true; do
|
|
24
|
-
NOW=$(date +%s)
|
|
25
|
-
|
|
26
|
-
if [ $((NOW - LAST_START)) -gt $RAPID_WINDOW ]; then
|
|
27
|
-
RESTART_COUNT=0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
if [ $RESTART_COUNT -ge $MAX_RAPID_RESTARTS ]; then
|
|
31
|
-
echo "[supervisor] Too many restarts ($RESTART_COUNT in ${RAPID_WINDOW}s). Stopping."
|
|
32
|
-
exit 1
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
LAST_START=$NOW
|
|
36
|
-
RESTART_COUNT=$((RESTART_COUNT + 1))
|
|
37
|
-
|
|
38
|
-
if [ $RESTART_COUNT -gt 1 ]; then
|
|
39
|
-
echo "[supervisor] Restarting proxy (restart #$RESTART_COUNT)..."
|
|
40
|
-
else
|
|
41
|
-
echo "[supervisor] Starting proxy..."
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# Run in subshell so crashes don't kill the supervisor
|
|
45
|
-
(exec bun run ./bin/claude-proxy.ts)
|
|
46
|
-
EXIT_CODE=$?
|
|
47
|
-
|
|
48
|
-
if [ $EXIT_CODE -eq 0 ]; then
|
|
49
|
-
echo "[supervisor] Proxy exited cleanly."
|
|
50
|
-
break
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
# Signal-based exits (128+signal)
|
|
54
|
-
if [ $EXIT_CODE -gt 128 ]; then
|
|
55
|
-
SIG=$((EXIT_CODE - 128))
|
|
56
|
-
echo "[supervisor] Proxy killed by signal $SIG. Restarting in 1s..."
|
|
57
|
-
else
|
|
58
|
-
echo "[supervisor] Proxy exited (code $EXIT_CODE). Restarting in 1s..."
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
sleep 1
|
|
62
|
-
done
|
package/bin/claude-proxy.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { startProxyServer } from "../src/proxy/server"
|
|
4
|
-
import { execSync } from "child_process"
|
|
5
|
-
|
|
6
|
-
// Prevent SDK subprocess crashes from killing the proxy
|
|
7
|
-
process.on("uncaughtException", (err) => {
|
|
8
|
-
console.error(`[PROXY] Uncaught exception (recovered): ${err.message}`)
|
|
9
|
-
})
|
|
10
|
-
process.on("unhandledRejection", (reason) => {
|
|
11
|
-
console.error(`[PROXY] Unhandled rejection (recovered): ${reason instanceof Error ? reason.message : reason}`)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const port = parseInt(process.env.CLAUDE_PROXY_PORT || "3456", 10)
|
|
15
|
-
const host = process.env.CLAUDE_PROXY_HOST || "127.0.0.1"
|
|
16
|
-
const idleTimeoutSeconds = parseInt(process.env.CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS || "120", 10)
|
|
17
|
-
|
|
18
|
-
// Pre-flight auth check
|
|
19
|
-
try {
|
|
20
|
-
const authJson = execSync("claude auth status", { encoding: "utf-8", timeout: 5000 })
|
|
21
|
-
const auth = JSON.parse(authJson)
|
|
22
|
-
if (!auth.loggedIn) {
|
|
23
|
-
console.error("\x1b[31m✗ Not logged in to Claude.\x1b[0m Run: claude login")
|
|
24
|
-
process.exit(1)
|
|
25
|
-
}
|
|
26
|
-
if (auth.subscriptionType !== "max") {
|
|
27
|
-
console.error(`\x1b[33m⚠ Claude subscription: ${auth.subscriptionType || "unknown"} (Max recommended)\x1b[0m`)
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
console.error("\x1b[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1b[0m")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
await startProxyServer({ port, host, idleTimeoutSeconds })
|
package/bin/docker-auth.sh
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Copy Claude credentials from host into Docker container.
|
|
3
|
-
#
|
|
4
|
-
# macOS stores scopes in Keychain, so the credentials file has scopes: ""
|
|
5
|
-
# Linux Claude CLI needs scopes as an array. This script fixes the format.
|
|
6
|
-
#
|
|
7
|
-
# Usage: ./bin/docker-auth.sh
|
|
8
|
-
|
|
9
|
-
set -e
|
|
10
|
-
|
|
11
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
-
cd "$SCRIPT_DIR/.."
|
|
13
|
-
|
|
14
|
-
HOST_CREDS="$HOME/.claude/.credentials.json"
|
|
15
|
-
|
|
16
|
-
if [ ! -f "$HOST_CREDS" ]; then
|
|
17
|
-
echo "❌ No credentials found at $HOST_CREDS"
|
|
18
|
-
echo " Run 'claude login' on your host first."
|
|
19
|
-
exit 1
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
# Check if host is logged in
|
|
23
|
-
if ! claude auth status 2>/dev/null | grep -q '"loggedIn": true'; then
|
|
24
|
-
echo "❌ Not logged in on host. Run 'claude login' first."
|
|
25
|
-
exit 1
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
echo "📋 Reading credentials from host..."
|
|
29
|
-
|
|
30
|
-
# Fix the scopes field and copy into container
|
|
31
|
-
python3 -c "
|
|
32
|
-
import json, sys
|
|
33
|
-
|
|
34
|
-
with open('$HOST_CREDS') as f:
|
|
35
|
-
creds = json.load(f)
|
|
36
|
-
|
|
37
|
-
oauth = creds.get('claudeAiOauth', {})
|
|
38
|
-
if not oauth.get('accessToken'):
|
|
39
|
-
print('❌ No access token found in credentials')
|
|
40
|
-
sys.exit(1)
|
|
41
|
-
|
|
42
|
-
# Fix scopes: empty string -> proper array
|
|
43
|
-
if not oauth.get('scopes') or oauth['scopes'] == '':
|
|
44
|
-
oauth['scopes'] = [
|
|
45
|
-
'user:profile',
|
|
46
|
-
'user:inference',
|
|
47
|
-
'user:sessions:claude_code',
|
|
48
|
-
'user:mcp_servers',
|
|
49
|
-
'user:file_upload'
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
creds['claudeAiOauth'] = oauth
|
|
53
|
-
print(json.dumps(creds))
|
|
54
|
-
" | docker compose exec -T proxy bash -c 'cat > /home/claude/.claude/.credentials.json'
|
|
55
|
-
|
|
56
|
-
if [ $? -ne 0 ]; then
|
|
57
|
-
echo "❌ Failed to copy credentials. Is the container running?"
|
|
58
|
-
echo " Run 'docker compose up -d' first."
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
# Also copy .claude.json if it exists
|
|
63
|
-
if [ -f "$HOME/.claude/.claude.json" ]; then
|
|
64
|
-
docker compose exec -T proxy bash -c 'cat > /home/claude/.claude/.claude.json' < "$HOME/.claude/.claude.json"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# Verify
|
|
68
|
-
echo "🔍 Verifying..."
|
|
69
|
-
AUTH=$(docker compose exec proxy claude auth status 2>&1)
|
|
70
|
-
if echo "$AUTH" | grep -q '"loggedIn": true'; then
|
|
71
|
-
EMAIL=$(echo "$AUTH" | grep -o '"email": "[^"]*"' | head -1)
|
|
72
|
-
echo "✅ Docker container authenticated! $EMAIL"
|
|
73
|
-
else
|
|
74
|
-
echo "❌ Authentication failed inside container"
|
|
75
|
-
echo "$AUTH"
|
|
76
|
-
exit 1
|
|
77
|
-
fi
|
package/bin/docker-entrypoint.sh
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Docker entrypoint:
|
|
3
|
-
# 1. Fix volume permissions (created as root, need claude ownership)
|
|
4
|
-
# 2. Symlink .claude.json into persistent volume
|
|
5
|
-
|
|
6
|
-
CLAUDE_DIR="/home/claude/.claude"
|
|
7
|
-
CLAUDE_JSON="/home/claude/.claude.json"
|
|
8
|
-
CLAUDE_JSON_VOL="$CLAUDE_DIR/.claude.json"
|
|
9
|
-
|
|
10
|
-
# Fix ownership if volume was created as root
|
|
11
|
-
if [ -d "$CLAUDE_DIR" ] && [ ! -w "$CLAUDE_DIR" ]; then
|
|
12
|
-
echo "[entrypoint] Fixing volume permissions..."
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
# Symlink .claude.json into volume so it persists across restarts
|
|
16
|
-
if [ -f "$CLAUDE_JSON_VOL" ] && [ ! -f "$CLAUDE_JSON" ]; then
|
|
17
|
-
ln -sf "$CLAUDE_JSON_VOL" "$CLAUDE_JSON"
|
|
18
|
-
elif [ -f "$CLAUDE_JSON" ] && [ ! -L "$CLAUDE_JSON" ] && [ -w "$CLAUDE_DIR" ]; then
|
|
19
|
-
cp "$CLAUDE_JSON" "$CLAUDE_JSON_VOL" 2>/dev/null
|
|
20
|
-
rm -f "$CLAUDE_JSON"
|
|
21
|
-
ln -sf "$CLAUDE_JSON_VOL" "$CLAUDE_JSON"
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
exec "$@"
|
package/bin/oc.sh
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
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/src/logger.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks"
|
|
2
|
-
|
|
3
|
-
type LogFields = Record<string, unknown>
|
|
4
|
-
|
|
5
|
-
const contextStore = new AsyncLocalStorage<LogFields>()
|
|
6
|
-
|
|
7
|
-
const shouldLog = () => process.env["OPENCODE_CLAUDE_PROVIDER_DEBUG"]
|
|
8
|
-
const shouldLogStreamDebug = () => process.env["OPENCODE_CLAUDE_PROVIDER_STREAM_DEBUG"]
|
|
9
|
-
|
|
10
|
-
const isVerboseStreamEvent = (event: string): boolean => {
|
|
11
|
-
return event.startsWith("stream.") || event === "response.empty_stream"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const REDACTED_KEYS = new Set([
|
|
15
|
-
"authorization",
|
|
16
|
-
"cookie",
|
|
17
|
-
"x-api-key",
|
|
18
|
-
"apiKey",
|
|
19
|
-
"apikey",
|
|
20
|
-
"prompt",
|
|
21
|
-
"messages",
|
|
22
|
-
"content"
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
const sanitize = (value: unknown): unknown => {
|
|
26
|
-
if (value === null || value === undefined) return value
|
|
27
|
-
|
|
28
|
-
if (typeof value === "string") {
|
|
29
|
-
if (value.length > 512) {
|
|
30
|
-
return `${value.slice(0, 512)}... [truncated=${value.length}]`
|
|
31
|
-
}
|
|
32
|
-
return value
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (Array.isArray(value)) {
|
|
36
|
-
return value.map(sanitize)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (typeof value === "object") {
|
|
40
|
-
const out: Record<string, unknown> = {}
|
|
41
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
42
|
-
if (REDACTED_KEYS.has(k)) {
|
|
43
|
-
if (typeof v === "string") {
|
|
44
|
-
out[k] = `[redacted len=${v.length}]`
|
|
45
|
-
} else if (Array.isArray(v)) {
|
|
46
|
-
out[k] = `[redacted array len=${v.length}]`
|
|
47
|
-
} else {
|
|
48
|
-
out[k] = "[redacted]"
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
out[k] = sanitize(v)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return out
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return value
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const withClaudeLogContext = <T>(context: LogFields, fn: () => T): T => {
|
|
61
|
-
return contextStore.run(context, fn)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const claudeLog = (event: string, extra?: LogFields) => {
|
|
65
|
-
if (!shouldLog()) return
|
|
66
|
-
if (isVerboseStreamEvent(event) && !shouldLogStreamDebug()) return
|
|
67
|
-
|
|
68
|
-
const context = contextStore.getStore() || {}
|
|
69
|
-
const payload = sanitize({ ts: new Date().toISOString(), event, ...context, ...(extra || {}) })
|
|
70
|
-
|
|
71
|
-
console.debug(`[opencode-claude-code-provider] ${JSON.stringify(payload)}`)
|
|
72
|
-
}
|
package/src/mcpTools.ts
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
-
import { z } from "zod"
|
|
3
|
-
import * as fs from "node:fs/promises"
|
|
4
|
-
import * as path from "node:path"
|
|
5
|
-
import { exec } from "node:child_process"
|
|
6
|
-
import { promisify } from "node:util"
|
|
7
|
-
import { glob as globLib } from "glob"
|
|
8
|
-
|
|
9
|
-
const execAsync = promisify(exec)
|
|
10
|
-
|
|
11
|
-
const getCwd = () => process.env.CLAUDE_PROXY_WORKDIR || process.cwd()
|
|
12
|
-
|
|
13
|
-
export const opencodeMcpServer = createSdkMcpServer({
|
|
14
|
-
name: "opencode",
|
|
15
|
-
version: "1.0.0",
|
|
16
|
-
tools: [
|
|
17
|
-
tool(
|
|
18
|
-
"read",
|
|
19
|
-
"Read the contents of a file at the specified path",
|
|
20
|
-
{
|
|
21
|
-
path: z.string().describe("Absolute or relative path to the file"),
|
|
22
|
-
encoding: z.string().optional().describe("File encoding, defaults to utf-8")
|
|
23
|
-
},
|
|
24
|
-
async (args) => {
|
|
25
|
-
try {
|
|
26
|
-
const filePath = path.isAbsolute(args.path)
|
|
27
|
-
? args.path
|
|
28
|
-
: path.resolve(getCwd(), args.path)
|
|
29
|
-
const content = await fs.readFile(filePath, (args.encoding || "utf-8") as BufferEncoding)
|
|
30
|
-
return {
|
|
31
|
-
content: [{ type: "text", text: content }]
|
|
32
|
-
}
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return {
|
|
35
|
-
content: [{ type: "text", text: `Error reading file: ${error instanceof Error ? error.message : String(error)}` }],
|
|
36
|
-
isError: true
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
),
|
|
41
|
-
|
|
42
|
-
tool(
|
|
43
|
-
"write",
|
|
44
|
-
"Write content to a file, creating directories if needed",
|
|
45
|
-
{
|
|
46
|
-
path: z.string().describe("Path to write to"),
|
|
47
|
-
content: z.string().describe("Content to write")
|
|
48
|
-
},
|
|
49
|
-
async (args) => {
|
|
50
|
-
try {
|
|
51
|
-
const filePath = path.isAbsolute(args.path)
|
|
52
|
-
? args.path
|
|
53
|
-
: path.resolve(getCwd(), args.path)
|
|
54
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
55
|
-
await fs.writeFile(filePath, args.content, "utf-8")
|
|
56
|
-
return {
|
|
57
|
-
content: [{ type: "text", text: `Successfully wrote to ${args.path}` }]
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
return {
|
|
61
|
-
content: [{ type: "text", text: `Error writing file: ${error instanceof Error ? error.message : String(error)}` }],
|
|
62
|
-
isError: true
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
),
|
|
67
|
-
|
|
68
|
-
tool(
|
|
69
|
-
"edit",
|
|
70
|
-
"Edit a file by replacing oldString with newString",
|
|
71
|
-
{
|
|
72
|
-
path: z.string().describe("Path to the file to edit"),
|
|
73
|
-
oldString: z.string().describe("The text to replace"),
|
|
74
|
-
newString: z.string().describe("The replacement text")
|
|
75
|
-
},
|
|
76
|
-
async (args) => {
|
|
77
|
-
try {
|
|
78
|
-
const filePath = path.isAbsolute(args.path)
|
|
79
|
-
? args.path
|
|
80
|
-
: path.resolve(getCwd(), args.path)
|
|
81
|
-
const content = await fs.readFile(filePath, "utf-8")
|
|
82
|
-
if (!content.includes(args.oldString)) {
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text", text: `Error: oldString not found in file` }],
|
|
85
|
-
isError: true
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const newContent = content.replace(args.oldString, args.newString)
|
|
89
|
-
await fs.writeFile(filePath, newContent, "utf-8")
|
|
90
|
-
return {
|
|
91
|
-
content: [{ type: "text", text: `Successfully edited ${args.path}` }]
|
|
92
|
-
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
return {
|
|
95
|
-
content: [{ type: "text", text: `Error editing file: ${error instanceof Error ? error.message : String(error)}` }],
|
|
96
|
-
isError: true
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
),
|
|
101
|
-
|
|
102
|
-
tool(
|
|
103
|
-
"bash",
|
|
104
|
-
"Execute a bash command and return the output",
|
|
105
|
-
{
|
|
106
|
-
command: z.string().describe("The command to execute"),
|
|
107
|
-
cwd: z.string().optional().describe("Working directory for the command")
|
|
108
|
-
},
|
|
109
|
-
async (args) => {
|
|
110
|
-
try {
|
|
111
|
-
const options = {
|
|
112
|
-
cwd: args.cwd || getCwd(),
|
|
113
|
-
timeout: 120000
|
|
114
|
-
}
|
|
115
|
-
const { stdout, stderr } = await execAsync(args.command, options)
|
|
116
|
-
const output = stdout || stderr || "(no output)"
|
|
117
|
-
return {
|
|
118
|
-
content: [{ type: "text", text: output }]
|
|
119
|
-
}
|
|
120
|
-
} catch (error: unknown) {
|
|
121
|
-
const execError = error as { stdout?: string; stderr?: string; message?: string }
|
|
122
|
-
const output = execError.stdout || execError.stderr || execError.message || String(error)
|
|
123
|
-
return {
|
|
124
|
-
content: [{ type: "text", text: output }],
|
|
125
|
-
isError: true
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
),
|
|
130
|
-
|
|
131
|
-
tool(
|
|
132
|
-
"glob",
|
|
133
|
-
"Find files matching a glob pattern",
|
|
134
|
-
{
|
|
135
|
-
pattern: z.string().describe("Glob pattern like **/*.ts"),
|
|
136
|
-
cwd: z.string().optional().describe("Base directory for the search")
|
|
137
|
-
},
|
|
138
|
-
async (args) => {
|
|
139
|
-
try {
|
|
140
|
-
const files = await globLib(args.pattern, {
|
|
141
|
-
cwd: args.cwd || getCwd(),
|
|
142
|
-
nodir: true,
|
|
143
|
-
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
144
|
-
})
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: "text", text: files.join("\n") || "(no matches)" }]
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
151
|
-
isError: true
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
),
|
|
156
|
-
|
|
157
|
-
tool(
|
|
158
|
-
"grep",
|
|
159
|
-
"Search for a pattern in files",
|
|
160
|
-
{
|
|
161
|
-
pattern: z.string().describe("Regex pattern to search for"),
|
|
162
|
-
path: z.string().optional().describe("Directory or file to search in"),
|
|
163
|
-
include: z.string().optional().describe("File pattern to include, e.g., *.ts")
|
|
164
|
-
},
|
|
165
|
-
async (args) => {
|
|
166
|
-
try {
|
|
167
|
-
const searchPath = args.path || getCwd()
|
|
168
|
-
const includePattern = args.include || "*"
|
|
169
|
-
|
|
170
|
-
let cmd = `grep -rn --include="${includePattern}" "${args.pattern}" "${searchPath}" 2>/dev/null || true`
|
|
171
|
-
const { stdout } = await execAsync(cmd, { maxBuffer: 10 * 1024 * 1024 })
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
content: [{ type: "text", text: stdout || "(no matches)" }]
|
|
175
|
-
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return {
|
|
178
|
-
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
179
|
-
isError: true
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
)
|
|
184
|
-
]
|
|
185
|
-
})
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode plugin that injects session tracking headers into Anthropic API requests.
|
|
3
|
-
*
|
|
4
|
-
* This enables the claude-max-proxy to reliably track sessions and resume
|
|
5
|
-
* Claude Agent SDK conversations instead of starting fresh every time.
|
|
6
|
-
*
|
|
7
|
-
* Installation:
|
|
8
|
-
* Add to your opencode.json:
|
|
9
|
-
* { "plugin": ["./path/to/claude-max-headers.ts"] }
|
|
10
|
-
*
|
|
11
|
-
* Or copy to your project's .opencode/plugin/ directory.
|
|
12
|
-
*
|
|
13
|
-
* What it does:
|
|
14
|
-
* Adds x-opencode-session and x-opencode-request headers to requests
|
|
15
|
-
* sent to the Anthropic provider. The proxy uses these to map OpenCode
|
|
16
|
-
* sessions to Claude SDK sessions for conversation resumption.
|
|
17
|
-
*
|
|
18
|
-
* Without this plugin:
|
|
19
|
-
* The proxy falls back to fingerprint-based session matching (hashing
|
|
20
|
-
* the first user message). This works but is less reliable.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
type ChatHeadersHook = (
|
|
24
|
-
incoming: {
|
|
25
|
-
sessionID: string
|
|
26
|
-
agent: any
|
|
27
|
-
model: { providerID: string }
|
|
28
|
-
provider: any
|
|
29
|
-
message: { id: string }
|
|
30
|
-
},
|
|
31
|
-
output: { headers: Record<string, string> }
|
|
32
|
-
) => Promise<void>
|
|
33
|
-
|
|
34
|
-
type PluginHooks = {
|
|
35
|
-
"chat.headers"?: ChatHeadersHook
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type PluginFn = (input: any) => Promise<PluginHooks>
|
|
39
|
-
|
|
40
|
-
export const ClaudeMaxHeadersPlugin: PluginFn = async (_input) => {
|
|
41
|
-
return {
|
|
42
|
-
"chat.headers": async (incoming, output) => {
|
|
43
|
-
// Only inject headers for Anthropic provider requests
|
|
44
|
-
if (incoming.model.providerID !== "anthropic") return
|
|
45
|
-
|
|
46
|
-
output.headers["x-opencode-session"] = incoming.sessionID
|
|
47
|
-
output.headers["x-opencode-request"] = incoming.message.id
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default ClaudeMaxHeadersPlugin
|