@rynfar/meridian 1.21.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 +410 -0
- package/assets/banner.svg +46 -0
- package/assets/how-it-works.svg +87 -0
- package/assets/icon.svg +16 -0
- package/assets/logo.svg +15 -0
- package/dist/cli-zyn4cqp2.js +24392 -0
- package/dist/cli.js +46 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/mcpTools.d.ts +15 -0
- package/dist/mcpTools.d.ts.map +1 -0
- package/dist/proxy/adapter.d.ts +78 -0
- package/dist/proxy/adapter.d.ts.map +1 -0
- package/dist/proxy/adapters/crush.d.ts +22 -0
- package/dist/proxy/adapters/crush.d.ts.map +1 -0
- package/dist/proxy/adapters/detect.d.ts +18 -0
- package/dist/proxy/adapters/detect.d.ts.map +1 -0
- package/dist/proxy/adapters/droid.d.ts +19 -0
- package/dist/proxy/adapters/droid.d.ts.map +1 -0
- package/dist/proxy/adapters/opencode.d.ts +9 -0
- package/dist/proxy/adapters/opencode.d.ts.map +1 -0
- package/dist/proxy/agentDefs.d.ts +51 -0
- package/dist/proxy/agentDefs.d.ts.map +1 -0
- package/dist/proxy/agentMatch.d.ts +18 -0
- package/dist/proxy/agentMatch.d.ts.map +1 -0
- package/dist/proxy/errors.d.ts +25 -0
- package/dist/proxy/errors.d.ts.map +1 -0
- package/dist/proxy/messages.d.ts +24 -0
- package/dist/proxy/messages.d.ts.map +1 -0
- package/dist/proxy/models.d.ts +46 -0
- package/dist/proxy/models.d.ts.map +1 -0
- package/dist/proxy/passthroughTools.d.ts +30 -0
- package/dist/proxy/passthroughTools.d.ts.map +1 -0
- package/dist/proxy/query.d.ts +105 -0
- package/dist/proxy/query.d.ts.map +1 -0
- package/dist/proxy/server.d.ts +10 -0
- package/dist/proxy/server.d.ts.map +1 -0
- package/dist/proxy/session/cache.d.ts +32 -0
- package/dist/proxy/session/cache.d.ts.map +1 -0
- package/dist/proxy/session/fingerprint.d.ts +31 -0
- package/dist/proxy/session/fingerprint.d.ts.map +1 -0
- package/dist/proxy/session/lineage.d.ts +103 -0
- package/dist/proxy/session/lineage.d.ts.map +1 -0
- package/dist/proxy/sessionStore.d.ts +36 -0
- package/dist/proxy/sessionStore.d.ts.map +1 -0
- package/dist/proxy/tools.d.ts +26 -0
- package/dist/proxy/tools.d.ts.map +1 -0
- package/dist/proxy/types.d.ts +27 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/server.js +18 -0
- package/dist/telemetry/dashboard.d.ts +6 -0
- package/dist/telemetry/dashboard.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +7 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/landing.d.ts +7 -0
- package/dist/telemetry/landing.d.ts.map +1 -0
- package/dist/telemetry/logStore.d.ts +50 -0
- package/dist/telemetry/logStore.d.ts.map +1 -0
- package/dist/telemetry/routes.d.ts +11 -0
- package/dist/telemetry/routes.d.ts.map +1 -0
- package/dist/telemetry/store.d.ts +39 -0
- package/dist/telemetry/store.d.ts.map +1 -0
- package/dist/telemetry/types.d.ts +93 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/utils/lruMap.d.ts +28 -0
- package/dist/utils/lruMap.d.ts.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/banner.svg" alt="Meridian" width="800"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/rynfar/meridian/releases"><img src="https://img.shields.io/github/v/release/rynfar/@rynfar/meridian?style=flat-square&color=6366f1&label=release" alt="Release"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@rynfar/meridian"><img src="https://img.shields.io/npm/v/@rynfar/meridian?style=flat-square&color=8b5cf6&label=npm" alt="npm"></a>
|
|
8
|
+
<a href="#"><img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-a78bfa?style=flat-square" alt="Platform"></a>
|
|
9
|
+
<a href="#"><img src="https://img.shields.io/badge/license-MIT-c4b5fd?style=flat-square" alt="License"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic protocol — OpenCode, Crush, Cline, Continue, Aider — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
|
|
15
|
+
|
|
16
|
+
Harness Claude, your way.
|
|
17
|
+
|
|
18
|
+
> [!NOTE]
|
|
19
|
+
> **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.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Install
|
|
25
|
+
npm install -g @rynfar/meridian
|
|
26
|
+
|
|
27
|
+
# Authenticate (one time)
|
|
28
|
+
claude login
|
|
29
|
+
|
|
30
|
+
# Start
|
|
31
|
+
meridian
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Meridian starts on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at it:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The API key value doesn't matter — Meridian authenticates through your Claude Max session, not API keys.
|
|
41
|
+
|
|
42
|
+
## Why Meridian?
|
|
43
|
+
|
|
44
|
+
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
|
+
Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription. Claude does the work — Meridian just lets you pick the tool.
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Standard Anthropic API** — drop-in compatible with any tool that supports custom `base_url`
|
|
55
|
+
- **Session management** — conversations persist across requests, survive compaction and undo, resume after proxy restarts
|
|
56
|
+
- **Streaming** — full SSE streaming with MCP tool filtering
|
|
57
|
+
- **Concurrent sessions** — run parent + subagent requests in parallel
|
|
58
|
+
- **Passthrough mode** — forward tool calls to the client instead of executing internally
|
|
59
|
+
- **Multimodal** — images, documents, and file attachments pass through to Claude
|
|
60
|
+
- **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
|
+
## Agent Setup
|
|
65
|
+
|
|
66
|
+
### OpenCode
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For automatic session tracking, use a plugin like [opencode-meridian](https://github.com/ianjwhite99/opencode-meridian), or see the [reference plugin](examples/opencode-plugin/claude-max-headers.ts) to build your own.
|
|
73
|
+
|
|
74
|
+
### Crush
|
|
75
|
+
|
|
76
|
+
Add a provider to `~/.config/crush/crush.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"providers": {
|
|
81
|
+
"claude-max": {
|
|
82
|
+
"id": "claude-max",
|
|
83
|
+
"name": "Claude Max (Meridian)",
|
|
84
|
+
"type": "anthropic",
|
|
85
|
+
"base_url": "http://127.0.0.1:3456",
|
|
86
|
+
"api_key": "dummy",
|
|
87
|
+
"models": [
|
|
88
|
+
{ "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true },
|
|
89
|
+
{ "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
|
|
90
|
+
{ "id": "claude-haiku-4-5-20251001", "name": "Claude Haiku 4.5", "context_window": 200000, "default_max_tokens": 16384, "can_reason": true, "supports_attachments": true }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then use Meridian models in Crush:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
crush run --model claude-max/claude-sonnet-4-6 "refactor this function"
|
|
101
|
+
crush --model claude-max/claude-opus-4-6 # interactive TUI
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Crush is automatically detected from its `Charm-Crush/` User-Agent — no extra configuration needed. In `crush run` headless mode all tool operations (read, write, bash) execute automatically without prompting.
|
|
105
|
+
|
|
106
|
+
### Droid (Factory AI)
|
|
107
|
+
|
|
108
|
+
Droid connects via its BYOK (Bring Your Own Key) feature. This is a one-time setup.
|
|
109
|
+
|
|
110
|
+
**1. Add Meridian as a custom model provider** in `~/.factory/settings.json`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"customModels": [
|
|
115
|
+
{
|
|
116
|
+
"model": "claude-sonnet-4-6",
|
|
117
|
+
"name": "Sonnet 4.6 (1M — Claude Max)",
|
|
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 — Claude Max)",
|
|
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 (Claude Max)",
|
|
132
|
+
"provider": "anthropic",
|
|
133
|
+
"baseUrl": "http://127.0.0.1:3456",
|
|
134
|
+
"apiKey": "x"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The `apiKey` value doesn't matter — Meridian authenticates through your Claude Max session.
|
|
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.
|
|
154
|
+
|
|
155
|
+
### Cline
|
|
156
|
+
|
|
157
|
+
Cline CLI connects by setting `anthropicBaseUrl` in its config. This is a one-time setup.
|
|
158
|
+
|
|
159
|
+
**1. Authenticate Cline with the Anthropic provider:**
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**2. Add the proxy base URL** to `~/.cline/data/globalState.json`:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"anthropicBaseUrl": "http://127.0.0.1:3456",
|
|
170
|
+
"actModeApiProvider": "anthropic",
|
|
171
|
+
"actModeApiModelId": "claude-sonnet-4-6"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**3. Run Cline:**
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cline --yolo "refactor the login function" # interactive
|
|
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)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
No adapter or plugin needed — Cline uses the standard Anthropic SDK and falls through to the default adapter. All models (Sonnet 4.6, Opus 4.6, Haiku 4.5) route to their correct Claude Max tiers automatically.
|
|
184
|
+
|
|
185
|
+
### Any Anthropic-compatible tool
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
export ANTHROPIC_API_KEY=x
|
|
189
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
190
|
+
# Then start your tool normally
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Tested Agents
|
|
194
|
+
|
|
195
|
+
| Agent | Status | Plugin | Notes |
|
|
196
|
+
|-------|--------|--------|-------|
|
|
197
|
+
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | [opencode-meridian](https://github.com/ianjwhite99/opencode-meridian) | Full tool support, session resume, streaming, subagents |
|
|
198
|
+
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see setup above) | Full tool support, session resume, streaming; one-time BYOK setup |
|
|
199
|
+
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see setup above) | Full tool support, session resume, streaming, headless `crush run` |
|
|
200
|
+
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see setup above) | Full tool support, file read/write/edit, bash, session resume, all models |
|
|
201
|
+
| [Continue](https://github.com/continuedev/continue) | 🔲 Untested | — | Should work — standard Anthropic API |
|
|
202
|
+
| [Aider](https://github.com/paul-gauthier/aider) | 🔲 Untested | — | Should work — standard Anthropic API |
|
|
203
|
+
|
|
204
|
+
Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
|
|
205
|
+
|
|
206
|
+
## Architecture
|
|
207
|
+
|
|
208
|
+
Meridian is built as a modular proxy with clean separation of concerns:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
src/proxy/
|
|
212
|
+
├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
|
|
213
|
+
├── adapter.ts ← AgentAdapter interface (extensibility point)
|
|
214
|
+
├── adapters/
|
|
215
|
+
│ ├── detect.ts ← Agent detection from request headers
|
|
216
|
+
│ ├── opencode.ts ← OpenCode adapter
|
|
217
|
+
│ └── droid.ts ← Droid (Factory AI) adapter
|
|
218
|
+
├── query.ts ← SDK query options builder
|
|
219
|
+
├── errors.ts ← Error classification
|
|
220
|
+
├── models.ts ← Model mapping (sonnet/opus/haiku)
|
|
221
|
+
├── tools.ts ← Tool blocking lists
|
|
222
|
+
├── messages.ts ← Content normalization
|
|
223
|
+
├── session/
|
|
224
|
+
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
|
|
225
|
+
│ ├── fingerprint.ts ← Conversation fingerprinting
|
|
226
|
+
│ └── cache.ts ← LRU session caches
|
|
227
|
+
├── sessionStore.ts ← Cross-proxy file-based session persistence
|
|
228
|
+
├── agentDefs.ts ← Subagent definition extraction
|
|
229
|
+
└── passthroughTools.ts ← Tool forwarding mode
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Session Management
|
|
233
|
+
|
|
234
|
+
Sessions map agent conversations to Claude SDK sessions. Meridian classifies every incoming request:
|
|
235
|
+
|
|
236
|
+
| Classification | What Happened | Action |
|
|
237
|
+
|---------------|---------------|--------|
|
|
238
|
+
| **Continuation** | New messages appended | Resume SDK session |
|
|
239
|
+
| **Compaction** | Agent summarized old messages | Resume (suffix preserved) |
|
|
240
|
+
| **Undo** | User rolled back messages | Fork at rollback point |
|
|
241
|
+
| **Diverged** | Completely different conversation | Start fresh |
|
|
242
|
+
|
|
243
|
+
Sessions are stored in-memory (LRU) and persisted to `~/.cache/@rynfar/meridian/sessions.json` for cross-proxy resume.
|
|
244
|
+
|
|
245
|
+
### Adding a New Agent
|
|
246
|
+
|
|
247
|
+
Implement the `AgentAdapter` interface in `src/proxy/adapters/`:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
interface AgentAdapter {
|
|
251
|
+
// Required
|
|
252
|
+
getSessionId(c: Context): string | undefined
|
|
253
|
+
extractWorkingDirectory(body: any): string | undefined
|
|
254
|
+
normalizeContent(content: any): string
|
|
255
|
+
getBlockedBuiltinTools(): readonly string[]
|
|
256
|
+
getAgentIncompatibleTools(): readonly string[]
|
|
257
|
+
getMcpServerName(): string
|
|
258
|
+
getAllowedMcpTools(): readonly string[]
|
|
259
|
+
|
|
260
|
+
// Optional
|
|
261
|
+
buildSdkAgents?(body: any, mcpToolNames: readonly string[]): Record<string, any>
|
|
262
|
+
buildSdkHooks?(body: any, sdkAgents: Record<string, any>): any
|
|
263
|
+
buildSystemContextAddendum?(body: any, sdkAgents: Record<string, any>): string
|
|
264
|
+
usesPassthrough?(): boolean // overrides CLAUDE_PROXY_PASSTHROUGH per-agent
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Agent detection is automatic from the `User-Agent` header:
|
|
269
|
+
|
|
270
|
+
| User-Agent prefix | Adapter |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `Charm-Crush/` | Crush |
|
|
273
|
+
| `factory-cli/` | Droid |
|
|
274
|
+
| *(anything else)* | OpenCode (default) |
|
|
275
|
+
|
|
276
|
+
See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for reference.
|
|
277
|
+
|
|
278
|
+
## Configuration
|
|
279
|
+
|
|
280
|
+
| Variable | Default | Description |
|
|
281
|
+
|----------|---------|-------------|
|
|
282
|
+
| `CLAUDE_PROXY_PORT` | `3456` | Port to listen on |
|
|
283
|
+
| `CLAUDE_PROXY_HOST` | `127.0.0.1` | Host to bind to |
|
|
284
|
+
| `CLAUDE_PROXY_PASSTHROUGH` | unset | Forward tool calls to client instead of executing |
|
|
285
|
+
| `CLAUDE_PROXY_MAX_CONCURRENT` | `10` | Maximum concurrent SDK sessions |
|
|
286
|
+
| `CLAUDE_PROXY_MAX_SESSIONS` | `1000` | In-memory LRU session cache size |
|
|
287
|
+
| `CLAUDE_PROXY_MAX_STORED_SESSIONS` | `10000` | File-based session store capacity |
|
|
288
|
+
| `CLAUDE_PROXY_WORKDIR` | `cwd()` | Default working directory for SDK |
|
|
289
|
+
| `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
|
|
290
|
+
| `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
|
|
291
|
+
|
|
292
|
+
## Programmatic API
|
|
293
|
+
|
|
294
|
+
Meridian can be used as a library for building agent plugins and integrations.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { startProxyServer } from "@rynfar/meridian"
|
|
298
|
+
|
|
299
|
+
// Start a proxy instance
|
|
300
|
+
const instance = await startProxyServer({
|
|
301
|
+
port: 3456,
|
|
302
|
+
host: "127.0.0.1",
|
|
303
|
+
silent: true, // suppress console output
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// instance.config — resolved ProxyConfig
|
|
307
|
+
// instance.server — underlying http.Server
|
|
308
|
+
|
|
309
|
+
// Shut down cleanly
|
|
310
|
+
await instance.close()
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Session Header Contract
|
|
314
|
+
|
|
315
|
+
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.
|
|
316
|
+
|
|
317
|
+
| Header | Purpose |
|
|
318
|
+
|--------|---------|
|
|
319
|
+
| `x-opencode-session` | Maps agent conversations to Claude SDK sessions for resume, undo, and compaction |
|
|
320
|
+
|
|
321
|
+
The proxy uses this header to maintain conversation continuity across requests. Plugin authors should inject it on every request to `/v1/messages`.
|
|
322
|
+
|
|
323
|
+
### Plugin Architecture
|
|
324
|
+
|
|
325
|
+
Meridian is the proxy. Plugins live in the agent's ecosystem.
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
329
|
+
│ Agent │ HTTP │ Meridian │ SDK │ Claude Max │
|
|
330
|
+
│ (OpenCode, │────────▶│ Proxy │────────▶│ │
|
|
331
|
+
│ Crush, etc) │◀────────│ │◀────────│ │
|
|
332
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
333
|
+
│
|
|
334
|
+
│ plugin injects headers,
|
|
335
|
+
│ manages proxy lifecycle
|
|
336
|
+
│
|
|
337
|
+
┌──────────────┐
|
|
338
|
+
│ Agent Plugin │
|
|
339
|
+
│ (optional) │
|
|
340
|
+
└──────────────┘
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A plugin's job is to:
|
|
344
|
+
1. Start/stop a Meridian instance (`startProxyServer` / `instance.close()`)
|
|
345
|
+
2. Inject session headers into outgoing requests
|
|
346
|
+
3. Check proxy health (`GET /health`)
|
|
347
|
+
|
|
348
|
+
See [`examples/opencode-plugin/`](examples/opencode-plugin/) for a reference implementation.
|
|
349
|
+
|
|
350
|
+
## Endpoints
|
|
351
|
+
|
|
352
|
+
| Endpoint | Description |
|
|
353
|
+
|----------|-------------|
|
|
354
|
+
| `GET /` | Landing page (HTML) or status JSON (`Accept: application/json`) |
|
|
355
|
+
| `POST /v1/messages` | Anthropic Messages API |
|
|
356
|
+
| `POST /messages` | Alias for `/v1/messages` |
|
|
357
|
+
| `GET /health` | Auth status, subscription type, mode |
|
|
358
|
+
| `GET /telemetry` | Performance dashboard |
|
|
359
|
+
| `GET /telemetry/requests` | Recent request metrics (JSON) |
|
|
360
|
+
| `GET /telemetry/summary` | Aggregate statistics (JSON) |
|
|
361
|
+
| `GET /telemetry/logs` | Diagnostic logs (JSON) |
|
|
362
|
+
|
|
363
|
+
## Docker
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Or with docker-compose:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
docker compose up -d
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Testing
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
npm test # 339 unit/integration tests (bun test)
|
|
379
|
+
npm run build # Build with bun + tsc
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Three test tiers:
|
|
383
|
+
|
|
384
|
+
| Tier | What | Speed |
|
|
385
|
+
|------|------|-------|
|
|
386
|
+
| Unit | Pure functions, no mocks | Fast |
|
|
387
|
+
| Integration | HTTP layer with mocked SDK | Fast |
|
|
388
|
+
| E2E | Real proxy + real Claude Max ([`E2E.md`](E2E.md)) | Manual |
|
|
389
|
+
|
|
390
|
+
## FAQ
|
|
391
|
+
|
|
392
|
+
**Is this allowed by Anthropic's terms?**
|
|
393
|
+
Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes and maintains for programmatic access. It authenticates through your existing Claude Max session using OAuth, not API keys. Nothing is modified, reverse-engineered, or bypassed.
|
|
394
|
+
|
|
395
|
+
**How is this different from using an API key?**
|
|
396
|
+
API keys are billed per token. Your Max subscription is a flat monthly fee with higher rate limits. Meridian lets you use that subscription from any compatible tool.
|
|
397
|
+
|
|
398
|
+
**Does it work with Claude Pro?**
|
|
399
|
+
It works with any Claude subscription that supports the Claude Code SDK. Max is recommended for the best rate limits.
|
|
400
|
+
|
|
401
|
+
**What happens if my session expires?**
|
|
402
|
+
The SDK handles token refresh automatically. If it can't refresh, Meridian returns a clear error telling you to run `claude login`.
|
|
403
|
+
|
|
404
|
+
## Contributing
|
|
405
|
+
|
|
406
|
+
Issues and PRs welcome. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for module structure and dependency rules, [`CLAUDE.md`](CLAUDE.md) for coding guidelines, and [`E2E.md`](E2E.md) for end-to-end test procedures.
|
|
407
|
+
|
|
408
|
+
## License
|
|
409
|
+
|
|
410
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 400" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#0f0b1a"/>
|
|
5
|
+
<stop offset="100%" stop-color="#1a1030"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<rect width="1200" height="400" rx="16" fill="url(#bg)"/>
|
|
10
|
+
|
|
11
|
+
<!-- Subtle dot grid -->
|
|
12
|
+
<g opacity="0.05" fill="#a78bfa">
|
|
13
|
+
<circle cx="100" cy="100" r="1.5"/><circle cx="200" cy="100" r="1.5"/><circle cx="300" cy="100" r="1.5"/>
|
|
14
|
+
<circle cx="100" cy="200" r="1.5"/><circle cx="200" cy="200" r="1.5"/><circle cx="300" cy="200" r="1.5"/>
|
|
15
|
+
<circle cx="100" cy="300" r="1.5"/><circle cx="200" cy="300" r="1.5"/><circle cx="300" cy="300" r="1.5"/>
|
|
16
|
+
<circle cx="900" cy="100" r="1.5"/><circle cx="1000" cy="100" r="1.5"/><circle cx="1100" cy="100" r="1.5"/>
|
|
17
|
+
<circle cx="900" cy="200" r="1.5"/><circle cx="1000" cy="200" r="1.5"/><circle cx="1100" cy="200" r="1.5"/>
|
|
18
|
+
<circle cx="900" cy="300" r="1.5"/><circle cx="1000" cy="300" r="1.5"/><circle cx="1100" cy="300" r="1.5"/>
|
|
19
|
+
</g>
|
|
20
|
+
|
|
21
|
+
<!-- Logo mark — V2 full globe -->
|
|
22
|
+
<g transform="translate(380, 90)">
|
|
23
|
+
<!-- Meridian axis -->
|
|
24
|
+
<line x1="80" y1="8" x2="80" y2="192" stroke="#8B7CF6" stroke-width="5" stroke-linecap="round"/>
|
|
25
|
+
<!-- Latitude arcs — outer -->
|
|
26
|
+
<path d="M28 50 A60 60 0 0 1 132 50" fill="none" stroke="#C4B5FD" stroke-width="2.5" opacity="0.4"/>
|
|
27
|
+
<path d="M28 150 A60 60 0 0 0 132 150" fill="none" stroke="#C4B5FD" stroke-width="2.5" opacity="0.4"/>
|
|
28
|
+
<!-- Latitude arcs — inner -->
|
|
29
|
+
<path d="M38 78 A46 46 0 0 1 122 78" fill="none" stroke="#C4B5FD" stroke-width="1.5" opacity="0.2"/>
|
|
30
|
+
<path d="M38 122 A46 46 0 0 0 122 122" fill="none" stroke="#C4B5FD" stroke-width="1.5" opacity="0.2"/>
|
|
31
|
+
<!-- Poles -->
|
|
32
|
+
<circle cx="80" cy="8" r="8" fill="#C4B5FD"/>
|
|
33
|
+
<circle cx="80" cy="192" r="8" fill="#C4B5FD"/>
|
|
34
|
+
<!-- Center node -->
|
|
35
|
+
<circle cx="80" cy="100" r="8" fill="#8B7CF6"/>
|
|
36
|
+
</g>
|
|
37
|
+
|
|
38
|
+
<!-- Wordmark -->
|
|
39
|
+
<text x="540" y="195" font-family="Inter, -apple-system, 'SF Pro Display', 'Segoe UI', Helvetica, Arial, sans-serif" font-size="72" font-weight="700" fill="#e0e7ff" letter-spacing="10">MERIDIAN</text>
|
|
40
|
+
|
|
41
|
+
<!-- Tagline -->
|
|
42
|
+
<text x="543" y="240" font-family="Inter, -apple-system, 'SF Pro Display', 'Segoe UI', Helvetica, Arial, sans-serif" font-size="17" font-weight="600" fill="#8B7CF6" letter-spacing="3">HARNESS CLAUDE, YOUR WAY.</text>
|
|
43
|
+
|
|
44
|
+
<!-- Subtitle -->
|
|
45
|
+
<text x="543" y="278" font-family="Inter, -apple-system, 'SF Pro Display', 'Segoe UI', Helvetica, Arial, sans-serif" font-size="14" font-weight="300" fill="rgba(255,255,255,0.35)">Local Anthropic API powered by the Claude Code SDK</text>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 310" fill="none">
|
|
2
|
+
<style>
|
|
3
|
+
.title { font-family: 'Inter', -apple-system, sans-serif; font-size: 20px; font-weight: 700; fill: #C4B5FD; }
|
|
4
|
+
.box-label { font-family: 'Inter', -apple-system, sans-serif; font-size: 13px; font-weight: 600; fill: #F5F5F7; }
|
|
5
|
+
.box-sub { font-family: 'JetBrains Mono', 'SF Mono', monospace; font-size: 10px; fill: #8B7CF6; }
|
|
6
|
+
.box-detail { font-family: 'Inter', -apple-system, sans-serif; font-size: 10px; fill: #9898B4; }
|
|
7
|
+
.tag { font-family: 'Inter', -apple-system, sans-serif; font-size: 9px; font-weight: 600; fill: #5E5B72; letter-spacing: 1px; }
|
|
8
|
+
.step-num { font-family: 'Inter', -apple-system, sans-serif; font-size: 10px; font-weight: 700; }
|
|
9
|
+
.footer { font-family: 'Inter', -apple-system, sans-serif; font-size: 11px; fill: #5E5B72; }
|
|
10
|
+
.arrow-label { font-family: 'Inter', -apple-system, sans-serif; font-size: 9px; fill: #5E5B72; text-anchor: middle; }
|
|
11
|
+
</style>
|
|
12
|
+
|
|
13
|
+
<rect width="920" height="310" rx="16" fill="#13111C"/>
|
|
14
|
+
<rect x="0.5" y="0.5" width="919" height="309" rx="15.5" stroke="#2A2740" stroke-width="1" fill="none"/>
|
|
15
|
+
|
|
16
|
+
<text class="title" x="460" y="38" text-anchor="middle">How it works</text>
|
|
17
|
+
|
|
18
|
+
<!-- ═══ STEP 1: YOUR TOOLS ═══ -->
|
|
19
|
+
<rect x="32" y="64" width="156" height="140" rx="12" fill="#1C1930" stroke="#2A2740" stroke-width="1"/>
|
|
20
|
+
<rect x="44" y="54" width="18" height="18" rx="9" fill="#8B7CF6"/>
|
|
21
|
+
<text class="step-num" x="53" y="67" text-anchor="middle" fill="#13111C">1</text>
|
|
22
|
+
<text class="box-label" x="110" y="100" text-anchor="middle">Your tools</text>
|
|
23
|
+
<text class="box-detail" x="110" y="122" text-anchor="middle">OpenCode, Cline</text>
|
|
24
|
+
<text class="box-detail" x="110" y="136" text-anchor="middle">Continue, Crush, aider</text>
|
|
25
|
+
<text class="box-detail" x="110" y="150" text-anchor="middle">or any Anthropic client</text>
|
|
26
|
+
<text class="tag" x="110" y="192" text-anchor="middle">ANTHROPIC API FORMAT</text>
|
|
27
|
+
|
|
28
|
+
<!-- ═══ ARROWS 1 ↔ 2 ═══ -->
|
|
29
|
+
<line x1="200" y1="122" x2="250" y2="122" stroke="#8B7CF6" stroke-width="1.5"/>
|
|
30
|
+
<polygon points="248,118 256,122 248,126" fill="#8B7CF6"/>
|
|
31
|
+
<text class="arrow-label" x="226" y="114">request</text>
|
|
32
|
+
<line x1="250" y1="142" x2="200" y2="142" stroke="#3D3858" stroke-width="1"/>
|
|
33
|
+
<polygon points="202,138 194,142 202,146" fill="#3D3858"/>
|
|
34
|
+
<text class="arrow-label" x="226" y="162">response</text>
|
|
35
|
+
|
|
36
|
+
<!-- ═══ STEP 2: MERIDIAN ═══ -->
|
|
37
|
+
<rect x="264" y="64" width="156" height="140" rx="12" fill="#1C1930" stroke="#8B7CF6" stroke-width="1.5"/>
|
|
38
|
+
<rect x="276" y="54" width="18" height="18" rx="9" fill="#8B7CF6"/>
|
|
39
|
+
<text class="step-num" x="285" y="67" text-anchor="middle" fill="#13111C">2</text>
|
|
40
|
+
<text class="box-label" x="342" y="100" text-anchor="middle">Meridian</text>
|
|
41
|
+
<text class="box-sub" x="342" y="118" text-anchor="middle">localhost:3456</text>
|
|
42
|
+
<text class="box-detail" x="342" y="140" text-anchor="middle">Local proxy server</text>
|
|
43
|
+
<text class="box-detail" x="342" y="154" text-anchor="middle">Translates API calls</text>
|
|
44
|
+
<text class="box-detail" x="342" y="168" text-anchor="middle">to Claude Code SDK</text>
|
|
45
|
+
<text class="tag" x="342" y="192" text-anchor="middle">BRIDGES THE GAP</text>
|
|
46
|
+
|
|
47
|
+
<!-- ═══ ARROWS 2 ↔ 3 ═══ -->
|
|
48
|
+
<line x1="432" y1="122" x2="482" y2="122" stroke="#8B7CF6" stroke-width="1.5"/>
|
|
49
|
+
<polygon points="480,118 488,122 480,126" fill="#8B7CF6"/>
|
|
50
|
+
<text class="arrow-label" x="458" y="114">SDK call</text>
|
|
51
|
+
<line x1="482" y1="142" x2="432" y2="142" stroke="#3D3858" stroke-width="1"/>
|
|
52
|
+
<polygon points="434,138 426,142 434,146" fill="#3D3858"/>
|
|
53
|
+
<text class="arrow-label" x="458" y="162">result</text>
|
|
54
|
+
|
|
55
|
+
<!-- ═══ STEP 3: CLAUDE CODE SDK ═══ -->
|
|
56
|
+
<rect x="496" y="64" width="172" height="140" rx="12" fill="#1C1930" stroke="#2A2740" stroke-width="1"/>
|
|
57
|
+
<rect x="508" y="54" width="18" height="18" rx="9" fill="#8B7CF6"/>
|
|
58
|
+
<text class="step-num" x="517" y="67" text-anchor="middle" fill="#13111C">3</text>
|
|
59
|
+
<text class="box-label" x="582" y="100" text-anchor="middle">Claude Code SDK</text>
|
|
60
|
+
<text class="box-sub" x="582" y="118" text-anchor="middle">@anthropic-ai/claude-code</text>
|
|
61
|
+
<text class="box-detail" x="582" y="140" text-anchor="middle">Official Anthropic SDK</text>
|
|
62
|
+
<text class="box-detail" x="582" y="154" text-anchor="middle">Handles auth + sessions</text>
|
|
63
|
+
<text class="tag" x="582" y="192" text-anchor="middle">ANTHROPIC'S OWN SDK</text>
|
|
64
|
+
|
|
65
|
+
<!-- ═══ ARROWS 3 ↔ 4 ═══ -->
|
|
66
|
+
<line x1="680" y1="122" x2="726" y2="122" stroke="#8B7CF6" stroke-width="1.5"/>
|
|
67
|
+
<polygon points="724,118 732,122 724,126" fill="#8B7CF6"/>
|
|
68
|
+
<text class="arrow-label" x="704" y="114">API call</text>
|
|
69
|
+
<line x1="726" y1="142" x2="680" y2="142" stroke="#3D3858" stroke-width="1"/>
|
|
70
|
+
<polygon points="682,138 674,142 682,146" fill="#3D3858"/>
|
|
71
|
+
<text class="arrow-label" x="704" y="162">stream</text>
|
|
72
|
+
|
|
73
|
+
<!-- ═══ STEP 4: CLAUDE ═══ -->
|
|
74
|
+
<rect x="740" y="64" width="140" height="140" rx="12" fill="#1C1930" stroke="#2A2740" stroke-width="1"/>
|
|
75
|
+
<rect x="752" y="54" width="18" height="18" rx="9" fill="#8B7CF6"/>
|
|
76
|
+
<text class="step-num" x="761" y="67" text-anchor="middle" fill="#13111C">4</text>
|
|
77
|
+
<text class="box-label" x="810" y="100" text-anchor="middle">Claude</text>
|
|
78
|
+
<text class="box-sub" x="810" y="118" text-anchor="middle">api.anthropic.com</text>
|
|
79
|
+
<text class="box-detail" x="810" y="140" text-anchor="middle">Opus, Sonnet, Haiku</text>
|
|
80
|
+
<text class="box-detail" x="810" y="154" text-anchor="middle">Extended thinking</text>
|
|
81
|
+
<text class="tag" x="810" y="192" text-anchor="middle">YOUR SUBSCRIPTION</text>
|
|
82
|
+
|
|
83
|
+
<!-- ═══ BOTTOM ═══ -->
|
|
84
|
+
<line x1="60" y1="246" x2="860" y2="246" stroke="#2A2740" stroke-width="1"/>
|
|
85
|
+
<text class="footer" x="460" y="274" text-anchor="middle">No API keys needed — authenticates through your existing Claude Code / Max subscription</text>
|
|
86
|
+
|
|
87
|
+
</svg>
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#1C1830"/>
|
|
3
|
+
<!-- Meridian axis -->
|
|
4
|
+
<line x1="32" y1="10" x2="32" y2="54" stroke="#8B7CF6" stroke-width="2.5" stroke-linecap="round"/>
|
|
5
|
+
<!-- Latitude arcs — outer -->
|
|
6
|
+
<path d="M16 20 A18 18 0 0 1 48 20" fill="none" stroke="#C4B5FD" stroke-width="1.2" opacity="0.4"/>
|
|
7
|
+
<path d="M16 44 A18 18 0 0 0 48 44" fill="none" stroke="#C4B5FD" stroke-width="1.2" opacity="0.4"/>
|
|
8
|
+
<!-- Latitude arcs — inner -->
|
|
9
|
+
<path d="M20 30 A14 14 0 0 1 44 30" fill="none" stroke="#C4B5FD" stroke-width="0.8" opacity="0.2"/>
|
|
10
|
+
<path d="M20 34 A14 14 0 0 0 44 34" fill="none" stroke="#C4B5FD" stroke-width="0.8" opacity="0.2"/>
|
|
11
|
+
<!-- Poles -->
|
|
12
|
+
<circle cx="32" cy="10" r="3.5" fill="#C4B5FD"/>
|
|
13
|
+
<circle cx="32" cy="54" r="3.5" fill="#C4B5FD"/>
|
|
14
|
+
<!-- Center node -->
|
|
15
|
+
<circle cx="32" cy="32" r="3" fill="#8B7CF6"/>
|
|
16
|
+
</svg>
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
|
|
2
|
+
<!-- Meridian axis -->
|
|
3
|
+
<line x1="256" y1="40" x2="256" y2="472" stroke="#8B7CF6" stroke-width="12" stroke-linecap="round"/>
|
|
4
|
+
<!-- Latitude arcs — outer pair -->
|
|
5
|
+
<path d="M110 130 A166 166 0 0 1 402 130" fill="none" stroke="#C4B5FD" stroke-width="7" opacity="0.4"/>
|
|
6
|
+
<path d="M110 382 A166 166 0 0 0 402 382" fill="none" stroke="#C4B5FD" stroke-width="7" opacity="0.4"/>
|
|
7
|
+
<!-- Latitude arcs — inner pair -->
|
|
8
|
+
<path d="M132 210 A136 136 0 0 1 380 210" fill="none" stroke="#C4B5FD" stroke-width="5" opacity="0.25"/>
|
|
9
|
+
<path d="M132 302 A136 136 0 0 0 380 302" fill="none" stroke="#C4B5FD" stroke-width="5" opacity="0.25"/>
|
|
10
|
+
<!-- Poles -->
|
|
11
|
+
<circle cx="256" cy="40" r="22" fill="#C4B5FD"/>
|
|
12
|
+
<circle cx="256" cy="472" r="22" fill="#C4B5FD"/>
|
|
13
|
+
<!-- Center node -->
|
|
14
|
+
<circle cx="256" cy="256" r="20" fill="#8B7CF6"/>
|
|
15
|
+
</svg>
|