@rynfar/meridian 1.43.0 → 1.44.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.
Files changed (39) hide show
  1. package/README.md +44 -9
  2. package/dist/{cli-7k1fcprd.js → cli-1kbcm3yn.js} +37 -20
  3. package/dist/{cli-1x5gqsqc.js → cli-6rezv582.js} +206 -64
  4. package/dist/{cli-rtab0qa6.js → cli-je60fevk.js} +38 -20
  5. package/dist/cli.js +25 -18
  6. package/dist/env.d.ts +16 -0
  7. package/dist/env.d.ts.map +1 -1
  8. package/dist/{profileCli-0h4nc2h8.js → profileCli-xcmmr5w4.js} +208 -46
  9. package/dist/proxy/adapters/claudecode.d.ts.map +1 -1
  10. package/dist/proxy/adapters/detect.d.ts.map +1 -1
  11. package/dist/proxy/adapters/droid.d.ts.map +1 -1
  12. package/dist/proxy/adapters/openai.d.ts +23 -0
  13. package/dist/proxy/adapters/openai.d.ts.map +1 -0
  14. package/dist/proxy/adapters/opencode.d.ts.map +1 -1
  15. package/dist/proxy/adapters/pi.d.ts.map +1 -1
  16. package/dist/proxy/effort.d.ts +21 -0
  17. package/dist/proxy/effort.d.ts.map +1 -0
  18. package/dist/proxy/openai.d.ts +12 -0
  19. package/dist/proxy/openai.d.ts.map +1 -1
  20. package/dist/proxy/query.d.ts +3 -2
  21. package/dist/proxy/query.d.ts.map +1 -1
  22. package/dist/proxy/sdkFeatures.d.ts.map +1 -1
  23. package/dist/proxy/server.d.ts +1 -0
  24. package/dist/proxy/server.d.ts.map +1 -1
  25. package/dist/proxy/setup.d.ts +9 -0
  26. package/dist/proxy/setup.d.ts.map +1 -1
  27. package/dist/proxy/streamIdleGuard.d.ts +28 -0
  28. package/dist/proxy/streamIdleGuard.d.ts.map +1 -0
  29. package/dist/proxy/tokenRefresh.d.ts +2 -0
  30. package/dist/proxy/tokenRefresh.d.ts.map +1 -1
  31. package/dist/proxy/transforms/opencode.d.ts.map +1 -1
  32. package/dist/proxy/transforms/pi.d.ts.map +1 -1
  33. package/dist/proxy/transforms/registry.d.ts.map +1 -1
  34. package/dist/proxy/types.d.ts +7 -0
  35. package/dist/proxy/types.d.ts.map +1 -1
  36. package/dist/server.js +5 -3
  37. package/dist/{setup-v5pnqe04.js → setup-6c11e8d6.js} +4 -2
  38. package/dist/{tokenRefresh-swetnf89.js → tokenRefresh-pvc2q8ea.js} +1 -1
  39. package/package.json +4 -3
package/README.md CHANGED
@@ -197,7 +197,7 @@ The system prompt controls are independent — any combination works:
197
197
 
198
198
  The core question is **who executes the tools** — the SDK or the client?
199
199
 
200
- - **Passthrough mode** (default for OpenCode) — Claude generates tool calls, but Meridian captures them and sends them back to the client for execution. The client runs the tool using its own implementation, with its own sandboxing, file tracking, and UI, then sends the result in the next request. This is how OpenCode, oh-my-opencagent (OMO), and most coding agents work — they have their own read/write/bash tools and need to stay in control of what runs on the user's machine.
200
+ - **Passthrough mode** (default for OpenCode and Pi) — Claude generates tool calls, but Meridian captures them and sends them back to the client for execution. The client runs the tool using its own implementation, with its own sandboxing, file tracking, and UI, then sends the result in the next request. This is how OpenCode, oh-my-opencagent (OMO), and most coding agents work — they have their own read/write/bash tools and need to stay in control of what runs on the user's machine.
201
201
  - **Internal mode** — Claude Code handles everything. The SDK executes tools directly on the host, runs its full agent loop, and returns the final result. This is for clients that are purely chat interfaces (Open WebUI, simple API consumers) with no tool execution of their own.
202
202
 
203
203
  Most users don't need to configure anything — the adapter sets the right mode automatically. To override:
@@ -239,6 +239,20 @@ meridian profile add work
239
239
 
240
240
  > **⚠ Important:** Claude's OAuth reuses your browser session. Before adding a second account, sign out of claude.ai and sign into the other account first.
