@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 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:** The bundled Claude Code binary (`claude.exe`) is patched with `autoPatchelfHook` at build time, so it runs on NixOS out of the box. If you previously enabled `programs.nix-ld.enable = true` as a workaround for `Could not start dynamically linked executable` (#501), that is no longer required for Meridian.
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
- # Point at an entry inside a packaged derivation so its deps come along.
126
- # pluginConfig = [ { path = "${pkgs.my-meridian-plugin}/lib/index.js"; } ];
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
- Install into Meridian's config dir and register the built file in
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` only when tools are present. What do I do?**
1004
- 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.
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
- 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`.)
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
- 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.
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"./cli-p9swy5t3.js";
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
- var PROFILES_DIR = join(homedir(), ".config", "meridian", "profiles");
19
- var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
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 join(PROFILES_DIR, id);
361
+ return join2(PROFILES_DIR, id);
37
362
  }
38
363
  function buildAuthLoginEnv(configDir, _options = {}, baseEnv = process.env) {
39
- const env = { ...baseEnv };
364
+ const env2 = { ...baseEnv };
40
365
  if (configDir)
41
- env.CLAUDE_CONFIG_DIR = configDir;
42
- return env;
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", OAUTH_SCOPES.join(" "));
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 (!existsSync(CONFIG_FILE))
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 = join(homedir(), ".claude");
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 = join(profilesDir, profile.id);
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 (existsSync(dir))
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
- export {
502
- profileSwitch,
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 };