@rynfar/meridian 1.52.0 → 1.54.0
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 +84 -16
- package/dist/{profileCli-s1h4vh8w.js → cli-jhm27q0x.js} +346 -31
- package/dist/{cli-tq3vtp0q.js → cli-mqht6rs9.js} +905 -61
- package/dist/cli.js +3 -3
- package/dist/grepTool.d.ts +23 -0
- package/dist/grepTool.d.ts.map +1 -0
- package/dist/mcpTools.d.ts.map +1 -1
- package/dist/profileCli-d9tcp916.js +35 -0
- package/dist/proxy/adapters/codex.d.ts +22 -0
- package/dist/proxy/adapters/codex.d.ts.map +1 -0
- package/dist/proxy/adapters/detect.d.ts.map +1 -1
- package/dist/proxy/design.d.ts +82 -0
- package/dist/proxy/design.d.ts.map +1 -0
- package/dist/proxy/openai.d.ts +5 -0
- package/dist/proxy/openai.d.ts.map +1 -1
- package/dist/proxy/openaiResponses.d.ts +137 -0
- package/dist/proxy/openaiResponses.d.ts.map +1 -0
- package/dist/proxy/profileCli.d.ts +48 -0
- package/dist/proxy/profileCli.d.ts.map +1 -0
- package/dist/proxy/sdkFeatures.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/transforms/codex.d.ts +12 -0
- package/dist/proxy/transforms/codex.d.ts.map +1 -0
- package/dist/proxy/transforms/opencode.d.ts.map +1 -1
- package/dist/proxy/transforms/registry.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/plugin/meridian.ts +58 -13
- package/dist/cli-vjeftz4z.js +0 -329
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Then in `~/.config/opencode/opencode.json`:
|
|
|
99
99
|
|
|
100
100
|
> **Important:** Do not use `meridian setup` on NixOS. It writes an absolute Nix store path (e.g. `/nix/store/...-meridian-1.x.x/lib/...`) into your OpenCode config, which will break on the next `nixos-rebuild switch` or `home-manager switch` when the store path changes. Use one of the approaches above instead.
|
|
101
101
|
|
|
102
|
-
> **Note:**
|
|
102
|
+
> **Note:** Meridian's package depends on the unfree `claude-code` from nixpkgs instead of bundling its own binary. The flake accepts the unfree license when it builds the package and exports the finished derivation, so consuming it through the overlay or `packages.<system>.meridian` does not re-run nixpkgs' unfree check and needs no `allowUnfree` setting.
|
|
103
103
|
|
|
104
104
|
**Home Manager service** -- run Meridian as a user systemd service:
|
|
105
105
|
|
|
@@ -122,8 +122,8 @@ Then in `~/.config/opencode/opencode.json`:
|
|
|
122
122
|
# defaultAgent = "opencode";
|
|
123
123
|
# sonnetModel = "sonnet";
|
|
124
124
|
# Load plugins from the Nix store (rendered to a plugins.json manifest).
|
|
125
|
-
#
|
|
126
|
-
# pluginConfig = [ { path =
|
|
125
|
+
# The official scrub plugins ship prebuilt via the meridian overlay:
|
|
126
|
+
# pluginConfig = [ { path = pkgs.meridianPlugins.opencode-scrub.path; } ];
|
|
127
127
|
# pluginDir = "/path/to/extra/plugins";
|
|
128
128
|
};
|
|
129
129
|
# Extra env vars not covered by settings
|
|
@@ -497,6 +497,30 @@ ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
|
|
|
497
497
|
|
|
498
498
|
> **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
|
|
499
499
|
|
|
500
|
+
### Codex CLI
|
|
501
|
+
|
|
502
|
+
Codex CLI ≥ 0.96 dropped `wire_api = "chat"` and speaks only the OpenAI **Responses API** (`/v1/responses`), which Meridian serves. Add a provider to `~/.codex/config.toml`:
|
|
503
|
+
|
|
504
|
+
```toml
|
|
505
|
+
model = "claude-sonnet-5"
|
|
506
|
+
model_provider = "meridian"
|
|
507
|
+
|
|
508
|
+
[model_providers.meridian]
|
|
509
|
+
name = "Meridian"
|
|
510
|
+
base_url = "http://127.0.0.1:3456/v1"
|
|
511
|
+
wire_api = "responses"
|
|
512
|
+
env_key = "MERIDIAN_KEY" # any value unless MERIDIAN_API_KEY is set
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
MERIDIAN_KEY=x codex "refactor this function"
|
|
517
|
+
MERIDIAN_KEY=x codex exec "run the tests and summarize failures" # non-interactive
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Codex is a tool-driving agent — Meridian runs the `/v1/responses` endpoint in **passthrough** mode automatically (Codex executes its own shell/apply-patch tools), so no `MERIDIAN_PASSTHROUGH` change is needed. A harmless `Model metadata for 'claude-sonnet-5' not found` warning from Codex is expected — it doesn't recognize non-OpenAI model ids but works regardless.
|
|
521
|
+
|
|
522
|
+
`model_reasoning_effort` is supported and won't stall the CLI, but Claude's private thinking isn't yet carried **across** turns — the Responses API's encrypted-reasoning envelope is OpenAI-specific and incompatible with Claude's signed thinking blocks, so cross-turn reasoning continuity is deferred (each turn still reasons with full context including tool results). Verified on Codex 0.144 with plain, tool-driving, and reasoning-enabled turns.
|
|
523
|
+
|
|
500
524
|
### OpenAI-compatible tools (Open WebUI, Continue, etc.)
|
|
501
525
|
|
|
502
526
|
Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
|
|
@@ -643,6 +667,47 @@ Run several configurations of the same adapter side by side — e.g. a passthrou
|
|
|
643
667
|
|
|
644
668
|
Built-in adapter names are reserved and can't be shadowed. With no instances configured, detection is exactly the built-in chain. Config file changes apply within ~5s, no restart needed.
|
|
645
669
|
|
|
670
|
+
### Claude Design MCP
|
|
671
|
+
|
|
672
|
+
Meridian proxies the Claude Design MCP API (`api.anthropic.com/v1/design/*`), so MCP clients can use Claude Design tools through your local endpoint.
|
|
673
|
+
|
|
674
|
+
**1. Add the MCP server.** For Claude Code:
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
claude mcp add -s user --transport http claude-design http://127.0.0.1:3456/v1/design/mcp
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Any other MCP client: point it at `http://127.0.0.1:3456/v1/design/mcp` (streamable HTTP).
|
|
681
|
+
|
|
682
|
+
**2. Grant Claude Design consent (one time, per Anthropic account).** Tool calls return a `needs_consent` error until you enable it: open [claude.ai/design/settings](https://claude.ai/design/settings), find **"Claude product access"** ("Let other Claude products, like Claude Code, read and edit your Design projects"), and switch it **On**. This is a setting on the Anthropic account itself — with multiple Meridian profiles, the account behind the *profile handling the request* is the one that needs the toggle.
|
|
683
|
+
|
|
684
|
+
That's it — your existing Claude Max login covers auth (`initialize`, `tools/list`, and tool calls are all verified working with a plain Max token). Verify with a quick handshake:
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
curl -s -X POST http://127.0.0.1:3456/v1/design/mcp -H 'content-type: application/json' \
|
|
688
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Multiple profiles:** design requests use the active profile by default. To pin design traffic to a specific profile regardless of which is active, register the server with a profile header:
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
claude mcp add -s user --transport http --header "x-meridian-profile: personal" \
|
|
695
|
+
claude-design http://127.0.0.1:3456/v1/design/mcp
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Fallback OAuth flow:** if the upstream ever rejects your token with an `auth_error` (scope enforcement has varied over time), `/design-login` obtains a dedicated token with the `user:design:read`/`user:design:write` scopes:
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
curl http://127.0.0.1:3456/design-login # returns an authorize URL — open it in your browser
|
|
702
|
+
curl -X POST http://127.0.0.1:3456/design-login \
|
|
703
|
+
-H 'content-type: application/json' \
|
|
704
|
+
-d '{"code": "<code-from-browser>"}' # paste the code you were shown
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
The design token is stored at `~/.config/meridian/design-token.json` (mode `0600`, global across profiles) and refreshed automatically when it expires.
|
|
708
|
+
|
|
709
|
+
> Contributed by [@sittitep](https://github.com/sittitep) (#543).
|
|
710
|
+
|
|
646
711
|
### Any Anthropic-compatible tool
|
|
647
712
|
|
|
648
713
|
```bash
|
|
@@ -664,6 +729,7 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
|
664
729
|
| [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 |
|
|
665
730
|
| [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 |
|
|
666
731
|
| [Cherry Studio](https://github.com/CherryHQ/cherry-studio) | ✅ Verified | `cherry` adapter (see above) — chat client with Claude's built-in web search via internal mode |
|
|
732
|
+
| [Codex CLI](https://github.com/openai/codex) | ✅ Verified | `/v1/responses` (see above) — Responses-API provider, passthrough tool execution; verified on 0.144 (plain + tool-driving turns) |
|
|
667
733
|
| [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
|
|
668
734
|
|
|
669
735
|
Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
|
|
@@ -684,12 +750,14 @@ src/proxy/
|
|
|
684
750
|
│ ├── cherry.ts ← Cherry Studio adapter (internal mode + web search)
|
|
685
751
|
│ ├── claudecode.ts ← Claude Code adapter (remote clients sharing a Max host)
|
|
686
752
|
│ ├── openai.ts ← OpenAI-endpoint adapter (/v1/chat/completions)
|
|
753
|
+
│ ├── codex.ts ← Codex CLI adapter (/v1/responses, forced passthrough)
|
|
687
754
|
│ └── passthrough.ts ← LiteLLM passthrough adapter
|
|
688
755
|
├── query.ts ← SDK query options builder
|
|
689
756
|
├── errors.ts ← Error classification
|
|
690
757
|
├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
|
|
691
758
|
├── tokenRefresh.ts ← Cross-platform OAuth token refresh
|
|
692
759
|
├── openai.ts ← OpenAI ↔ Anthropic format translation (pure)
|
|
760
|
+
├── openaiResponses.ts ← OpenAI Responses API ↔ Anthropic translation (pure)
|
|
693
761
|
├── setup.ts ← OpenCode plugin configuration
|
|
694
762
|
├── session/
|
|
695
763
|
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
|
|
@@ -805,7 +873,10 @@ ANTHROPIC_API_KEY=your-secret-key ANTHROPIC_BASE_URL=http://meridian-host:3456 o
|
|
|
805
873
|
| `POST /v1/messages` | Anthropic Messages API |
|
|
806
874
|
| `POST /messages` | Alias for `/v1/messages` |
|
|
807
875
|
| `POST /v1/chat/completions` | OpenAI-compatible chat completions |
|
|
876
|
+
| `POST /v1/responses` | OpenAI Responses API (Codex CLI ≥ 0.96) |
|
|
808
877
|
| `GET /v1/models` | OpenAI-compatible model list |
|
|
878
|
+
| `GET/POST /v1/design/*` | Claude Design MCP proxy (see [Claude Design MCP](#claude-design-mcp)) |
|
|
879
|
+
| `GET/POST /design-login` | OAuth flow for the design scopes |
|
|
809
880
|
| `GET /health` | Auth status, mode, plugin status |
|
|
810
881
|
| `POST /auth/refresh` | Manually refresh the OAuth token |
|
|
811
882
|
| `GET /telemetry` | Performance dashboard |
|
|
@@ -868,7 +939,9 @@ opt-in plugins instead:
|
|
|
868
939
|
| [`@rynfar/meridian-plugin-pi-scrub`](https://github.com/rynfar/meridian-plugin-pi-scrub) | Strips Pi's coding-agent-harness prompt line that Anthropic meters as Extra Usage. |
|
|
869
940
|
| [`@rynfar/meridian-plugin-opencode-scrub`](https://github.com/rynfar/meridian-plugin-opencode-scrub) | Strips OpenCode harness boilerplate from the system prompt before it reaches Claude. |
|
|
870
941
|
|
|
871
|
-
|
|
942
|
+
**Nix users:** the flake packages all three prebuilt — `pkgs.meridianPlugins.<name>` via the `meridian` overlay (or `meridian.legacyPackages.${system}.meridianPlugins`), each exposing `.path` for a `plugins.json` entry or the home-manager `pluginConfig` option. Pins are refreshed by a scheduled workflow that rebuilds every plugin before bumping.
|
|
943
|
+
|
|
944
|
+
Everyone else: install into Meridian's config dir and register the built file in
|
|
872
945
|
`~/.config/meridian/plugins.json`:
|
|
873
946
|
|
|
874
947
|
```bash
|
|
@@ -933,6 +1006,8 @@ docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
|
|
|
933
1006
|
|
|
934
1007
|
Meridian refreshes OAuth tokens automatically — once the credentials are mounted, no further browser access is needed.
|
|
935
1008
|
|
|
1009
|
+
> **macOS hosts:** mounting `~/.claude` does **not** carry credentials into the container — on macOS the CLI stores OAuth tokens in the Keychain, not in files, so the container sees an empty credential store and requests fail with an authentication error. Use an [OAuth-token profile](#oauth-token-profiles-in-docker-no-volume-mount) instead (recommended), or run `claude login` once inside the container (`docker exec -it <name> claude login`).
|
|
1010
|
+
|
|
936
1011
|
### Multiple profiles in Docker
|
|
937
1012
|
|
|
938
1013
|
Authenticate each profile locally, then pass them to Docker via the `MERIDIAN_PROFILES` environment variable:
|
|
@@ -1000,20 +1075,13 @@ meridian refresh-token
|
|
|
1000
1075
|
curl -X POST http://127.0.0.1:3456/auth/refresh
|
|
1001
1076
|
```
|
|
1002
1077
|
|
|
1003
|
-
**I'm getting `400 You're out of extra usage`
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
For the affected adapter, try disabling the connecting client's system prompt while keeping the Claude Code prompt enabled:
|
|
1007
|
-
|
|
1008
|
-
```bash
|
|
1009
|
-
curl -X PATCH http://127.0.0.1:3456/settings/api/features/pi \
|
|
1010
|
-
-H 'Content-Type: application/json' \
|
|
1011
|
-
-d '{"clientSystemPrompt":false,"codeSystemPrompt":true}'
|
|
1012
|
-
```
|
|
1078
|
+
**I'm getting `400 You're out of extra usage` on tool-bearing requests. What do I do?**
|
|
1079
|
+
This error class ([#516](https://github.com/rynfar/meridian/issues/516), historical) came from Anthropic's server-side classifier gating certain requests behind Extra Usage. It had two distinct triggers, both now addressed:
|
|
1013
1080
|
|
|
1014
|
-
|
|
1081
|
+
- **Harness fingerprints** — identity lines in a client's system prompt (e.g. pi's "coding agent harness" line) were metered as Extra Usage. The [official scrub plugins](#official-plugins) strip these and remain recommended for the affected harnesses.
|
|
1082
|
+
- **Tool-definition presence** — reported in mid-2026 as triggering independently of prompt content; as of July 2026 this no longer reproduces on Max accounts (verified with Extra Usage disabled, tools present, and an unscrubbed fingerprint prompt). It appears to have been resolved upstream in Anthropic's billing policy.
|
|
1015
1083
|
|
|
1016
|
-
|
|
1084
|
+
If you still hit the error on a current release, first check `GET /v1/usage/quota` to rule out genuinely exhausted quota, then try disabling the connecting client's system prompt for the affected adapter while keeping the Claude Code prompt enabled (in the `/settings` UI under **SDK Feature Toggles**, or `PATCH /settings/api/features/<adapter>` with `{"clientSystemPrompt":false,"codeSystemPrompt":true}`) — and please report it on [#516](https://github.com/rynfar/meridian/issues/516) with your plan type, since remaining occurrences are likely account-cohort specific (Team plans are treated differently by the API).
|
|
1017
1085
|
|
|
1018
1086
|
**I'm hitting rate limits on 1M context. What do I do?**
|
|
1019
1087
|
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.
|
|
@@ -1,22 +1,347 @@
|
|
|
1
|
-
import {
|
|
2
|
-
resolveClaudeExecutableSync
|
|
3
|
-
} from "./cli-vjeftz4z.js";
|
|
4
1
|
import {
|
|
5
2
|
setSetting
|
|
6
3
|
} from "./cli-340h1chz.js";
|
|
7
4
|
import {
|
|
8
5
|
createPlatformCredentialStore
|
|
9
6
|
} from "./cli-aq5zz92m.js";
|
|
10
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
__esm
|
|
9
|
+
} from "./cli-p9swy5t3.js";
|
|
10
|
+
|
|
11
|
+
// src/env.ts
|
|
12
|
+
function env(suffix) {
|
|
13
|
+
return process.env[`MERIDIAN_${suffix}`] ?? process.env[`CLAUDE_PROXY_${suffix}`];
|
|
14
|
+
}
|
|
15
|
+
function envBool(suffix) {
|
|
16
|
+
const val = env(suffix);
|
|
17
|
+
return val === "1" || val === "true" || val === "yes";
|
|
18
|
+
}
|
|
19
|
+
function resolvePassthrough(defaultValue) {
|
|
20
|
+
const val = env("PASSTHROUGH");
|
|
21
|
+
if (val === "1" || val === "true" || val === "yes")
|
|
22
|
+
return true;
|
|
23
|
+
if (val === "0" || val === "false" || val === "no")
|
|
24
|
+
return false;
|
|
25
|
+
return defaultValue;
|
|
26
|
+
}
|
|
27
|
+
function envInt(suffix, defaultValue) {
|
|
28
|
+
const val = env(suffix);
|
|
29
|
+
if (!val)
|
|
30
|
+
return defaultValue;
|
|
31
|
+
const parsed = parseInt(val, 10);
|
|
32
|
+
return Number.isFinite(parsed) ? parsed : defaultValue;
|
|
33
|
+
}
|
|
34
|
+
var init_env = () => {};
|
|
11
35
|
|
|
12
36
|
// src/proxy/profileCli.ts
|
|
13
37
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
14
38
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
39
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync as existsSync2 } from "node:fs";
|
|
16
40
|
import { homedir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
import { join as join2 } from "node:path";
|
|
42
|
+
|
|
43
|
+
// src/proxy/models.ts
|
|
44
|
+
init_env();
|
|
45
|
+
import { exec as execCallback, execFile as execFileCallback } from "child_process";
|
|
46
|
+
import { existsSync, statSync } from "fs";
|
|
47
|
+
import { fileURLToPath } from "url";
|
|
48
|
+
import { join, dirname } from "path";
|
|
49
|
+
import { promisify } from "util";
|
|
50
|
+
var exec = promisify(execCallback);
|
|
51
|
+
var execFile = promisify(execFileCallback);
|
|
52
|
+
var STUB_SIZE_THRESHOLD = 4096;
|
|
53
|
+
var CANONICAL_FABLE_MODEL = "claude-fable-5";
|
|
54
|
+
var CANONICAL_OPUS_MODEL = "claude-opus-4-8";
|
|
55
|
+
var CANONICAL_SONNET_MODEL = "claude-sonnet-5";
|
|
56
|
+
var CANONICAL_HAIKU_MODEL = "claude-haiku-4-5";
|
|
57
|
+
function resolveSdkModelDefaults(env2 = process.env) {
|
|
58
|
+
return {
|
|
59
|
+
ANTHROPIC_DEFAULT_FABLE_MODEL: env2.MERIDIAN_DEFAULT_FABLE_MODEL ?? CANONICAL_FABLE_MODEL,
|
|
60
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: env2.MERIDIAN_DEFAULT_OPUS_MODEL ?? CANONICAL_OPUS_MODEL,
|
|
61
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: env2.MERIDIAN_DEFAULT_SONNET_MODEL ?? CANONICAL_SONNET_MODEL,
|
|
62
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: env2.MERIDIAN_DEFAULT_HAIKU_MODEL ?? CANONICAL_HAIKU_MODEL
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function explicitModelPin(requestedModel) {
|
|
66
|
+
const base = requestedModel.trim().toLowerCase().replace(/\[1m\]$/, "");
|
|
67
|
+
const match = /^claude-(sonnet|opus|haiku|fable|mythos)-\d[\w.-]*$/.exec(base);
|
|
68
|
+
if (!match)
|
|
69
|
+
return;
|
|
70
|
+
const tier = match[1] === "mythos" ? "FABLE" : match[1].toUpperCase();
|
|
71
|
+
return { [`ANTHROPIC_DEFAULT_${tier}_MODEL`]: base };
|
|
72
|
+
}
|
|
73
|
+
var AUTH_STATUS_CACHE_TTL_MS = 60000;
|
|
74
|
+
var AUTH_STATUS_FAILURE_TTL_MS = 5000;
|
|
75
|
+
var cachedAuthStatus = null;
|
|
76
|
+
var lastKnownGoodAuthStatus = null;
|
|
77
|
+
var cachedAuthStatusAt = 0;
|
|
78
|
+
var cachedAuthStatusIsFailure = false;
|
|
79
|
+
var cachedAuthStatusPromise = null;
|
|
80
|
+
function supports1mContext(model) {
|
|
81
|
+
const override = env("1M_CONTEXT_SUPPORT");
|
|
82
|
+
if (override === "0" || override === "false" || override === "no")
|
|
83
|
+
return false;
|
|
84
|
+
if (model.includes("4-5") || model.includes("4.5"))
|
|
85
|
+
return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function mapModelToClaudeModel(model, subscriptionType, agentMode) {
|
|
89
|
+
if (model.includes("haiku"))
|
|
90
|
+
return "haiku";
|
|
91
|
+
const use1m = supports1mContext(model);
|
|
92
|
+
const isSubagent = agentMode === "subagent";
|
|
93
|
+
if (model.includes("fable") || model.includes("mythos")) {
|
|
94
|
+
if (use1m && !isSubagent && !isExtendedContextKnownUnavailable())
|
|
95
|
+
return "fable[1m]";
|
|
96
|
+
return "fable";
|
|
97
|
+
}
|
|
98
|
+
if (model.includes("opus")) {
|
|
99
|
+
if (use1m && !isSubagent && !isExtendedContextKnownUnavailable())
|
|
100
|
+
return "opus[1m]";
|
|
101
|
+
return "opus";
|
|
102
|
+
}
|
|
103
|
+
const sonnetOverride = process.env.MERIDIAN_SONNET_MODEL ?? process.env.CLAUDE_PROXY_SONNET_MODEL;
|
|
104
|
+
if (sonnetOverride === "sonnet[1m]") {
|
|
105
|
+
if (!use1m || isSubagent || isExtendedContextKnownUnavailable())
|
|
106
|
+
return "sonnet";
|
|
107
|
+
return "sonnet[1m]";
|
|
108
|
+
}
|
|
109
|
+
return "sonnet";
|
|
110
|
+
}
|
|
111
|
+
var EXTRA_USAGE_RETRY_MS = 60 * 60 * 1000;
|
|
112
|
+
var extraUsageUnavailableAt = 0;
|
|
113
|
+
function recordExtendedContextUnavailable() {
|
|
114
|
+
extraUsageUnavailableAt = Date.now();
|
|
115
|
+
}
|
|
116
|
+
function isExtendedContextKnownUnavailable() {
|
|
117
|
+
return extraUsageUnavailableAt > 0 && Date.now() - extraUsageUnavailableAt < EXTRA_USAGE_RETRY_MS;
|
|
118
|
+
}
|
|
119
|
+
function stripExtendedContext(model) {
|
|
120
|
+
if (model === "opus[1m]")
|
|
121
|
+
return "opus";
|
|
122
|
+
if (model === "sonnet[1m]")
|
|
123
|
+
return "sonnet";
|
|
124
|
+
if (model === "fable[1m]")
|
|
125
|
+
return "fable";
|
|
126
|
+
return model;
|
|
127
|
+
}
|
|
128
|
+
function hasExtendedContext(model) {
|
|
129
|
+
return model.endsWith("[1m]");
|
|
130
|
+
}
|
|
131
|
+
var profileAuthCaches = new Map;
|
|
132
|
+
function getAuthCacheInfo(profileId) {
|
|
133
|
+
if (!profileId) {
|
|
134
|
+
return { lastCheckedAt: cachedAuthStatusAt, lastSuccessAt: cachedAuthStatusIsFailure ? 0 : cachedAuthStatusAt, isFailure: cachedAuthStatusIsFailure };
|
|
135
|
+
}
|
|
136
|
+
const cache = profileAuthCaches.get(profileId);
|
|
137
|
+
if (!cache)
|
|
138
|
+
return { lastCheckedAt: 0, lastSuccessAt: 0, isFailure: false };
|
|
139
|
+
return { lastCheckedAt: cache.at, lastSuccessAt: cache.lastSuccessAt, isFailure: cache.isFailure };
|
|
140
|
+
}
|
|
141
|
+
function getAuthCache(key) {
|
|
142
|
+
let cache = profileAuthCaches.get(key);
|
|
143
|
+
if (!cache) {
|
|
144
|
+
cache = { status: null, lastKnownGood: null, at: 0, isFailure: false, promise: null, lastSuccessAt: 0 };
|
|
145
|
+
profileAuthCaches.set(key, cache);
|
|
146
|
+
}
|
|
147
|
+
return cache;
|
|
148
|
+
}
|
|
149
|
+
async function getClaudeAuthStatusAsync(profileId, envOverrides) {
|
|
150
|
+
const isDefault = !profileId;
|
|
151
|
+
const cache = isDefault ? null : getAuthCache(profileId);
|
|
152
|
+
const c_status = cache ? cache.status : cachedAuthStatus;
|
|
153
|
+
const c_lastKnownGood = cache ? cache.lastKnownGood : lastKnownGoodAuthStatus;
|
|
154
|
+
const c_at = cache ? cache.at : cachedAuthStatusAt;
|
|
155
|
+
const c_isFailure = cache ? cache.isFailure : cachedAuthStatusIsFailure;
|
|
156
|
+
let c_promise = cache ? cache.promise : cachedAuthStatusPromise;
|
|
157
|
+
const ttl = c_isFailure ? AUTH_STATUS_FAILURE_TTL_MS : AUTH_STATUS_CACHE_TTL_MS;
|
|
158
|
+
if (c_at > 0 && Date.now() - c_at < ttl) {
|
|
159
|
+
return c_status ?? c_lastKnownGood;
|
|
160
|
+
}
|
|
161
|
+
if (c_promise)
|
|
162
|
+
return c_promise;
|
|
163
|
+
c_promise = (async () => {
|
|
164
|
+
try {
|
|
165
|
+
const claudePath = await resolveClaudeExecutableAsync();
|
|
166
|
+
const { stdout } = await execFile(claudePath, ["auth", "status"], {
|
|
167
|
+
timeout: 5000,
|
|
168
|
+
...envOverrides ? { env: { ...process.env, ...envOverrides } } : {}
|
|
169
|
+
});
|
|
170
|
+
const parsed = JSON.parse(stdout);
|
|
171
|
+
if (cache) {
|
|
172
|
+
cache.status = parsed;
|
|
173
|
+
cache.lastKnownGood = parsed;
|
|
174
|
+
cache.at = Date.now();
|
|
175
|
+
cache.isFailure = false;
|
|
176
|
+
cache.lastSuccessAt = Date.now();
|
|
177
|
+
} else {
|
|
178
|
+
cachedAuthStatus = parsed;
|
|
179
|
+
lastKnownGoodAuthStatus = parsed;
|
|
180
|
+
cachedAuthStatusAt = Date.now();
|
|
181
|
+
cachedAuthStatusIsFailure = false;
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
} catch {
|
|
185
|
+
if (cache) {
|
|
186
|
+
cache.isFailure = true;
|
|
187
|
+
cache.at = Date.now();
|
|
188
|
+
cache.status = null;
|
|
189
|
+
return cache.lastKnownGood;
|
|
190
|
+
} else {
|
|
191
|
+
cachedAuthStatusIsFailure = true;
|
|
192
|
+
cachedAuthStatusAt = Date.now();
|
|
193
|
+
cachedAuthStatus = null;
|
|
194
|
+
return lastKnownGoodAuthStatus;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
if (cache)
|
|
199
|
+
cache.promise = c_promise;
|
|
200
|
+
else
|
|
201
|
+
cachedAuthStatusPromise = c_promise;
|
|
202
|
+
try {
|
|
203
|
+
return await c_promise;
|
|
204
|
+
} finally {
|
|
205
|
+
if (cache)
|
|
206
|
+
cache.promise = null;
|
|
207
|
+
else
|
|
208
|
+
cachedAuthStatusPromise = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
var cachedClaudeInfo = null;
|
|
212
|
+
var cachedClaudePathPromise = null;
|
|
213
|
+
var DEFAULT_DEPS = {
|
|
214
|
+
existsSync,
|
|
215
|
+
statSync: (p) => statSync(p),
|
|
216
|
+
exec,
|
|
217
|
+
resolvePackage: (specifier) => fileURLToPath(import.meta.resolve(specifier)),
|
|
218
|
+
envGet: (name) => process.env[name],
|
|
219
|
+
platform: process.platform,
|
|
220
|
+
arch: process.arch,
|
|
221
|
+
isBun: typeof process.versions.bun !== "undefined"
|
|
222
|
+
};
|
|
223
|
+
function tryEnvOverride(deps) {
|
|
224
|
+
const explicit = deps.envGet("MERIDIAN_CLAUDE_PATH");
|
|
225
|
+
if (!explicit)
|
|
226
|
+
return null;
|
|
227
|
+
return deps.existsSync(explicit) ? explicit : null;
|
|
228
|
+
}
|
|
229
|
+
function tryBundledBinary(deps) {
|
|
230
|
+
try {
|
|
231
|
+
const pkgPath = deps.resolvePackage("@anthropic-ai/claude-code/package.json");
|
|
232
|
+
const bundled = join(dirname(pkgPath), "bin", "claude.exe");
|
|
233
|
+
if (!deps.existsSync(bundled))
|
|
234
|
+
return null;
|
|
235
|
+
const size = deps.statSync(bundled).size;
|
|
236
|
+
if (size <= STUB_SIZE_THRESHOLD)
|
|
237
|
+
return null;
|
|
238
|
+
return bundled;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function tryPlatformPackage(deps) {
|
|
244
|
+
const binName = deps.platform === "win32" ? "claude.exe" : "claude";
|
|
245
|
+
const candidates = [`@anthropic-ai/claude-code-${deps.platform}-${deps.arch}`];
|
|
246
|
+
if (deps.platform === "linux") {
|
|
247
|
+
candidates.push(`@anthropic-ai/claude-code-${deps.platform}-${deps.arch}-musl`);
|
|
248
|
+
}
|
|
249
|
+
for (const pkg of candidates) {
|
|
250
|
+
try {
|
|
251
|
+
const pkgJson = deps.resolvePackage(`${pkg}/package.json`);
|
|
252
|
+
const candidate = join(dirname(pkgJson), binName);
|
|
253
|
+
if (deps.existsSync(candidate))
|
|
254
|
+
return candidate;
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
async function tryPathLookup(deps) {
|
|
260
|
+
const cmd = deps.platform === "win32" ? "where claude" : "which claude";
|
|
261
|
+
try {
|
|
262
|
+
const { stdout } = await deps.exec(cmd);
|
|
263
|
+
const candidates = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
264
|
+
for (const candidate of candidates) {
|
|
265
|
+
if (deps.platform === "win32" && candidate.startsWith("/"))
|
|
266
|
+
continue;
|
|
267
|
+
if (deps.existsSync(candidate))
|
|
268
|
+
return candidate;
|
|
269
|
+
}
|
|
270
|
+
} catch {}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function tryLegacySdkCliJs(deps) {
|
|
274
|
+
if (!deps.isBun)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
const sdkPath = deps.resolvePackage("@anthropic-ai/claude-agent-sdk");
|
|
278
|
+
const cliJs = join(dirname(sdkPath), "cli.js");
|
|
279
|
+
return deps.existsSync(cliJs) ? cliJs : null;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function resolveClaudeExecutableWithSource(deps = DEFAULT_DEPS) {
|
|
285
|
+
const env2 = tryEnvOverride(deps);
|
|
286
|
+
if (env2)
|
|
287
|
+
return { path: env2, source: "env" };
|
|
288
|
+
const bundled = tryBundledBinary(deps);
|
|
289
|
+
if (bundled)
|
|
290
|
+
return { path: bundled, source: "bundled" };
|
|
291
|
+
const platformPkg = tryPlatformPackage(deps);
|
|
292
|
+
if (platformPkg)
|
|
293
|
+
return { path: platformPkg, source: "platform-package" };
|
|
294
|
+
const pathLookup = await tryPathLookup(deps);
|
|
295
|
+
if (pathLookup)
|
|
296
|
+
return { path: pathLookup, source: "path-lookup" };
|
|
297
|
+
const legacy = tryLegacySdkCliJs(deps);
|
|
298
|
+
if (legacy)
|
|
299
|
+
return { path: legacy, source: "legacy-cli-js" };
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
function resolveClaudeExecutableSync(deps = DEFAULT_DEPS) {
|
|
303
|
+
const env2 = tryEnvOverride(deps);
|
|
304
|
+
if (env2)
|
|
305
|
+
return { path: env2, source: "env" };
|
|
306
|
+
const bundled = tryBundledBinary(deps);
|
|
307
|
+
if (bundled)
|
|
308
|
+
return { path: bundled, source: "bundled" };
|
|
309
|
+
const platformPkg = tryPlatformPackage(deps);
|
|
310
|
+
if (platformPkg)
|
|
311
|
+
return { path: platformPkg, source: "platform-package" };
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function getResolvedClaudeExecutableInfo() {
|
|
315
|
+
return cachedClaudeInfo;
|
|
316
|
+
}
|
|
317
|
+
async function resolveClaudeExecutableAsync() {
|
|
318
|
+
if (cachedClaudeInfo)
|
|
319
|
+
return cachedClaudeInfo.path;
|
|
320
|
+
if (cachedClaudePathPromise)
|
|
321
|
+
return cachedClaudePathPromise;
|
|
322
|
+
cachedClaudePathPromise = (async () => {
|
|
323
|
+
const resolved = await resolveClaudeExecutableWithSource();
|
|
324
|
+
if (resolved) {
|
|
325
|
+
cachedClaudeInfo = resolved;
|
|
326
|
+
return resolved.path;
|
|
327
|
+
}
|
|
328
|
+
throw new Error("Could not find Claude Code executable. Install via: npm install -g @anthropic-ai/claude-code, " + "or set MERIDIAN_CLAUDE_PATH=/path/to/claude to point at an existing binary.");
|
|
329
|
+
})();
|
|
330
|
+
try {
|
|
331
|
+
return await cachedClaudePathPromise;
|
|
332
|
+
} finally {
|
|
333
|
+
cachedClaudePathPromise = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function isClosedControllerError(error) {
|
|
337
|
+
if (!(error instanceof Error))
|
|
338
|
+
return false;
|
|
339
|
+
return error.message.includes("Controller is already closed");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/proxy/profileCli.ts
|
|
343
|
+
var PROFILES_DIR = join2(homedir(), ".config", "meridian", "profiles");
|
|
344
|
+
var CONFIG_FILE = join2(homedir(), ".config", "meridian", "profiles.json");
|
|
20
345
|
var OAUTH_AUTHORIZE_URL = "https://claude.com/cai/oauth/authorize";
|
|
21
346
|
var OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
22
347
|
var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -33,18 +358,19 @@ function ensureProfilesDir() {
|
|
|
33
358
|
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
34
359
|
}
|
|
35
360
|
function getProfileDir(id) {
|
|
36
|
-
return
|
|
361
|
+
return join2(PROFILES_DIR, id);
|
|
37
362
|
}
|
|
38
363
|
function buildAuthLoginEnv(configDir, _options = {}, baseEnv = process.env) {
|
|
39
|
-
const
|
|
364
|
+
const env2 = { ...baseEnv };
|
|
40
365
|
if (configDir)
|
|
41
|
-
|
|
42
|
-
return
|
|
366
|
+
env2.CLAUDE_CONFIG_DIR = configDir;
|
|
367
|
+
return env2;
|
|
43
368
|
}
|
|
44
369
|
function base64Url(bytes) {
|
|
45
370
|
return bytes.toString("base64url");
|
|
46
371
|
}
|
|
47
|
-
function createManualOAuthSession() {
|
|
372
|
+
function createManualOAuthSession(scopes) {
|
|
373
|
+
const scopeList = scopes ?? OAUTH_SCOPES;
|
|
48
374
|
const codeVerifier = base64Url(randomBytes(32));
|
|
49
375
|
const state = base64Url(randomBytes(32));
|
|
50
376
|
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -53,7 +379,7 @@ function createManualOAuthSession() {
|
|
|
53
379
|
url.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
54
380
|
url.searchParams.set("response_type", "code");
|
|
55
381
|
url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
|
|
56
|
-
url.searchParams.set("scope",
|
|
382
|
+
url.searchParams.set("scope", scopeList.join(" "));
|
|
57
383
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
58
384
|
url.searchParams.set("code_challenge_method", "S256");
|
|
59
385
|
url.searchParams.set("state", state);
|
|
@@ -78,7 +404,7 @@ function parseAuthorizationCodeInput(input) {
|
|
|
78
404
|
return code ? { code, state } : null;
|
|
79
405
|
}
|
|
80
406
|
function loadProfileConfig() {
|
|
81
|
-
if (!
|
|
407
|
+
if (!existsSync2(CONFIG_FILE))
|
|
82
408
|
return [];
|
|
83
409
|
try {
|
|
84
410
|
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
@@ -186,7 +512,7 @@ async function profileAdd(id, options = {}) {
|
|
|
186
512
|
console.error(` Run: meridian profile list`);
|
|
187
513
|
process.exit(1);
|
|
188
514
|
}
|
|
189
|
-
const defaultClaudeDir =
|
|
515
|
+
const defaultClaudeDir = join2(homedir(), ".claude");
|
|
190
516
|
const alreadyImported = profiles.some((p) => p.claudeConfigDir === defaultClaudeDir);
|
|
191
517
|
if (!alreadyImported) {
|
|
192
518
|
const defaultAuth = getAuthStatus(defaultClaudeDir);
|
|
@@ -317,7 +643,7 @@ function dirsToRemoveOnProfileRemove(profile, profilesDir) {
|
|
|
317
643
|
dirs.push(profile.claudeConfigDir);
|
|
318
644
|
}
|
|
319
645
|
if (profile.oauthToken || profile.type === "oauth-token") {
|
|
320
|
-
const isolationDir =
|
|
646
|
+
const isolationDir = join2(profilesDir, profile.id);
|
|
321
647
|
if (!dirs.includes(isolationDir))
|
|
322
648
|
dirs.push(isolationDir);
|
|
323
649
|
}
|
|
@@ -339,7 +665,7 @@ function profileRemove(id) {
|
|
|
339
665
|
profiles.splice(idx, 1);
|
|
340
666
|
saveProfileConfig(profiles);
|
|
341
667
|
for (const dir of dirsToRemove) {
|
|
342
|
-
if (
|
|
668
|
+
if (existsSync2(dir))
|
|
343
669
|
rmSync(dir, { recursive: true, force: true });
|
|
344
670
|
}
|
|
345
671
|
console.log(`\x1B[32m✓ Profile "${id}" removed.\x1B[0m`);
|
|
@@ -498,16 +824,5 @@ Examples:
|
|
|
498
824
|
meridian profile switch work # Switch to work account
|
|
499
825
|
meridian profile list # Show all profiles`);
|
|
500
826
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
profileRemove,
|
|
504
|
-
profileLogin,
|
|
505
|
-
profileList,
|
|
506
|
-
profileHelp,
|
|
507
|
-
profileAddOauthToken,
|
|
508
|
-
profileAdd,
|
|
509
|
-
parseAuthorizationCodeInput,
|
|
510
|
-
dirsToRemoveOnProfileRemove,
|
|
511
|
-
createManualOAuthSession,
|
|
512
|
-
buildAuthLoginEnv
|
|
513
|
-
};
|
|
827
|
+
|
|
828
|
+
export { env, envBool, resolvePassthrough, envInt, init_env, CANONICAL_SONNET_MODEL, resolveSdkModelDefaults, explicitModelPin, mapModelToClaudeModel, recordExtendedContextUnavailable, stripExtendedContext, hasExtendedContext, getAuthCacheInfo, getClaudeAuthStatusAsync, getResolvedClaudeExecutableInfo, resolveClaudeExecutableAsync, isClosedControllerError, OAUTH_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, buildAuthLoginEnv, createManualOAuthSession, parseAuthorizationCodeInput, profileAdd, profileAddOauthToken, profileList, dirsToRemoveOnProfileRemove, profileRemove, profileSwitch, profileLogin, profileHelp };
|