@rynfar/meridian 1.24.5 → 1.25.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 +122 -196
- package/dist/cli-a05ws7rb.js +18 -0
- package/dist/{cli-jd4atcxs.js → cli-m9pfb7h9.js} +1 -18
- package/dist/cli-rtab0qa6.js +67 -0
- package/dist/{cli-9pc43rfa.js → cli-s6f9jefk.js} +106 -49
- package/dist/cli.js +39 -3
- package/dist/proxy/adapter.d.ts +1 -1
- package/dist/proxy/fileChanges.d.ts.map +1 -1
- package/dist/proxy/models.d.ts +17 -1
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/setup.d.ts +42 -0
- package/dist/proxy/setup.d.ts.map +1 -0
- package/dist/server.js +4 -2
- package/dist/setup-5x116vbs.js +13 -0
- package/dist/telemetry/dashboard.d.ts +1 -1
- package/dist/telemetry/dashboard.d.ts.map +1 -1
- package/dist/telemetry/routes.d.ts.map +1 -1
- package/dist/{tokenRefresh-wzn2bvrq.js → tokenRefresh-ywwpe8k2.js} +2 -1
- package/package.json +76 -75
- package/plugin/meridian.ts +54 -0
package/README.md
CHANGED
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic protocol — OpenCode, Crush, Cline,
|
|
15
|
-
|
|
16
|
-
Harness Claude, your way.
|
|
14
|
+
Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic protocol — OpenCode, Crush, Cline, Aider — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
|
|
17
15
|
|
|
18
16
|
> [!NOTE]
|
|
19
17
|
> **Renamed from `opencode-claude-max-proxy`.** If you're upgrading, see [`MIGRATION.md`](MIGRATION.md) for the checklist. Your existing sessions, env vars, and agent configs all continue to work.
|
|
@@ -21,17 +19,20 @@ Harness Claude, your way.
|
|
|
21
19
|
## Quick Start
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
# Install
|
|
22
|
+
# 1. Install
|
|
25
23
|
npm install -g @rynfar/meridian
|
|
26
24
|
|
|
27
|
-
# Authenticate (one time)
|
|
25
|
+
# 2. Authenticate (one time)
|
|
28
26
|
claude login
|
|
29
27
|
|
|
30
|
-
#
|
|
28
|
+
# 3. Configure OpenCode plugin (one time — OpenCode users only)
|
|
29
|
+
meridian setup
|
|
30
|
+
|
|
31
|
+
# 4. Start
|
|
31
32
|
meridian
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Meridian
|
|
35
|
+
Meridian runs on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at it:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
@@ -43,7 +44,7 @@ The API key value doesn't matter — Meridian authenticates through your Claude
|
|
|
43
44
|
|
|
44
45
|
You're paying for Claude Max. It includes programmatic access through the Claude Code SDK. But your favorite coding tools expect an Anthropic API endpoint and an API key.
|
|
45
46
|
|
|
46
|
-
Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
|
|
47
|
+
Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
|
|
47
48
|
|
|
48
49
|
<p align="center">
|
|
49
50
|
<img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
|
|
@@ -51,25 +52,45 @@ Meridian bridges that gap. It runs locally, accepts standard Anthropic API reque
|
|
|
51
52
|
|
|
52
53
|
## Features
|
|
53
54
|
|
|
54
|
-
- **Standard Anthropic API** — drop-in compatible with any tool that supports custom `base_url`
|
|
55
|
+
- **Standard Anthropic API** — drop-in compatible with any tool that supports a custom `base_url`
|
|
55
56
|
- **Session management** — conversations persist across requests, survive compaction and undo, resume after proxy restarts
|
|
56
57
|
- **Streaming** — full SSE streaming with MCP tool filtering
|
|
57
|
-
- **Concurrent sessions** — run parent
|
|
58
|
+
- **Concurrent sessions** — run parent and subagent requests in parallel
|
|
59
|
+
- **Subagent model selection** — primary agents get 1M context; subagents get 200k, preserving rate-limit budget
|
|
60
|
+
- **Auto token refresh** — expired OAuth tokens are refreshed automatically; requests continue without interruption
|
|
58
61
|
- **Passthrough mode** — forward tool calls to the client instead of executing internally
|
|
59
62
|
- **Multimodal** — images, documents, and file attachments pass through to Claude
|
|
60
63
|
- **Telemetry dashboard** — real-time performance metrics at `/telemetry`
|
|
61
|
-
- **Cross-proxy resume** — sessions persist to disk and survive restarts
|
|
62
|
-
- **Agent adapter pattern** — extensible architecture for supporting new agent protocols
|
|
63
64
|
|
|
64
65
|
## Agent Setup
|
|
65
66
|
|
|
66
67
|
### OpenCode
|
|
67
68
|
|
|
69
|
+
**Step 1: Run `meridian setup` (required, one time)**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
meridian setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This adds the Meridian plugin to your OpenCode global config (`~/.config/opencode/opencode.json`). The plugin enables:
|
|
76
|
+
|
|
77
|
+
- **Session tracking** — reliable conversation continuity across requests
|
|
78
|
+
- **Subagent model selection** — primary agents use `sonnet[1m]`; subagents automatically use `sonnet` (200k), preserving your 1M context rate-limit budget
|
|
79
|
+
|
|
80
|
+
If the plugin is missing, Meridian warns at startup and reports `"plugin": "not-configured"` in the health endpoint.
|
|
81
|
+
|
|
82
|
+
**Step 2: Start**
|
|
83
|
+
|
|
68
84
|
```bash
|
|
69
85
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
70
86
|
```
|
|
71
87
|
|
|
72
|
-
|
|
88
|
+
Or set these in your shell profile so they're always active:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export ANTHROPIC_API_KEY=x
|
|
92
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
93
|
+
```
|
|
73
94
|
|
|
74
95
|
### Crush
|
|
75
96
|
|
|
@@ -94,75 +115,38 @@ Add a provider to `~/.config/crush/crush.json`:
|
|
|
94
115
|
}
|
|
95
116
|
```
|
|
96
117
|
|
|
97
|
-
Then use Meridian models in Crush:
|
|
98
|
-
|
|
99
118
|
```bash
|
|
100
119
|
crush run --model meridian/claude-sonnet-4-6 "refactor this function"
|
|
101
120
|
crush --model meridian/claude-opus-4-6 # interactive TUI
|
|
102
121
|
```
|
|
103
122
|
|
|
104
|
-
Crush is automatically detected from its `Charm-Crush/` User-Agent — no
|
|
123
|
+
Crush is automatically detected from its `Charm-Crush/` User-Agent — no plugin needed.
|
|
105
124
|
|
|
106
125
|
### Droid (Factory AI)
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
**1. Add Meridian as a custom model provider** in `~/.factory/settings.json`:
|
|
127
|
+
Add Meridian as a custom model provider in `~/.factory/settings.json`:
|
|
111
128
|
|
|
112
129
|
```json
|
|
113
130
|
{
|
|
114
131
|
"customModels": [
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"provider": "anthropic",
|
|
119
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
120
|
-
"apiKey": "x"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"model": "claude-opus-4-6",
|
|
124
|
-
"name": "Opus 4.6 (1M — Meridian)",
|
|
125
|
-
"provider": "anthropic",
|
|
126
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
127
|
-
"apiKey": "x"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"model": "claude-haiku-4-5-20251001",
|
|
131
|
-
"name": "Haiku 4.5 (Meridian)",
|
|
132
|
-
"provider": "anthropic",
|
|
133
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
134
|
-
"apiKey": "x"
|
|
135
|
-
}
|
|
132
|
+
{ "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
133
|
+
{ "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
134
|
+
{ "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
|
|
136
135
|
]
|
|
137
136
|
}
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
**2. In the Droid TUI**, open the model selector (`/model`) and choose any `custom:claude-*` model.
|
|
143
|
-
|
|
144
|
-
**How models map to Claude Max tiers:**
|
|
145
|
-
|
|
146
|
-
| Model name in config | Claude Max tier |
|
|
147
|
-
|---|---|
|
|
148
|
-
| `claude-sonnet-4-6` | `sonnet[1m]` — Sonnet 4.6 with 1M context |
|
|
149
|
-
| `claude-opus-4-6` | `opus[1m]` — Opus 4.6 with 1M context |
|
|
150
|
-
| `claude-haiku-4-5-20251001` | `haiku` — Haiku 4.5 |
|
|
151
|
-
| `claude-sonnet-4-5-*` | `sonnet` — Sonnet 4.5, no extended context |
|
|
152
|
-
|
|
153
|
-
> **Note:** Droid automatically uses Meridian's internal tool execution mode regardless of the global `CLAUDE_PROXY_PASSTHROUGH` setting. No extra configuration needed.
|
|
139
|
+
Then pick any `custom:claude-*` model in the Droid TUI. No plugin needed — Droid is automatically detected.
|
|
154
140
|
|
|
155
141
|
### Cline
|
|
156
142
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
**1. Authenticate Cline with the Anthropic provider:**
|
|
143
|
+
**1. Authenticate:**
|
|
160
144
|
|
|
161
145
|
```bash
|
|
162
146
|
cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
163
147
|
```
|
|
164
148
|
|
|
165
|
-
**2.
|
|
149
|
+
**2. Set the proxy URL** in `~/.cline/data/globalState.json`:
|
|
166
150
|
|
|
167
151
|
```json
|
|
168
152
|
{
|
|
@@ -172,81 +156,73 @@ cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
|
172
156
|
}
|
|
173
157
|
```
|
|
174
158
|
|
|
175
|
-
**3. Run
|
|
159
|
+
**3. Run:**
|
|
176
160
|
|
|
177
161
|
```bash
|
|
178
|
-
cline --yolo "refactor the login function"
|
|
179
|
-
cline --yolo --model claude-opus-4-6 "review this codebase" # opus
|
|
180
|
-
cline --yolo --model claude-haiku-4-5-20251001 "quick question" # haiku (fastest)
|
|
162
|
+
cline --yolo "refactor the login function"
|
|
181
163
|
```
|
|
182
164
|
|
|
183
|
-
No
|
|
165
|
+
No plugin needed — Cline uses the standard Anthropic SDK.
|
|
184
166
|
|
|
185
167
|
### Aider
|
|
186
168
|
|
|
187
|
-
Aider works out of the box — no plugin or config file needed:
|
|
188
|
-
|
|
189
169
|
```bash
|
|
190
170
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
|
|
191
171
|
aider --model anthropic/claude-sonnet-4-5-20250929
|
|
192
172
|
```
|
|
193
173
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
> **Note:** Aider's `--no-stream` flag is incompatible due to a litellm parsing issue — use the default streaming mode (no flag needed).
|
|
174
|
+
> **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
|
|
197
175
|
|
|
198
176
|
### Any Anthropic-compatible tool
|
|
199
177
|
|
|
200
178
|
```bash
|
|
201
179
|
export ANTHROPIC_API_KEY=x
|
|
202
180
|
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
203
|
-
# Then start your tool normally
|
|
204
181
|
```
|
|
205
182
|
|
|
206
183
|
## Tested Agents
|
|
207
184
|
|
|
208
|
-
| Agent | Status |
|
|
209
|
-
|
|
210
|
-
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified |
|
|
211
|
-
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see
|
|
212
|
-
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see
|
|
213
|
-
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see
|
|
214
|
-
| [
|
|
215
|
-
| [
|
|
185
|
+
| Agent | Status | Notes |
|
|
186
|
+
|-------|--------|-------|
|
|
187
|
+
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | Requires `meridian setup` — full tool support, session resume, streaming, subagents |
|
|
188
|
+
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see above) — full tool support, session resume, streaming |
|
|
189
|
+
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see above) — full tool support, session resume, headless `crush run` |
|
|
190
|
+
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see above) — full tool support, file read/write/edit, bash, session resume |
|
|
191
|
+
| [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
|
|
192
|
+
| [Continue](https://github.com/continuedev/continue) | 🔲 Untested | Should work — standard Anthropic API |
|
|
216
193
|
|
|
217
194
|
Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
|
|
218
195
|
|
|
219
196
|
## Architecture
|
|
220
197
|
|
|
221
|
-
Meridian is built as a modular proxy with clean separation of concerns:
|
|
222
|
-
|
|
223
198
|
```
|
|
224
199
|
src/proxy/
|
|
225
200
|
├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
|
|
226
|
-
├── adapter.ts ← AgentAdapter interface
|
|
201
|
+
├── adapter.ts ← AgentAdapter interface
|
|
227
202
|
├── adapters/
|
|
228
203
|
│ ├── detect.ts ← Agent detection from request headers
|
|
229
204
|
│ ├── opencode.ts ← OpenCode adapter
|
|
230
|
-
│ ├── crush.ts ← Crush
|
|
231
|
-
│ ├── droid.ts ← Droid
|
|
205
|
+
│ ├── crush.ts ← Crush adapter
|
|
206
|
+
│ ├── droid.ts ← Droid adapter
|
|
232
207
|
│ └── passthrough.ts ← LiteLLM passthrough adapter
|
|
233
208
|
├── query.ts ← SDK query options builder
|
|
234
209
|
├── errors.ts ← Error classification
|
|
235
|
-
├── models.ts ← Model mapping (sonnet/opus/haiku)
|
|
236
|
-
├──
|
|
237
|
-
├──
|
|
210
|
+
├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
|
|
211
|
+
├── tokenRefresh.ts ← Cross-platform OAuth token refresh
|
|
212
|
+
├── setup.ts ← OpenCode plugin configuration
|
|
238
213
|
├── session/
|
|
239
214
|
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
|
|
240
215
|
│ ├── fingerprint.ts ← Conversation fingerprinting
|
|
241
216
|
│ └── cache.ts ← LRU session caches
|
|
242
217
|
├── sessionStore.ts ← Cross-proxy file-based session persistence
|
|
243
|
-
├── agentDefs.ts ← Subagent definition extraction
|
|
244
218
|
└── passthroughTools.ts ← Tool forwarding mode
|
|
219
|
+
plugin/
|
|
220
|
+
└── meridian.ts ← OpenCode plugin (session headers + agent mode)
|
|
245
221
|
```
|
|
246
222
|
|
|
247
223
|
### Session Management
|
|
248
224
|
|
|
249
|
-
|
|
225
|
+
Every incoming request is classified:
|
|
250
226
|
|
|
251
227
|
| Classification | What Happened | Action |
|
|
252
228
|
|---------------|---------------|--------|
|
|
@@ -257,30 +233,9 @@ Sessions map agent conversations to Claude SDK sessions. Meridian classifies eve
|
|
|
257
233
|
|
|
258
234
|
Sessions are stored in-memory (LRU) and persisted to `~/.cache/meridian/sessions.json` for cross-proxy resume.
|
|
259
235
|
|
|
260
|
-
###
|
|
261
|
-
|
|
262
|
-
Implement the `AgentAdapter` interface in `src/proxy/adapters/`:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
interface AgentAdapter {
|
|
266
|
-
// Required
|
|
267
|
-
getSessionId(c: Context): string | undefined
|
|
268
|
-
extractWorkingDirectory(body: any): string | undefined
|
|
269
|
-
normalizeContent(content: any): string
|
|
270
|
-
getBlockedBuiltinTools(): readonly string[]
|
|
271
|
-
getAgentIncompatibleTools(): readonly string[]
|
|
272
|
-
getMcpServerName(): string
|
|
273
|
-
getAllowedMcpTools(): readonly string[]
|
|
274
|
-
|
|
275
|
-
// Optional
|
|
276
|
-
buildSdkAgents?(body: any, mcpToolNames: readonly string[]): Record<string, any>
|
|
277
|
-
buildSdkHooks?(body: any, sdkAgents: Record<string, any>): any
|
|
278
|
-
buildSystemContextAddendum?(body: any, sdkAgents: Record<string, any>): string
|
|
279
|
-
usesPassthrough?(): boolean // overrides CLAUDE_PROXY_PASSTHROUGH per-agent
|
|
280
|
-
}
|
|
281
|
-
```
|
|
236
|
+
### Agent Detection
|
|
282
237
|
|
|
283
|
-
|
|
238
|
+
Agents are identified from request headers automatically:
|
|
284
239
|
|
|
285
240
|
| User-Agent prefix | Adapter |
|
|
286
241
|
|---|---|
|
|
@@ -288,7 +243,9 @@ Agent detection is automatic from the `User-Agent` header:
|
|
|
288
243
|
| `factory-cli/` | Droid |
|
|
289
244
|
| *(anything else)* | OpenCode (default) |
|
|
290
245
|
|
|
291
|
-
|
|
246
|
+
### Adding a New Agent
|
|
247
|
+
|
|
248
|
+
Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for a reference.
|
|
292
249
|
|
|
293
250
|
## Configuration
|
|
294
251
|
|
|
@@ -304,103 +261,73 @@ See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode
|
|
|
304
261
|
| `MERIDIAN_IDLE_TIMEOUT_SECONDS` | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
|
|
305
262
|
| `MERIDIAN_TELEMETRY_SIZE` | `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
|
|
306
263
|
| `MERIDIAN_NO_FILE_CHANGES` | `CLAUDE_PROXY_NO_FILE_CHANGES` | unset | Disable "Files changed" summary in responses |
|
|
307
|
-
| `MERIDIAN_SONNET_MODEL` | `CLAUDE_PROXY_SONNET_MODEL` | `sonnet[1m]`* | Force sonnet tier: `sonnet` (200k) or `sonnet[1m]` (1M). Set to `sonnet` if you hit 1M context rate limits
|
|
308
|
-
|
|
309
|
-
*`sonnet[1m]` only for Max subscribers with Extra Usage enabled; falls back to `sonnet` automatically otherwise.
|
|
310
|
-
|
|
311
|
-
## Programmatic API
|
|
312
|
-
|
|
313
|
-
Meridian can be used as a library for building agent plugins and integrations.
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import { startProxyServer } from "@rynfar/meridian"
|
|
317
|
-
|
|
318
|
-
// Start a proxy instance
|
|
319
|
-
const instance = await startProxyServer({
|
|
320
|
-
port: 3456,
|
|
321
|
-
host: "127.0.0.1",
|
|
322
|
-
silent: true, // suppress console output
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
// instance.config — resolved ProxyConfig
|
|
326
|
-
// instance.server — underlying http.Server
|
|
327
|
-
|
|
328
|
-
// Shut down cleanly
|
|
329
|
-
await instance.close()
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Session Header Contract
|
|
333
|
-
|
|
334
|
-
For reliable session tracking, agents should send a session identifier via HTTP header. Without it, the proxy falls back to fingerprint-based matching (hashing the first user message + working directory), which is less reliable.
|
|
335
|
-
|
|
336
|
-
| Header | Purpose |
|
|
337
|
-
|--------|---------|
|
|
338
|
-
| `x-opencode-session` | Maps agent conversations to Claude SDK sessions for resume, undo, and compaction |
|
|
339
|
-
|
|
340
|
-
The proxy uses this header to maintain conversation continuity across requests. Plugin authors should inject it on every request to `/v1/messages`.
|
|
264
|
+
| `MERIDIAN_SONNET_MODEL` | `CLAUDE_PROXY_SONNET_MODEL` | `sonnet[1m]`* | Force sonnet tier: `sonnet` (200k) or `sonnet[1m]` (1M). Set to `sonnet` if you hit 1M context rate limits |
|
|
341
265
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
Meridian is the proxy. Plugins live in the agent's ecosystem.
|
|
345
|
-
|
|
346
|
-
```
|
|
347
|
-
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
348
|
-
│ Agent │ HTTP │ Meridian │ SDK │ Claude Max │
|
|
349
|
-
│ (OpenCode, │────────▶│ Proxy │────────▶│ │
|
|
350
|
-
│ Crush, etc) │◀────────│ │◀────────│ │
|
|
351
|
-
└──────────────┘ └──────────────┘ └──────────────┘
|
|
352
|
-
│
|
|
353
|
-
│ plugin injects headers,
|
|
354
|
-
│ manages proxy lifecycle
|
|
355
|
-
│
|
|
356
|
-
┌──────────────┐
|
|
357
|
-
│ Agent Plugin │
|
|
358
|
-
│ (optional) │
|
|
359
|
-
└──────────────┘
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
A plugin's job is to:
|
|
363
|
-
1. Start/stop a Meridian instance (`startProxyServer` / `instance.close()`)
|
|
364
|
-
2. Inject session headers into outgoing requests
|
|
365
|
-
3. Check proxy health (`GET /health`)
|
|
366
|
-
|
|
367
|
-
See [`examples/opencode-plugin/`](examples/opencode-plugin/) for a reference implementation.
|
|
266
|
+
*`sonnet[1m]` requires Max subscription with Extra Usage enabled. Falls back to `sonnet` automatically if not available.
|
|
368
267
|
|
|
369
268
|
## Endpoints
|
|
370
269
|
|
|
371
270
|
| Endpoint | Description |
|
|
372
271
|
|----------|-------------|
|
|
373
|
-
| `GET /` | Landing page
|
|
272
|
+
| `GET /` | Landing page |
|
|
374
273
|
| `POST /v1/messages` | Anthropic Messages API |
|
|
375
274
|
| `POST /messages` | Alias for `/v1/messages` |
|
|
376
|
-
| `GET /health` | Auth status,
|
|
275
|
+
| `GET /health` | Auth status, mode, plugin status |
|
|
377
276
|
| `POST /auth/refresh` | Manually refresh the OAuth token |
|
|
378
277
|
| `GET /telemetry` | Performance dashboard |
|
|
379
278
|
| `GET /telemetry/requests` | Recent request metrics (JSON) |
|
|
380
279
|
| `GET /telemetry/summary` | Aggregate statistics (JSON) |
|
|
381
280
|
| `GET /telemetry/logs` | Diagnostic logs (JSON) |
|
|
382
281
|
|
|
383
|
-
|
|
282
|
+
Health response example:
|
|
384
283
|
|
|
385
|
-
```
|
|
386
|
-
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"status": "healthy",
|
|
287
|
+
"auth": { "loggedIn": true, "email": "you@example.com", "subscriptionType": "max" },
|
|
288
|
+
"mode": "internal",
|
|
289
|
+
"plugin": { "opencode": "configured" }
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`plugin.opencode` is `"configured"` when `meridian setup` has been run, `"not-configured"` otherwise.
|
|
294
|
+
|
|
295
|
+
## CLI Commands
|
|
296
|
+
|
|
297
|
+
| Command | Description |
|
|
298
|
+
|---------|-------------|
|
|
299
|
+
| `meridian` | Start the proxy server |
|
|
300
|
+
| `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
|
|
301
|
+
| `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
|
|
302
|
+
|
|
303
|
+
## Programmatic API
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { startProxyServer } from "@rynfar/meridian"
|
|
307
|
+
|
|
308
|
+
const instance = await startProxyServer({
|
|
309
|
+
port: 3456,
|
|
310
|
+
host: "127.0.0.1",
|
|
311
|
+
silent: true,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// instance.server — underlying http.Server
|
|
315
|
+
await instance.close()
|
|
387
316
|
```
|
|
388
317
|
|
|
389
|
-
|
|
318
|
+
## Docker
|
|
390
319
|
|
|
391
320
|
```bash
|
|
392
|
-
docker
|
|
321
|
+
docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
|
|
393
322
|
```
|
|
394
323
|
|
|
395
324
|
## Testing
|
|
396
325
|
|
|
397
326
|
```bash
|
|
398
|
-
npm test
|
|
399
|
-
npm run build
|
|
327
|
+
npm test # unit + integration tests
|
|
328
|
+
npm run build # build with bun + tsc
|
|
400
329
|
```
|
|
401
330
|
|
|
402
|
-
Three test tiers:
|
|
403
|
-
|
|
404
331
|
| Tier | What | Speed |
|
|
405
332
|
|------|------|-------|
|
|
406
333
|
| Unit | Pure functions, no mocks | Fast |
|
|
@@ -410,30 +337,29 @@ Three test tiers:
|
|
|
410
337
|
## FAQ
|
|
411
338
|
|
|
412
339
|
**Is this allowed by Anthropic's terms?**
|
|
413
|
-
Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes
|
|
340
|
+
Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes for programmatic access. It authenticates through your existing Claude Max session using OAuth.
|
|
414
341
|
|
|
415
342
|
**How is this different from using an API key?**
|
|
416
|
-
API keys are billed per token.
|
|
343
|
+
API keys are billed per token. Claude Max is a flat monthly fee. Meridian lets you use that subscription from any compatible tool.
|
|
417
344
|
|
|
418
|
-
**
|
|
419
|
-
|
|
345
|
+
**What happens if my OAuth token expires?**
|
|
346
|
+
Tokens expire roughly every 8 hours. Meridian detects the expiry, refreshes the token automatically, and retries the request — so requests continue transparently. If the refresh fails (e.g. the refresh token has expired after weeks of inactivity), Meridian returns a clear error telling you to run `claude login`.
|
|
420
347
|
|
|
421
|
-
**
|
|
422
|
-
OAuth tokens expire roughly every 8 hours. Meridian detects the expiry on the next request, refreshes the token automatically, and retries — so requests continue to work transparently. If the refresh itself fails (e.g. your refresh token has expired after weeks of inactivity), Meridian returns a clear error telling you to run `claude login`.
|
|
423
|
-
|
|
424
|
-
**Can I trigger a refresh manually?**
|
|
425
|
-
Yes — two options:
|
|
348
|
+
**Can I trigger a token refresh manually?**
|
|
426
349
|
|
|
427
350
|
```bash
|
|
428
|
-
# CLI
|
|
351
|
+
# CLI — works whether the proxy is running or not
|
|
429
352
|
meridian refresh-token
|
|
430
353
|
|
|
431
|
-
# HTTP
|
|
432
|
-
curl -
|
|
433
|
-
# {"success":true,"message":"OAuth token refreshed successfully"}
|
|
354
|
+
# HTTP — while the proxy is running
|
|
355
|
+
curl -X POST http://127.0.0.1:3456/auth/refresh
|
|
434
356
|
```
|
|
435
357
|
|
|
436
|
-
|
|
358
|
+
**I'm hitting rate limits on 1M context. What do I do?**
|
|
359
|
+
Set `MERIDIAN_SONNET_MODEL=sonnet` to use the 200k model for all requests. If you're using OpenCode with the Meridian plugin, subagents already use 200k automatically — only the primary agent uses 1M.
|
|
360
|
+
|
|
361
|
+
**Why does the health endpoint show `"plugin": "not-configured"`?**
|
|
362
|
+
You haven't run `meridian setup`. Without the plugin, OpenCode requests won't have session tracking or subagent model selection. Run `meridian setup` and restart OpenCode.
|
|
437
363
|
|
|
438
364
|
## Contributing
|
|
439
365
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
export { __export, __require };
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true,
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
17
|
-
|
|
18
1
|
// src/proxy/tokenRefresh.ts
|
|
19
2
|
import { execFile as execFileCb } from "child_process";
|
|
20
3
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -217,4 +200,4 @@ function resetInflightRefresh() {
|
|
|
217
200
|
inflightRefresh = null;
|
|
218
201
|
}
|
|
219
202
|
|
|
220
|
-
export {
|
|
203
|
+
export { withClaudeLogContext, claudeLog, createPlatformCredentialStore, refreshOAuthToken, resetInflightRefresh };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/proxy/setup.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, platform } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function findOpencodeConfigPath() {
|
|
7
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
8
|
+
return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json");
|
|
9
|
+
}
|
|
10
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
11
|
+
return join(process.env.XDG_CONFIG_HOME, "opencode", "opencode.json");
|
|
12
|
+
}
|
|
13
|
+
if (platform() === "win32" && process.env.APPDATA) {
|
|
14
|
+
return join(process.env.APPDATA, "opencode", "opencode.json");
|
|
15
|
+
}
|
|
16
|
+
return join(homedir(), ".config", "opencode", "opencode.json");
|
|
17
|
+
}
|
|
18
|
+
function findPluginPath(fromUrl) {
|
|
19
|
+
const dir = dirname(fileURLToPath(fromUrl));
|
|
20
|
+
return join(dir, "..", "plugin", "meridian.ts");
|
|
21
|
+
}
|
|
22
|
+
var STALE_PATTERNS = [
|
|
23
|
+
"opencode-claude-max-proxy",
|
|
24
|
+
"claude-max-headers",
|
|
25
|
+
"meridian-agent-mode"
|
|
26
|
+
];
|
|
27
|
+
function isMeridianEntry(entry) {
|
|
28
|
+
return STALE_PATTERNS.some((p) => entry.includes(p)) || entry.includes("meridian.ts") || entry.includes("@rynfar/meridian");
|
|
29
|
+
}
|
|
30
|
+
function checkPluginConfigured(configPath) {
|
|
31
|
+
const path = configPath ?? findOpencodeConfigPath();
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(path, "utf-8");
|
|
36
|
+
const config = JSON.parse(raw);
|
|
37
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
38
|
+
return plugins.some((p) => typeof p === "string" && isMeridianEntry(p));
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function runSetup(pluginPath, configPath) {
|
|
44
|
+
const path = configPath ?? findOpencodeConfigPath();
|
|
45
|
+
const dir = dirname(path);
|
|
46
|
+
let config = {};
|
|
47
|
+
let created = false;
|
|
48
|
+
if (existsSync(path)) {
|
|
49
|
+
try {
|
|
50
|
+
config = JSON.parse(readFileSync(path, "utf-8"));
|
|
51
|
+
} catch {}
|
|
52
|
+
} else {
|
|
53
|
+
created = true;
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const existing = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
|
|
58
|
+
const removedStale = existing.filter(isMeridianEntry);
|
|
59
|
+
const others = existing.filter((p) => !isMeridianEntry(p));
|
|
60
|
+
const alreadyConfigured = removedStale.some((p) => p === pluginPath);
|
|
61
|
+
config.plugin = [...others, pluginPath];
|
|
62
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
63
|
+
`, "utf-8");
|
|
64
|
+
return { configPath: path, pluginPath, alreadyConfigured, removedStale, created };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { findOpencodeConfigPath, findPluginPath, checkPluginConfigured, runSetup };
|