opencode-claude-max-proxy 1.8.0 → 1.10.0
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 +49 -20
- package/bin/docker-auth.sh +77 -0
- package/bin/docker-entrypoint.sh +24 -0
- package/bin/oc.sh +62 -0
- package/package.json +1 -1
- package/src/proxy/server.ts +39 -4
- package/src/proxy/sessionStore.ts +92 -0
package/README.md
CHANGED
|
@@ -102,19 +102,54 @@ curl http://127.0.0.1:3456/health
|
|
|
102
102
|
|
|
103
103
|
### Connect OpenCode
|
|
104
104
|
|
|
105
|
+
#### Per-Terminal Launcher (recommended)
|
|
106
|
+
|
|
105
107
|
```bash
|
|
106
|
-
|
|
108
|
+
./bin/oc.sh
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
Each terminal gets its own proxy on a random port. No port conflicts, no concurrency crashes. The proxy starts automatically, connects OpenCode, and cleans up when you exit. Sessions resume across terminals via a shared session file.
|
|
112
|
+
|
|
113
|
+
Add to your shell config for easy access:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# ~/.zshrc or ~/.bashrc
|
|
117
|
+
alias oc='/path/to/opencode-claude-max-proxy/bin/oc.sh'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Shared Proxy
|
|
110
121
|
|
|
111
|
-
|
|
122
|
+
If you prefer a single long-running proxy:
|
|
112
123
|
|
|
113
124
|
```bash
|
|
114
|
-
#
|
|
115
|
-
|
|
125
|
+
# Terminal 1: start the proxy
|
|
126
|
+
CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
|
|
127
|
+
|
|
128
|
+
# Terminal 2+: connect OpenCode
|
|
129
|
+
ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `ANTHROPIC_API_KEY` can be any non-empty string — the proxy doesn't use it. Authentication is handled by your `claude login` session.
|
|
133
|
+
|
|
134
|
+
#### OpenCode Desktop / Config File
|
|
135
|
+
|
|
136
|
+
For OpenCode Desktop (or to avoid env vars), add the proxy to `~/.config/opencode/opencode.json`. Start the shared proxy in the background and Desktop connects automatically.
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"provider": {
|
|
141
|
+
"anthropic": {
|
|
142
|
+
"options": {
|
|
143
|
+
"baseURL": "http://127.0.0.1:3456",
|
|
144
|
+
"apiKey": "dummy"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
116
149
|
```
|
|
117
150
|
|
|
151
|
+
> **Tip:** Use the shared proxy with the supervisor for Desktop: `CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy`. Both Desktop and terminal instances share sessions via the file store.
|
|
152
|
+
|
|
118
153
|
## Modes
|
|
119
154
|
|
|
120
155
|
### Passthrough Mode (recommended)
|
|
@@ -161,6 +196,7 @@ The proxy tracks SDK session IDs and resumes conversations on follow-up requests
|
|
|
161
196
|
|
|
162
197
|
- **Faster responses** — no re-processing of conversation history
|
|
163
198
|
- **Better context** — the SDK remembers tool results from previous turns
|
|
199
|
+
- **Works across terminals** — sessions are shared via a file store at `~/.cache/opencode-claude-max-proxy/sessions.json`
|
|
164
200
|
|
|
165
201
|
Session tracking works two ways:
|
|
166
202
|
|
|
@@ -171,7 +207,7 @@ Session tracking works two ways:
|
|
|
171
207
|
|
|
172
208
|
2. **Fingerprint-based** (automatic fallback) — hashes the first user message to identify returning conversations
|
|
173
209
|
|
|
174
|
-
Sessions are cached for 24 hours.
|
|
210
|
+
Sessions are cached for 24 hours. When using per-terminal proxies (`oc.sh`), the shared file store ensures a session started in one terminal can be resumed from another.
|
|
175
211
|
|
|
176
212
|
## Configuration
|
|
177
213
|
|
|
@@ -186,26 +222,19 @@ Sessions are cached for 24 hours.
|
|
|
186
222
|
|
|
187
223
|
## Concurrency
|
|
188
224
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
**Use the auto-restart supervisor** (recommended):
|
|
225
|
+
**Per-terminal proxies (`oc.sh`)** are the recommended approach for multiple terminals. Each OpenCode instance gets its own proxy — no concurrency issues at all.
|
|
192
226
|
|
|
193
|
-
|
|
194
|
-
CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
|
|
195
|
-
# or directly:
|
|
196
|
-
CLAUDE_PROXY_PASSTHROUGH=1 ./bin/claude-proxy-supervisor.sh
|
|
197
|
-
```
|
|
227
|
+
**Shared proxy** supports concurrent requests but has a known limitation:
|
|
198
228
|
|
|
199
229
|
> **⚠️ Known Issue: Bun SSE Crash ([oven-sh/bun#17947](https://github.com/oven-sh/bun/issues/17947))**
|
|
200
230
|
>
|
|
201
|
-
> The Claude Agent SDK's `cli.js` subprocess
|
|
231
|
+
> The Claude Agent SDK's `cli.js` subprocess (compiled with Bun) has a known segfault during cleanup of concurrent streaming responses.
|
|
202
232
|
>
|
|
203
|
-
> **
|
|
204
|
-
> - **
|
|
205
|
-
> - **
|
|
206
|
-
> - **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.
|
|
233
|
+
> - **All responses are delivered correctly** — the crash only occurs after responses complete
|
|
234
|
+
> - **The supervisor auto-restarts** in ~1-3 seconds
|
|
235
|
+
> - **Per-terminal proxies avoid this entirely** — no concurrency, no crash
|
|
207
236
|
>
|
|
208
|
-
> We are monitoring the upstream Bun issue for a fix.
|
|
237
|
+
> We are monitoring the upstream Bun issue for a fix.
|
|
209
238
|
|
|
210
239
|
## Model Mapping
|
|
211
240
|
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
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
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
package/src/proxy/server.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
|
|
88
|
+
// No session ID — use fingerprint fallback
|
|
71
89
|
const fp = getConversationFingerprint(messages)
|
|
72
|
-
if (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)
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
/** Extract only the last user message (for resume — SDK already has history) */
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
24
|
+
|
|
25
|
+
function getStorePath(): string {
|
|
26
|
+
const dir = process.env.CLAUDE_PROXY_SESSION_DIR
|
|
27
|
+
|| join(homedir(), ".cache", "opencode-claude-max-proxy")
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true })
|
|
30
|
+
}
|
|
31
|
+
return join(dir, "sessions.json")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readStore(): Record<string, StoredSession> {
|
|
35
|
+
const path = getStorePath()
|
|
36
|
+
if (!existsSync(path)) return {}
|
|
37
|
+
try {
|
|
38
|
+
const data = readFileSync(path, "utf-8")
|
|
39
|
+
const store = JSON.parse(data) as Record<string, StoredSession>
|
|
40
|
+
// Prune expired entries
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
const pruned: Record<string, StoredSession> = {}
|
|
43
|
+
for (const [key, session] of Object.entries(store)) {
|
|
44
|
+
if (now - session.lastUsedAt < SESSION_TTL_MS) {
|
|
45
|
+
pruned[key] = session
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return pruned
|
|
49
|
+
} catch {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeStore(store: Record<string, StoredSession>): void {
|
|
55
|
+
const path = getStorePath()
|
|
56
|
+
const tmp = path + ".tmp"
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2))
|
|
59
|
+
renameSync(tmp, path) // atomic write
|
|
60
|
+
} catch {
|
|
61
|
+
// If rename fails, try direct write
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(path, JSON.stringify(store, null, 2))
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function lookupSharedSession(key: string): StoredSession | undefined {
|
|
69
|
+
const store = readStore()
|
|
70
|
+
const session = store[key]
|
|
71
|
+
if (!session) return undefined
|
|
72
|
+
if (Date.now() - session.lastUsedAt >= SESSION_TTL_MS) return undefined
|
|
73
|
+
return session
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function storeSharedSession(key: string, claudeSessionId: string): void {
|
|
77
|
+
const store = readStore()
|
|
78
|
+
const existing = store[key]
|
|
79
|
+
store[key] = {
|
|
80
|
+
claudeSessionId,
|
|
81
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
82
|
+
lastUsedAt: Date.now(),
|
|
83
|
+
}
|
|
84
|
+
writeStore(store)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clearSharedSessions(): void {
|
|
88
|
+
const path = getStorePath()
|
|
89
|
+
try {
|
|
90
|
+
writeFileSync(path, "{}")
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|