241
241
 
242
+ #### Headless / SSH: complete Claude OAuth with a pasted code
243
+
244
+ When you still want a normal Claude Max browser-login profile but the Meridian host cannot open a browser (SSH, WSL, containers, remote servers), use `--headless`. Meridian prints a Claude OAuth URL, prompts for the returned code, exchanges it with PKCE, and saves the resulting credentials into the profile's isolated `CLAUDE_CONFIG_DIR`:
245
+
246
+ ```bash
247
+ meridian profile add work --headless
248
+ ```
249
+
250
+ Open the printed URL in a browser, sign in to the target Claude account, then paste the returned code at Meridian's `Paste code:` prompt. For an existing browser-login profile:
251
+
252
+ ```bash
253
+ meridian profile login work --headless
254
+ ```
255
+
242
256
  #### Headless / CI: register an OAuth token
243
257
 
244
258
  When a browser isn't available (containers, CI runners, remote shells), generate a long-lived OAuth token with `claude setup-token` and register it as a profile:
@@ -269,11 +283,11 @@ You can also switch profiles from the web UI at `http://127.0.0.1:3456/profiles`
269
283
 
270
284
  | Command | Description |
271
285
  |---------|-------------|
272
- | `meridian profile add <name>` | Add a profile and authenticate via browser |
286
+ | `meridian profile add <name> [--headless]` | Add a profile and authenticate via Claude OAuth; `--headless` prints a URL, prompts for the returned code, and stores the exchanged credentials |
273
287
  | `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
274
288
  | `meridian profile list` | List profiles and auth status |
275
289
  | `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
276
- | `meridian profile login <name>` | Re-authenticate an expired profile (browser-login profiles only) |
290
+ | `meridian profile login <name> [--headless]` | Re-authenticate an expired profile (browser-login profiles only); `--headless` uses the URL/code flow |
277
291
  | `meridian profile remove <name>` | Remove a profile and its credentials |
278
292
 
279
293
  ### How it works
@@ -361,9 +375,11 @@ Add a provider to `~/.config/crush/crush.json`:
361
375
  "base_url": "http://127.0.0.1:3456",
362
376
  "api_key": "dummy",
