@rynfar/meridian 1.24.5 → 1.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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, 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.
14
+ Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, Crush, Cline, Aider, Open WebUI — 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
- # Start
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 starts on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at it:
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. Claude does the work — Meridian just lets you pick the tool.
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,46 @@ 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`
56
+ - **OpenAI-compatible API** — `/v1/chat/completions` and `/v1/models` for tools that only speak the OpenAI protocol (Open WebUI, Continue, etc.) — no LiteLLM needed
55
57
  - **Session management** — conversations persist across requests, survive compaction and undo, resume after proxy restarts
56
58
  - **Streaming** — full SSE streaming with MCP tool filtering
57
- - **Concurrent sessions** — run parent + subagent requests in parallel
59
+ - **Concurrent sessions** — run parent and subagent requests in parallel
60
+ - **Subagent model selection** — primary agents get 1M context; subagents get 200k, preserving rate-limit budget
61
+ - **Auto token refresh** — expired OAuth tokens are refreshed automatically; requests continue without interruption
58
62
  - **Passthrough mode** — forward tool calls to the client instead of executing internally
59
63
  - **Multimodal** — images, documents, and file attachments pass through to Claude
60
64
  - **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
65
 
64
66
  ## Agent Setup
65
67
 
66
68
  ### OpenCode
67
69
 
70
+ **Step 1: Run `meridian setup` (required, one time)**
71
+
72
+ ```bash
73
+ meridian setup
74
+ ```
75
+
76
+ This adds the Meridian plugin to your OpenCode global config (`~/.config/opencode/opencode.json`). The plugin enables:
77
+
78
+ - **Session tracking** — reliable conversation continuity across requests
79
+ - **Subagent model selection** — primary agents use `sonnet[1m]`; subagents automatically use `sonnet` (200k), preserving your 1M context rate-limit budget
80
+
81
+ If the plugin is missing, Meridian warns at startup and reports `"plugin": "not-configured"` in the health endpoint.
82
+
83
+ **Step 2: Start**
84
+
68
85
  ```bash
69
86
  ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
70
87
  ```
71
88
 
72
- For automatic session tracking, use a plugin like [opencode-with-claude](https://github.com/ianjwhite99/opencode-with-claude), or see the [reference plugin](examples/opencode-plugin/claude-max-headers.ts) to build your own.
89
+ Or set these in your shell profile so they're always active:
90
+
91
+ ```bash
92
+ export ANTHROPIC_API_KEY=x
93
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
94
+ ```
73
95
 
74
96
  ### Crush
75
97
 
@@ -94,75 +116,38 @@ Add a provider to `~/.config/crush/crush.json`:
94
116
  }
95
117
  ```
96
118
 
97
- Then use Meridian models in Crush:
98
-
99
119
  ```bash
100
120
  crush run --model meridian/claude-sonnet-4-6 "refactor this function"
101
121
  crush --model meridian/claude-opus-4-6 # interactive TUI
102
122
  ```
103
123
 
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.
124
+ Crush is automatically detected from its `Charm-Crush/` User-Agent — no plugin needed.
105
125
 
106
126
  ### Droid (Factory AI)
107
127
 
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`:
128
+ Add Meridian as a custom model provider in `~/.factory/settings.json`:
111
129
 
112
130
  ```json
113
131
  {
114
132
  "customModels": [
115
- {
116
- "model": "claude-sonnet-4-6",
117
- "name": "Sonnet 4.6 (1M — Meridian)",
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
- }
133
+ { "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
134
+ { "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
135
+ { "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
136
136
  ]
137
137
  }
138
138
  ```
139
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.
140
+ Then pick any `custom:claude-*` model in the Droid TUI. No plugin needed Droid is automatically detected.
154
141
 
155
142
  ### Cline
156
143
 
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:**
144
+ **1. Authenticate:**
160
145
 
161
146
  ```bash
162
147
  cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
163
148
  ```
164
149
 
165
- **2. Add the proxy base URL** to `~/.cline/data/globalState.json`:
150
+ **2. Set the proxy URL** in `~/.cline/data/globalState.json`:
166
151
 
167
152
  ```json
168
153
  {
@@ -172,81 +157,93 @@ cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
172
157
  }
173
158
  ```
174
159
 
175
- **3. Run Cline:**
160
+ **3. Run:**
176
161
 
177
162
  ```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)
163
+ cline --yolo "refactor the login function"
181
164
  ```
182
165
 
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.
166
+ No plugin needed — Cline uses the standard Anthropic SDK.
184
167
 
185
168
  ### Aider
186
169
 
187
- Aider works out of the box — no plugin or config file needed:
188
-
189
170
  ```bash
190
171
  ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
191
172
  aider --model anthropic/claude-sonnet-4-5-20250929
192
173
  ```
193
174
 
194
- All standard aider features work: file editing, repo-map, git integration, multi-file changes.
175
+ > **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
176
+
177
+ ### OpenAI-compatible tools (Open WebUI, Continue, etc.)
178
+
179
+ Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
180
+
181
+ **`POST /v1/chat/completions`** — accepts OpenAI chat format, returns OpenAI completion format (streaming and non-streaming)
182
+
183
+ **`GET /v1/models`** — returns available Claude models in OpenAI format
184
+
185
+ Point any OpenAI-compatible tool at `http://127.0.0.1:3456` with any API key value:
195
186
 
196
- > **Note:** Aider's `--no-stream` flag is incompatible due to a litellm parsing issue — use the default streaming mode (no flag needed).
187
+ ```bash
188
+ # Open WebUI: set OpenAI API base to http://127.0.0.1:3456, API key to any value
189
+ # Continue: set apiBase to http://127.0.0.1:3456 with provider: openai
190
+ # Any OpenAI SDK: set base_url="http://127.0.0.1:3456", api_key="dummy"
191
+ ```
192
+
193
+ > **Note:** Multi-turn conversations work by packing prior turns into the system prompt. Each request is a fresh SDK session — OpenAI clients replay full history themselves and don't use Meridian's session resumption.
197
194
 
198
195
  ### Any Anthropic-compatible tool
199
196
 
200
197
  ```bash
201
198
  export ANTHROPIC_API_KEY=x
202
199
  export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
203
- # Then start your tool normally
204
200
  ```
205
201
 
206
202
  ## Tested Agents
207
203
 
208
- | Agent | Status | Plugin | Notes |
209
- |-------|--------|--------|-------|
210
- | [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | [opencode-with-claude](https://github.com/ianjwhite99/opencode-with-claude) | Full tool support, session resume, streaming, subagents |
211
- | [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see setup above) | Full tool support, session resume, streaming; one-time BYOK setup |
212
- | [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see setup above) | Full tool support, session resume, streaming, headless `crush run` |
213
- | [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see setup above) | Full tool support, file read/write/edit, bash, session resume, all models |
214
- | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | — | Should work standard Anthropic API |
215
- | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars (see setup above) | File editing, streaming; `--no-stream` broken (litellm bug) |
204
+ | Agent | Status | Notes |
205
+ |-------|--------|-------|
206
+ | [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | Requires `meridian setup` — full tool support, session resume, streaming, subagents |
207
+ | [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see above) full tool support, session resume, streaming |
208
+ | [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see above) full tool support, session resume, headless `crush run` |
209
+ | [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see above) full tool support, file read/write/edit, bash, session resume |
210
+ | [Aider](https://github.com/paul-gauthier/aider) | Verified | Env vars file editing, streaming; `--no-stream` broken (litellm bug) |
211
+ | [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints set base URL to `http://127.0.0.1:3456` |
212
+ | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
216
213
 
217
214
  Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
218
215
 
219
216
  ## Architecture
220
217
 
221
- Meridian is built as a modular proxy with clean separation of concerns:
222
-
223
218
  ```
224
219
  src/proxy/
225
220
  ├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
226
- ├── adapter.ts ← AgentAdapter interface (extensibility point)
221
+ ├── adapter.ts ← AgentAdapter interface
227
222
  ├── adapters/
228
223
  │ ├── detect.ts ← Agent detection from request headers
229
224
  │ ├── opencode.ts ← OpenCode adapter
230
- │ ├── crush.ts ← Crush (Charm) adapter
231
- │ ├── droid.ts ← Droid (Factory AI) adapter
225
+ │ ├── crush.ts ← Crush adapter
226
+ │ ├── droid.ts ← Droid adapter
232
227
  │ └── passthrough.ts ← LiteLLM passthrough adapter
233
228
  ├── query.ts ← SDK query options builder
234
229
  ├── errors.ts ← Error classification
235
- ├── models.ts ← Model mapping (sonnet/opus/haiku)
236
- ├── tools.ts Tool blocking lists
237
- ├── messages.ts Content normalization
230
+ ├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
231
+ ├── tokenRefresh.ts Cross-platform OAuth token refresh
232
+ ├── openai.ts OpenAI ↔ Anthropic format translation (pure)
233
+ ├── setup.ts ← OpenCode plugin configuration
238
234
  ├── session/
239
235
  │ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
240
236
  │ ├── fingerprint.ts ← Conversation fingerprinting
241
237
  │ └── cache.ts ← LRU session caches
242
238
  ├── sessionStore.ts ← Cross-proxy file-based session persistence
243
- ├── agentDefs.ts ← Subagent definition extraction
244
239
  └── passthroughTools.ts ← Tool forwarding mode
240
+ plugin/
241
+ └── meridian.ts ← OpenCode plugin (session headers + agent mode)
245
242
  ```
246
243
 
247
244
  ### Session Management
248
245
 
249
- Sessions map agent conversations to Claude SDK sessions. Meridian classifies every incoming request:
246
+ Every incoming request is classified:
250
247
 
251
248
  | Classification | What Happened | Action |
252
249
  |---------------|---------------|--------|
@@ -257,30 +254,9 @@ Sessions map agent conversations to Claude SDK sessions. Meridian classifies eve
257
254
 
258
255
  Sessions are stored in-memory (LRU) and persisted to `~/.cache/meridian/sessions.json` for cross-proxy resume.
259
256
 
260
- ### Adding a New Agent
261
-
262
- Implement the `AgentAdapter` interface in `src/proxy/adapters/`:
257
+ ### Agent Detection
263
258
 
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
- ```
282
-
283
- Agent detection is automatic from the `User-Agent` header:
259
+ Agents are identified from request headers automatically:
284
260
 
285
261
  | User-Agent prefix | Adapter |
286
262
  |---|---|
@@ -288,7 +264,9 @@ Agent detection is automatic from the `User-Agent` header:
288
264
  | `factory-cli/` | Droid |
289
265
  | *(anything else)* | OpenCode (default) |
290
266
 
291
- See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for reference.
267
+ ### Adding a New Agent
268
+
269
+ Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for a reference.
292
270
 
293
271
  ## Configuration
294
272
 
@@ -304,103 +282,75 @@ See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode
304
282
  | `MERIDIAN_IDLE_TIMEOUT_SECONDS` | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
305
283
  | `MERIDIAN_TELEMETRY_SIZE` | `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
306
284
  | `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 frequently |
285
+ | `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
286
 
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`.
341
-
342
- ### Plugin Architecture
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.
287
+ *`sonnet[1m]` requires Max subscription with Extra Usage enabled. Falls back to `sonnet` automatically if not available.
368
288
 
369
289
  ## Endpoints
370
290
 
371
291
  | Endpoint | Description |
372
292
  |----------|-------------|
373
- | `GET /` | Landing page (HTML) or status JSON (`Accept: application/json`) |
293
+ | `GET /` | Landing page |
374
294
  | `POST /v1/messages` | Anthropic Messages API |
375
295
  | `POST /messages` | Alias for `/v1/messages` |
376
- | `GET /health` | Auth status, subscription type, mode |
296
+ | `POST /v1/chat/completions` | OpenAI-compatible chat completions |
297
+ | `GET /v1/models` | OpenAI-compatible model list |
298
+ | `GET /health` | Auth status, mode, plugin status |
377
299
  | `POST /auth/refresh` | Manually refresh the OAuth token |
378
300
  | `GET /telemetry` | Performance dashboard |
379
301
  | `GET /telemetry/requests` | Recent request metrics (JSON) |
380
302
  | `GET /telemetry/summary` | Aggregate statistics (JSON) |
381
303
  | `GET /telemetry/logs` | Diagnostic logs (JSON) |
382
304
 
383
- ## Docker
305
+ Health response example:
384
306
 
385
- ```bash
386
- docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
307
+ ```json
308
+ {
309
+ "status": "healthy",
310
+ "auth": { "loggedIn": true, "email": "you@example.com", "subscriptionType": "max" },
311
+ "mode": "internal",
312
+ "plugin": { "opencode": "configured" }
313
+ }
387
314
  ```
388
315
 
389
- Or with docker-compose:
316
+ `plugin.opencode` is `"configured"` when `meridian setup` has been run, `"not-configured"` otherwise.
317
+
318
+ ## CLI Commands
319
+
320
+ | Command | Description |
321
+ |---------|-------------|
322
+ | `meridian` | Start the proxy server |
323
+ | `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
324
+ | `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
325
+
326
+ ## Programmatic API
327
+
328
+ ```typescript
329
+ import { startProxyServer } from "@rynfar/meridian"
330
+
331
+ const instance = await startProxyServer({
332
+ port: 3456,
333
+ host: "127.0.0.1",
334
+ silent: true,
335
+ })
336
+
337
+ // instance.server — underlying http.Server
338
+ await instance.close()
339
+ ```
340
+
341
+ ## Docker
390
342
 
391
343
  ```bash
392
- docker compose up -d
344
+ docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
393
345
  ```
394
346
 
395
347
  ## Testing
396
348
 
397
349
  ```bash
398
- npm test # 522 unit/integration tests (bun test)
399
- npm run build # Build with bun + tsc
350
+ npm test # unit + integration tests
351
+ npm run build # build with bun + tsc
400
352
  ```
401
353
 
402
- Three test tiers:
403
-
404
354
  | Tier | What | Speed |
405
355
  |------|------|-------|
406
356
  | Unit | Pure functions, no mocks | Fast |
@@ -410,30 +360,29 @@ Three test tiers:
410
360
  ## FAQ
411
361
 
412
362
  **Is this allowed by Anthropic's terms?**
413
- 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.
363
+ 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
364
 
415
365
  **How is this different from using an API key?**
416
- 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.
366
+ API keys are billed per token. Claude Max is a flat monthly fee. Meridian lets you use that subscription from any compatible tool.
417
367
 
418
- **Does it work with Claude Pro?**
419
- It works with any Claude subscription that supports the Claude Code SDK. Max is recommended for the best rate limits.
368
+ **What happens if my OAuth token expires?**
369
+ 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
370
 
421
- **What happens if my session expires?**
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:
371
+ **Can I trigger a token refresh manually?**
426
372
 
427
373
  ```bash
428
- # CLI (works whether the proxy is running or not)
374
+ # CLI works whether the proxy is running or not
429
375
  meridian refresh-token
430
376
 
431
- # HTTP endpoint (while the proxy is running)
432
- curl -s -X POST http://127.0.0.1:3456/auth/refresh
433
- # {"success":true,"message":"OAuth token refreshed successfully"}
377
+ # HTTP while the proxy is running
378
+ curl -X POST http://127.0.0.1:3456/auth/refresh
434
379
  ```
435
380
 
436
- The CLI exits 0 on success and 1 on failure, so it integrates cleanly into scripts or health checks.
381
+ **I'm hitting rate limits on 1M context. What do I do?**
382
+ 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.
383
+
384
+ **Why does the health endpoint show `"plugin": "not-configured"`?**
385
+ 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
386
 
438
387
  ## Contributing
439
388
 
@@ -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 { __export, __require, withClaudeLogContext, claudeLog, createPlatformCredentialStore, refreshOAuthToken, resetInflightRefresh };
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 };