@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 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 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
- # 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,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 + subagent requests in parallel
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
- 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.
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 extra configuration needed. In `crush run` headless mode all tool operations (read, write, bash) execute automatically without prompting.
123
+ Crush is automatically detected from its `Charm-Crush/` User-Agent — no plugin needed.
105
124
 
106
125
  ### Droid (Factory AI)
107
126
 
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`:
127
+ Add Meridian as a custom model provider in `~/.factory/settings.json`:
111
128
 
112
129
  ```json
113
130
  {
114
131
  "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
- }
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
- 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.
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
- Cline CLI connects by setting `anthropicBaseUrl` in its config. This is a one-time setup.
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. Add the proxy base URL** to `~/.cline/data/globalState.json`:
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 Cline:**
159
+ **3. Run:**
176
160
 
177
161
  ```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)
162
+ cline --yolo "refactor the login function"
181
163
  ```
182
164
 
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.
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
- All standard aider features work: file editing, repo-map, git integration, multi-file changes.
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 | 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) |
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 (extensibility point)
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 (Charm) adapter
231
- │ ├── droid.ts ← Droid (Factory AI) adapter
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
- ├── tools.ts Tool blocking lists
237
- ├── messages.ts Content normalization
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
- Sessions map agent conversations to Claude SDK sessions. Meridian classifies every incoming request:
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
- ### Adding a New Agent
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
- Agent detection is automatic from the `User-Agent` header:
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
- See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for reference.
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 frequently |
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
- ### 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.
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 (HTML) or status JSON (`Accept: application/json`) |
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, subscription type, mode |
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
- ## Docker
282
+ Health response example:
384
283
 
385
- ```bash
386
- docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
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
- Or with docker-compose:
318
+ ## Docker
390
319
 
391
320
  ```bash
392
- docker compose up -d
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 # 522 unit/integration tests (bun test)
399
- npm run build # Build with bun + tsc
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 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.
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. Your Max subscription is a flat monthly fee with higher rate limits. Meridian lets you use that subscription from any compatible tool.
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
- **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.
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
- **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:
348
+ **Can I trigger a token refresh manually?**
426
349
 
427
350
  ```bash
428
- # CLI (works whether the proxy is running or not)
351
+ # CLI works whether the proxy is running or not
429
352
  meridian refresh-token
430
353
 
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"}
354
+ # HTTP while the proxy is running
355
+ curl -X POST http://127.0.0.1:3456/auth/refresh
434
356
  ```
435
357
 
436
- The CLI exits 0 on success and 1 on failure, so it integrates cleanly into scripts or health checks.
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 { __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 };