363
377
  "models": [
364
- { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true },
365
- { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
366
- { "id": "claude-haiku-4-5-20251001", "name": "Claude Haiku 4.5", "context_window": 200000, "default_max_tokens": 16384, "can_reason": true, "supports_attachments": true }
378
+ { "id": "claude-opus-4-8", "name": "Claude Opus 4.8 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
379
+ { "id": "claude-opus-4-7", "name": "Claude Opus 4.7 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
380
+ { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true },
381
+ { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
382
+ { "id": "claude-haiku-4-5-20251001", "name": "Claude Haiku 4.5", "context_window": 200000, "default_max_tokens": 16384, "can_reason": true, "supports_attachments": true }
367
383
  ]
368
384
  }
369
385
  }
@@ -384,6 +400,8 @@ Add Meridian as a custom model provider in `~/.factory/settings.json`:
384
400
  ```json
385
401
  {
386
402
  "customModels": [
403
+ { "model": "claude-opus-4-8", "name": "Opus 4.8 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
404
+ { "model": "claude-opus-4-7", "name": "Opus 4.7 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
387
405
  { "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
388
406
  { "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
389
407
  { "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
@@ -508,6 +526,8 @@ Pi uses the `@mariozechner/pi-ai` library which supports a configurable `baseUrl
508
526
 
509
527
  Pi mimics Claude Code's User-Agent, so automatic detection isn't possible. The `x-meridian-agent: pi` header in the config above tells Meridian to use the Pi adapter. Alternatively, if Pi is your only agent, you can set `MERIDIAN_DEFAULT_AGENT=pi` as an env var instead.
510
528
 
529
+ Pi runs in passthrough mode by default — it executes its own tools and Meridian just forwards the `tool_use` blocks. Opt out with `MERIDIAN_PASSTHROUGH=0`.
530
+
511
531
  ### Claude Code
512
532
 
513
533
  Claude Code can point at Meridian like any other Anthropic API client. The
@@ -555,7 +575,7 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
555
575
  | [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see above) — full tool support, file read/write/edit, bash, session resume |
556
576
  | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
557
577
  | [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
558
- | [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see above) — requires `MERIDIAN_DEFAULT_AGENT=pi` |
578
+ | [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see above) — full tool support via passthrough; detected via `x-meridian-agent: pi` header |
559
579
  | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✅ Verified | `ANTHROPIC_BASE_URL` — remote clients share a Max subscription over the network; client CWD preserved in system prompt |
560
580
  | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
561
581
 
@@ -734,11 +754,11 @@ export default {
734
754
  |---------|-------------|
735
755
  | `meridian` | Start the proxy server |
736
756
  | `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
737
- | `meridian profile add <name>` | Add a profile and authenticate via browser |
757
+ | `meridian profile add <name> [--headless]` | Add a profile and authenticate via Claude OAuth; `--headless` prints a URL, prompts for the returned code, and stores the exchanged credentials |
738
758
  | `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
739
759
  | `meridian profile list` | List all profiles and their auth status |
740
760
  | `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
741
- | `meridian profile login <name>` | Re-authenticate an expired profile (browser-login profiles only) |
761
+ | `meridian profile login <name> [--headless]` | Re-authenticate an expired profile (browser-login profiles only); `--headless` uses the URL/code flow |
742
762
  | `meridian profile remove <name>` | Remove a profile and its credentials |
743
763
  | `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
744
764
 
@@ -840,6 +860,21 @@ meridian refresh-token
840
860
  curl -X POST http://127.0.0.1:3456/auth/refresh
841
861
  ```
842
862
 
863
+ **I'm getting `400 You're out of extra usage` only when tools are present. What do I do?**
864
+ First confirm the failure pattern: a tiny no-tool request succeeds, but the same client fails once it sends tool definitions. If that is the case, beta-header stripping and model fallback usually will not help because the request body still contains agentic tool context.
865
+
866
+ For the affected adapter, try disabling the connecting client's system prompt while keeping the Claude Code prompt enabled:
867
+
868
+ ```bash
869
+ curl -X PATCH http://127.0.0.1:3456/settings/api/features/pi \
870
+ -H 'Content-Type: application/json' \
871
+ -d '{"clientSystemPrompt":false,"codeSystemPrompt":true}'
872
+ ```
873
+
874
+ Replace `pi` with the adapter you use (`opencode`, `crush`, `forgecode`, `droid`, `passthrough`, or `openai`). You can make the same change in the `/settings` UI under **SDK Feature Toggles**. (The `openai` adapter — used by the `/v1/chat/completions` endpoint — already defaults `codeSystemPrompt` to off, so on that one you typically only need `clientSystemPrompt:false`.)
875
+
876
+ This keeps the SDK's preset prompt and tool bridge, but removes the external client's large agent prompt from the request. That may help when the error is triggered by the combination of tool definitions plus client prompt context. The tradeoff is that the connected agent may behave more like vanilla Claude Code because its own persona and workflow instructions are no longer included. If it still fails, the remaining options are to use fewer/no tools for that client, enable Extra Usage/API billing, or switch to a local/provider-backed model for that workflow. See [#516](https://github.com/rynfar/meridian/issues/516) for the current debugging thread.
877
+
843
878
  **I'm hitting rate limits on 1M context. What do I do?**
844
879
  Meridian defaults Sonnet to 200k context because Sonnet 1M is always billed as Extra Usage on Max plans — even when regular usage isn't exhausted. This is [Anthropic's intended billing model](https://code.claude.com/docs/en/model-config#extended-context), not a bug. Set `MERIDIAN_SONNET_MODEL=sonnet[1m]` to opt in if you have Extra Usage enabled and understand the billing implications. Opus defaults to 1M context, which is included with Max/Team/Enterprise subscriptions at no extra cost. Note: there is a [known upstream bug](https://github.com/anthropics/claude-code/issues/39841) where Claude Code incorrectly gates Opus 1M behind Extra Usage on Max — this is Anthropic's to fix.
845
880
 
@@ -1,10 +1,10 @@
1
1
  // src/proxy/tokenRefresh.ts
2
- import { execFile as execFileCb } from "child_process";
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
- import { homedir, platform, userInfo } from "os";
5
- import { join, dirname, resolve } from "path";
6
- import { createHash } from "crypto";
7
- import { promisify } from "util";
2
+ import { execFile as execFileCb } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir, platform, userInfo } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { promisify } from "node:util";
8
8
 
9
9
  // src/logger.ts
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -102,6 +102,7 @@ function parseKeychainValue(raw) {
102
102
  var keychainWasHexByService = new Map;
103
103
  function buildMacosStore(serviceName) {
104
104
  return {
105
+ refreshKey: `keychain:${serviceName}`,
105
106
  async read() {
106
107
  try {
107
108
  const { stdout } = await execFile("/usr/bin/security", ["find-generic-password", "-s", serviceName, "-a", userInfo().username, "-w"], { timeout: 5000 });
@@ -131,24 +132,26 @@ function buildMacosStore(serviceName) {
131
132
  }
132
133
  var macosStore = buildMacosStore(KEYCHAIN_SERVICE);
133
134
  function buildFileStore(filePath) {
135
+ const absPath = resolve(filePath);
134
136
  return {
137
+ refreshKey: `file:${absPath}`,
135
138
  async read() {
136
139
  try {
137
- if (!existsSync(filePath))
140
+ if (!existsSync(absPath))
138
141
  return null;
139
- return JSON.parse(readFileSync(filePath, "utf-8"));
142
+ return JSON.parse(readFileSync(absPath, "utf-8"));
140
143
  } catch (err) {
141
- claudeLog("token_refresh.file_read_failed", { path: filePath, error: String(err) });
144
+ claudeLog("token_refresh.file_read_failed", { path: absPath, error: String(err) });
142
145
  return null;
143
146
  }
144
147
  },
145
148
  async write(credentials) {
146
149
  try {
147
- mkdirSync(dirname(filePath), { recursive: true });
148
- writeFileSync(filePath, serializeCredentials(credentials), "utf-8");
150
+ mkdirSync(dirname(absPath), { recursive: true });
151
+ writeFileSync(absPath, serializeCredentials(credentials), "utf-8");
149
152
  return true;
150
153
  } catch (err) {
151
- claudeLog("token_refresh.file_write_failed", { path: filePath, error: String(err) });
154
+ claudeLog("token_refresh.file_write_failed", { path: absPath, error: String(err) });
152
155
  return false;
153
156
  }
154
157
  }
@@ -167,14 +170,29 @@ function createPlatformCredentialStore(opts) {
167
170
  function credentialsFilePathForProfile(claudeConfigDir) {
168
171
  return claudeConfigDir ? configDirToCredentialsFile(claudeConfigDir) : CREDENTIALS_FILE;
169
172
  }
170
- var inflightRefresh = null;
173
+ var inflightRefreshByKey = new Map;
174
+ var inflightRefreshByStore = new WeakMap;
171
175
  async function refreshOAuthToken(store) {
172
- if (inflightRefresh)
173
- return inflightRefresh;
174
- inflightRefresh = doRefresh(store ?? createPlatformCredentialStore()).finally(() => {
175
- inflightRefresh = null;
176
+ const s = store ?? createPlatformCredentialStore();
177
+ const refreshKey = s.refreshKey;
178
+ if (refreshKey) {
179
+ const inflight2 = inflightRefreshByKey.get(refreshKey);
180
+ if (inflight2)
181
+ return inflight2;
182
+ const refresh2 = doRefresh(s).finally(() => {
183
+ inflightRefreshByKey.delete(refreshKey);
184
+ });
185
+ inflightRefreshByKey.set(refreshKey, refresh2);
186
+ return refresh2;
187
+ }
188
+ const inflight = inflightRefreshByStore.get(s);
189
+ if (inflight)
190
+ return inflight;
191
+ const refresh = doRefresh(s).finally(() => {
192
+ inflightRefreshByStore.delete(s);
176
193
  });
177
- return inflightRefresh;
194
+ inflightRefreshByStore.set(s, refresh);
195
+ return refresh;
178
196
  }
179
197
  async function doRefresh(store) {
180
198
  const credentials = await store.read();
@@ -273,7 +291,6 @@ async function scheduleNext(store, bufferMs, failureRetryMs, gen) {
273
291
  if (!scheduledRefreshActive || gen !== scheduledRefreshGeneration)
274
292
  return;
275
293
  claudeLog("token_refresh.scheduled", { ok, immediate: true });
276
- console.error(`[token_refresh] scheduled refresh (immediate) ok=${ok}`);
277
294
  armTimer(ok ? 0 : failureRetryMs, store, bufferMs, failureRetryMs, gen);
278
295
  return;
279
296
  }
@@ -293,7 +310,7 @@ function isBackgroundRefreshActive() {
293
310
  return scheduledRefreshActive;
294
311
  }
295
312
  function resetInflightRefresh() {
296
- inflightRefresh = null;
313
+ inflightRefreshByKey.clear();
297
314
  }
298
315
 
299
316
  export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };