@rynfar/meridian 1.24.0 → 1.24.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -304,6 +304,9 @@ See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode
304
304
  | `MERIDIAN_IDLE_TIMEOUT_SECONDS` | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
305
305
  | `MERIDIAN_TELEMETRY_SIZE` | `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
306
306
  | `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.
307
310
 
308
311
  ## Programmatic API
309
312
 
@@ -371,6 +374,7 @@ See [`examples/opencode-plugin/`](examples/opencode-plugin/) for a reference imp
371
374
  | `POST /v1/messages` | Anthropic Messages API |
372
375
  | `POST /messages` | Alias for `/v1/messages` |
373
376
  | `GET /health` | Auth status, subscription type, mode |
377
+ | `POST /auth/refresh` | Manually refresh the OAuth token |
374
378
  | `GET /telemetry` | Performance dashboard |
375
379
  | `GET /telemetry/requests` | Recent request metrics (JSON) |
376
380
  | `GET /telemetry/summary` | Aggregate statistics (JSON) |
@@ -415,7 +419,21 @@ API keys are billed per token. Your Max subscription is a flat monthly fee with
415
419
  It works with any Claude subscription that supports the Claude Code SDK. Max is recommended for the best rate limits.
416
420
 
417
421
  **What happens if my session expires?**
418
- The SDK handles token refresh automatically. If it can't refresh, Meridian returns a clear error telling you to run `claude login`.
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:
426
+
427
+ ```bash
428
+ # CLI (works whether the proxy is running or not)
429
+ meridian refresh-token
430
+
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"}
434
+ ```
435
+
436
+ The CLI exits 0 on success and 1 on failure, so it integrates cleanly into scripts or health checks.
419
437
 
420
438
  ## Contributing
421
439
 
@@ -1,19 +1,10 @@
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);
1
+ import {
2
+ __export,
3
+ __require,
4
+ claudeLog,
5
+ refreshOAuthToken,
6
+ withClaudeLogContext
7
+ } from "./cli-jd4atcxs.js";
17
8
 
18
9
  // node_modules/hono/dist/compose.js
19
10
  var compose = (middleware, onError, onNotFound) => {
@@ -2179,68 +2170,6 @@ var DEFAULT_PROXY_CONFIG = {
2179
2170
  silent: false
2180
2171
  };
2181
2172
 
2182
- // src/logger.ts
2183
- import { AsyncLocalStorage } from "node:async_hooks";
2184
- var contextStore = new AsyncLocalStorage;
2185
- var shouldLog = () => process.env["OPENCODE_CLAUDE_PROVIDER_DEBUG"];
2186
- var shouldLogStreamDebug = () => process.env["OPENCODE_CLAUDE_PROVIDER_STREAM_DEBUG"];
2187
- var isVerboseStreamEvent = (event) => {
2188
- return event.startsWith("stream.") || event === "response.empty_stream";
2189
- };
2190
- var REDACTED_KEYS = new Set([
2191
- "authorization",
2192
- "cookie",
2193
- "x-api-key",
2194
- "apiKey",
2195
- "apikey",
2196
- "prompt",
2197
- "messages",
2198
- "content"
2199
- ]);
2200
- var sanitize = (value) => {
2201
- if (value === null || value === undefined)
2202
- return value;
2203
- if (typeof value === "string") {
2204
- if (value.length > 512) {
2205
- return `${value.slice(0, 512)}... [truncated=${value.length}]`;
2206
- }
2207
- return value;
2208
- }
2209
- if (Array.isArray(value)) {
2210
- return value.map(sanitize);
2211
- }
2212
- if (typeof value === "object") {
2213
- const out = {};
2214
- for (const [k, v] of Object.entries(value)) {
2215
- if (REDACTED_KEYS.has(k)) {
2216
- if (typeof v === "string") {
2217
- out[k] = `[redacted len=${v.length}]`;
2218
- } else if (Array.isArray(v)) {
2219
- out[k] = `[redacted array len=${v.length}]`;
2220
- } else {
2221
- out[k] = "[redacted]";
2222
- }
2223
- } else {
2224
- out[k] = sanitize(v);
2225
- }
2226
- }
2227
- return out;
2228
- }
2229
- return value;
2230
- };
2231
- var withClaudeLogContext = (context, fn) => {
2232
- return contextStore.run(context, fn);
2233
- };
2234
- var claudeLog = (event, extra) => {
2235
- if (!shouldLog())
2236
- return;
2237
- if (isVerboseStreamEvent(event) && !shouldLogStreamDebug())
2238
- return;
2239
- const context = contextStore.getStore() || {};
2240
- const payload = sanitize({ ts: new Date().toISOString(), event, ...context, ...extra || {} });
2241
- console.debug(`[opencode-claude-code-provider] ${JSON.stringify(payload)}`);
2242
- };
2243
-
2244
2173
  // src/proxy/server.ts
2245
2174
  import { exec as execCallback2 } from "child_process";
2246
2175
  import { promisify as promisify3 } from "util";
@@ -6940,6 +6869,13 @@ refresh();setInterval(refresh,10000);
6940
6869
  // src/proxy/errors.ts
6941
6870
  function classifyError(errMsg) {
6942
6871
  const lower = errMsg.toLowerCase();
6872
+ if (lower.includes("oauth token has expired") || lower.includes("not logged in")) {
6873
+ return {
6874
+ status: 401,
6875
+ type: "authentication_error",
6876
+ message: "Claude OAuth token has expired and could not be refreshed automatically. Run 'claude login' in your terminal to re-authenticate."
6877
+ };
6878
+ }
6943
6879
  if (lower.includes("401") || lower.includes("authentication") || lower.includes("invalid auth") || lower.includes("credentials")) {
6944
6880
  return {
6945
6881
  status: 401,
@@ -6948,10 +6884,11 @@ function classifyError(errMsg) {
6948
6884
  };
6949
6885
  }
6950
6886
  if (lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests")) {
6887
+ const hint = lower.includes("1m") || lower.includes("context") ? " If you're frequently hitting this, set MERIDIAN_SONNET_MODEL=sonnet to use the 200k model instead." : "";
6951
6888
  return {
6952
6889
  status: 429,
6953
6890
  type: "rate_limit_error",
6954
- message: "Claude Max rate limit reached. Wait a moment and try again."
6891
+ message: `Claude Max rate limit reached. Wait a moment and try again.${hint}`
6955
6892
  };
6956
6893
  }
6957
6894
  if (lower.includes("402") || lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) {
@@ -7014,6 +6951,10 @@ function classifyError(errMsg) {
7014
6951
  message: errMsg || "Unknown error"
7015
6952
  };
7016
6953
  }
6954
+ function isExpiredTokenError(errMsg) {
6955
+ const lower = errMsg.toLowerCase();
6956
+ return lower.includes("oauth token has expired") || lower.includes("not logged in");
6957
+ }
7017
6958
  function isStaleSessionError(error) {
7018
6959
  if (!(error instanceof Error))
7019
6960
  return false;
@@ -7023,6 +6964,10 @@ function isRateLimitError(errMsg) {
7023
6964
  const lower = errMsg.toLowerCase();
7024
6965
  return lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
7025
6966
  }
6967
+ function isExtraUsageRequiredError(errMsg) {
6968
+ const lower = errMsg.toLowerCase();
6969
+ return lower.includes("extra usage") && lower.includes("1m");
6970
+ }
7026
6971
 
7027
6972
  // src/proxy/models.ts
7028
6973
  import { exec as execCallback } from "child_process";
@@ -7103,14 +7048,17 @@ async function resolveClaudeExecutableAsync() {
7103
7048
  if (cachedClaudePathPromise)
7104
7049
  return cachedClaudePathPromise;
7105
7050
  cachedClaudePathPromise = (async () => {
7106
- try {
7107
- const sdkPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
7108
- const sdkCliJs = join(dirname(sdkPath), "cli.js");
7109
- if (existsSync(sdkCliJs)) {
7110
- cachedClaudePath = sdkCliJs;
7111
- return sdkCliJs;
7112
- }
7113
- } catch {}
7051
+ const runningUnderBun = typeof process.versions.bun !== "undefined";
7052
+ if (runningUnderBun) {
7053
+ try {
7054
+ const sdkPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
7055
+ const sdkCliJs = join(dirname(sdkPath), "cli.js");
7056
+ if (existsSync(sdkCliJs)) {
7057
+ cachedClaudePath = sdkCliJs;
7058
+ return sdkCliJs;
7059
+ }
7060
+ } catch {}
7061
+ }
7114
7062
  try {
7115
7063
  const { stdout } = await exec("which claude");
7116
7064
  const claudePath = stdout.trim();
@@ -7119,6 +7067,16 @@ async function resolveClaudeExecutableAsync() {
7119
7067
  return claudePath;
7120
7068
  }
7121
7069
  } catch {}
7070
+ if (!runningUnderBun) {
7071
+ try {
7072
+ const sdkPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
7073
+ const sdkCliJs = join(dirname(sdkPath), "cli.js");
7074
+ if (existsSync(sdkCliJs)) {
7075
+ cachedClaudePath = sdkCliJs;
7076
+ return sdkCliJs;
7077
+ }
7078
+ } catch {}
7079
+ }
7122
7080
  throw new Error("Could not find Claude Code executable. Install via: npm install -g @anthropic-ai/claude-code");
7123
7081
  })();
7124
7082
  try {
@@ -14339,6 +14297,7 @@ function createProxyServer(config = {}) {
14339
14297
  const RATE_LIMIT_BASE_DELAY_MS = 1000;
14340
14298
  const response = async function* () {
14341
14299
  let rateLimitRetries = 0;
14300
+ let tokenRefreshed = false;
14342
14301
  while (true) {
14343
14302
  let didYieldContent = false;
14344
14303
  try {
@@ -14401,6 +14360,27 @@ function createProxyServer(config = {}) {
14401
14360
  }));
14402
14361
  return;
14403
14362
  }
14363
+ if (isExtraUsageRequiredError(errMsg) && hasExtendedContext(model)) {
14364
+ const from = model;
14365
+ model = stripExtendedContext(model);
14366
+ claudeLog("upstream.context_fallback", {
14367
+ mode: "non_stream",
14368
+ from,
14369
+ to: model,
14370
+ reason: "extra_usage_required"
14371
+ });
14372
+ console.error(`[PROXY] ${requestMeta.requestId} extra usage required for [1m], falling back to ${model}`);
14373
+ continue;
14374
+ }
14375
+ if (isExpiredTokenError(errMsg) && !tokenRefreshed) {
14376
+ tokenRefreshed = true;
14377
+ const refreshed = await refreshOAuthToken();
14378
+ if (refreshed) {
14379
+ claudeLog("token_refresh.retrying", { mode: "non_stream" });
14380
+ console.error(`[PROXY] ${requestMeta.requestId} OAuth token expired — refreshed, retrying`);
14381
+ continue;
14382
+ }
14383
+ }
14404
14384
  if (isRateLimitError(errMsg)) {
14405
14385
  if (hasExtendedContext(model)) {
14406
14386
  const from = model;
@@ -14609,6 +14589,7 @@ Subprocess stderr: ${stderrOutput}`;
14609
14589
  const RATE_LIMIT_BASE_DELAY_MS = 1000;
14610
14590
  const response = async function* () {
14611
14591
  let rateLimitRetries = 0;
14592
+ let tokenRefreshed = false;
14612
14593
  while (true) {
14613
14594
  let didYieldClientEvent = false;
14614
14595
  try {
@@ -14671,6 +14652,27 @@ Subprocess stderr: ${stderrOutput}`;
14671
14652
  }));
14672
14653
  return;
14673
14654
  }
14655
+ if (isExtraUsageRequiredError(errMsg) && hasExtendedContext(model)) {
14656
+ const from = model;
14657
+ model = stripExtendedContext(model);
14658
+ claudeLog("upstream.context_fallback", {
14659
+ mode: "stream",
14660
+ from,
14661
+ to: model,
14662
+ reason: "extra_usage_required"
14663
+ });
14664
+ console.error(`[PROXY] ${requestMeta.requestId} extra usage required for [1m], falling back to ${model}`);
14665
+ continue;
14666
+ }
14667
+ if (isExpiredTokenError(errMsg) && !tokenRefreshed) {
14668
+ tokenRefreshed = true;
14669
+ const refreshed = await refreshOAuthToken();
14670
+ if (refreshed) {
14671
+ claudeLog("token_refresh.retrying", { mode: "stream" });
14672
+ console.error(`[PROXY] ${requestMeta.requestId} OAuth token expired — refreshed, retrying`);
14673
+ continue;
14674
+ }
14675
+ }
14674
14676
  if (isRateLimitError(errMsg)) {
14675
14677
  if (hasExtendedContext(model)) {
14676
14678
  const from = model;
@@ -15114,6 +15116,13 @@ data: ${JSON.stringify({
15114
15116
  });
15115
15117
  }
15116
15118
  });
15119
+ app.post("/auth/refresh", async (c) => {
15120
+ const success = await refreshOAuthToken();
15121
+ if (success) {
15122
+ return c.json({ success: true, message: "OAuth token refreshed successfully" });
15123
+ }
15124
+ return c.json({ success: false, message: "Token refresh failed. If the problem persists, run 'claude login'." }, 500);
15125
+ });
15117
15126
  app.all("*", (c) => {
15118
15127
  console.error(`[PROXY] UNHANDLED ${c.req.method} ${c.req.url}`);
15119
15128
  return c.json({ error: { type: "not_found", message: `Endpoint not supported: ${c.req.method} ${new URL(c.req.url).pathname}` } }, 404);
@@ -15164,4 +15173,4 @@ Or use a different port:`);
15164
15173
  };
15165
15174
  }
15166
15175
 
15167
- export { __require, computeLineageHash, hashMessage, computeMessageHashes, getMaxSessionsLimit, clearSessionCache, createProxyServer, startProxyServer };
15176
+ export { computeLineageHash, hashMessage, computeMessageHashes, getMaxSessionsLimit, clearSessionCache, createProxyServer, startProxyServer };
@@ -0,0 +1,220 @@
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
+ // src/proxy/tokenRefresh.ts
19
+ import { execFile as execFileCb } from "child_process";
20
+ import { existsSync, readFileSync, writeFileSync } from "fs";
21
+ import { homedir, platform, userInfo } from "os";
22
+ import { promisify } from "util";
23
+
24
+ // src/logger.ts
25
+ import { AsyncLocalStorage } from "node:async_hooks";
26
+ var contextStore = new AsyncLocalStorage;
27
+ var shouldLog = () => process.env["OPENCODE_CLAUDE_PROVIDER_DEBUG"];
28
+ var shouldLogStreamDebug = () => process.env["OPENCODE_CLAUDE_PROVIDER_STREAM_DEBUG"];
29
+ var isVerboseStreamEvent = (event) => {
30
+ return event.startsWith("stream.") || event === "response.empty_stream";
31
+ };
32
+ var REDACTED_KEYS = new Set([
33
+ "authorization",
34
+ "cookie",
35
+ "x-api-key",
36
+ "apiKey",
37
+ "apikey",
38
+ "prompt",
39
+ "messages",
40
+ "content"
41
+ ]);
42
+ var sanitize = (value) => {
43
+ if (value === null || value === undefined)
44
+ return value;
45
+ if (typeof value === "string") {
46
+ if (value.length > 512) {
47
+ return `${value.slice(0, 512)}... [truncated=${value.length}]`;
48
+ }
49
+ return value;
50
+ }
51
+ if (Array.isArray(value)) {
52
+ return value.map(sanitize);
53
+ }
54
+ if (typeof value === "object") {
55
+ const out = {};
56
+ for (const [k, v] of Object.entries(value)) {
57
+ if (REDACTED_KEYS.has(k)) {
58
+ if (typeof v === "string") {
59
+ out[k] = `[redacted len=${v.length}]`;
60
+ } else if (Array.isArray(v)) {
61
+ out[k] = `[redacted array len=${v.length}]`;
62
+ } else {
63
+ out[k] = "[redacted]";
64
+ }
65
+ } else {
66
+ out[k] = sanitize(v);
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ return value;
72
+ };
73
+ var withClaudeLogContext = (context, fn) => {
74
+ return contextStore.run(context, fn);
75
+ };
76
+ var claudeLog = (event, extra) => {
77
+ if (!shouldLog())
78
+ return;
79
+ if (isVerboseStreamEvent(event) && !shouldLogStreamDebug())
80
+ return;
81
+ const context = contextStore.getStore() || {};
82
+ const payload = sanitize({ ts: new Date().toISOString(), event, ...context, ...extra || {} });
83
+ console.debug(`[opencode-claude-code-provider] ${JSON.stringify(payload)}`);
84
+ };
85
+
86
+ // src/proxy/tokenRefresh.ts
87
+ var execFile = promisify(execFileCb);
88
+ var OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
89
+ var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
90
+ var KEYCHAIN_SERVICE = "Claude Code-credentials";
91
+ var CREDENTIALS_FILE = `${homedir()}/.claude/.credentials.json`;
92
+ function parseKeychainValue(raw) {
93
+ const trimmed = raw.trim();
94
+ try {
95
+ return { credentials: JSON.parse(trimmed), wasHex: false };
96
+ } catch {}
97
+ try {
98
+ const decoded = Buffer.from(trimmed, "hex").toString("utf-8");
99
+ return { credentials: JSON.parse(decoded), wasHex: true };
100
+ } catch {}
101
+ return null;
102
+ }
103
+ var keychainWasHex = false;
104
+ var macosStore = {
105
+ async read() {
106
+ try {
107
+ const { stdout } = await execFile("/usr/bin/security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", userInfo().username, "-w"], { timeout: 5000 });
108
+ const parsed = parseKeychainValue(stdout);
109
+ if (!parsed)
110
+ throw new Error("Could not parse keychain value as JSON or hex-encoded JSON");
111
+ keychainWasHex = parsed.wasHex;
112
+ return parsed.credentials;
113
+ } catch (err) {
114
+ claudeLog("token_refresh.keychain_read_failed", { error: String(err) });
115
+ return null;
116
+ }
117
+ },
118
+ async write(credentials) {
119
+ const json = JSON.stringify(credentials, null, 2);
120
+ const value = keychainWasHex ? Buffer.from(json).toString("hex") : json;
121
+ try {
122
+ await execFile("/usr/bin/security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", userInfo().username, "-w", value], { timeout: 5000 });
123
+ return true;
124
+ } catch (err) {
125
+ claudeLog("token_refresh.keychain_write_failed", { error: String(err) });
126
+ return false;
127
+ }
128
+ }
129
+ };
130
+ var fileStore = {
131
+ async read() {
132
+ try {
133
+ if (!existsSync(CREDENTIALS_FILE))
134
+ return null;
135
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
136
+ } catch (err) {
137
+ claudeLog("token_refresh.file_read_failed", { error: String(err) });
138
+ return null;
139
+ }
140
+ },
141
+ async write(credentials) {
142
+ try {
143
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), "utf-8");
144
+ return true;
145
+ } catch (err) {
146
+ claudeLog("token_refresh.file_write_failed", { error: String(err) });
147
+ return false;
148
+ }
149
+ }
150
+ };
151
+ function createPlatformCredentialStore() {
152
+ return platform() === "darwin" ? macosStore : fileStore;
153
+ }
154
+ var inflightRefresh = null;
155
+ async function refreshOAuthToken(store) {
156
+ if (inflightRefresh)
157
+ return inflightRefresh;
158
+ inflightRefresh = doRefresh(store ?? createPlatformCredentialStore()).finally(() => {
159
+ inflightRefresh = null;
160
+ });
161
+ return inflightRefresh;
162
+ }
163
+ async function doRefresh(store) {
164
+ const credentials = await store.read();
165
+ if (!credentials) {
166
+ claudeLog("token_refresh.no_credentials", {});
167
+ return false;
168
+ }
169
+ const { refreshToken } = credentials.claudeAiOauth;
170
+ if (!refreshToken) {
171
+ claudeLog("token_refresh.no_refresh_token", {});
172
+ return false;
173
+ }
174
+ let response;
175
+ try {
176
+ response = await fetch(OAUTH_TOKEN_URL, {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify({
180
+ grant_type: "refresh_token",
181
+ client_id: OAUTH_CLIENT_ID,
182
+ refresh_token: refreshToken
183
+ }),
184
+ signal: AbortSignal.timeout(15000)
185
+ });
186
+ } catch (err) {
187
+ claudeLog("token_refresh.request_failed", { error: String(err) });
188
+ return false;
189
+ }
190
+ if (!response.ok) {
191
+ const body = await response.text().catch(() => "");
192
+ claudeLog("token_refresh.bad_response", { status: response.status, body });
193
+ return false;
194
+ }
195
+ let tokenData;
196
+ try {
197
+ tokenData = await response.json();
198
+ } catch (err) {
199
+ claudeLog("token_refresh.parse_failed", { error: String(err) });
200
+ return false;
201
+ }
202
+ const now = Date.now();
203
+ const expiresAt = tokenData.expires_at ?? (tokenData.expires_in ? now + tokenData.expires_in * 1000 : now + 8 * 60 * 60 * 1000);
204
+ credentials.claudeAiOauth = {
205
+ ...credentials.claudeAiOauth,
206
+ accessToken: tokenData.access_token,
207
+ refreshToken: tokenData.refresh_token ?? refreshToken,
208
+ expiresAt
209
+ };
210
+ const written = await store.write(credentials);
211
+ if (!written)
212
+ return false;
213
+ claudeLog("token_refresh.success", { expiresAt });
214
+ return true;
215
+ }
216
+ function resetInflightRefresh() {
217
+ inflightRefresh = null;
218
+ }
219
+
220
+ export { __export, __require, withClaudeLogContext, claudeLog, createPlatformCredentialStore, refreshOAuthToken, resetInflightRefresh };
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- __require,
4
3
  startProxyServer
5
- } from "./cli-adpwqa7a.js";
4
+ } from "./cli-9pc43rfa.js";
5
+ import {
6
+ __require
7
+ } from "./cli-jd4atcxs.js";
6
8
 
7
9
  // bin/cli.ts
8
10
  import { createRequire } from "module";
@@ -20,7 +22,11 @@ if (args.includes("--help") || args.includes("-h")) {
20
22
 
21
23
  Local Anthropic API powered by your Claude Max subscription.
22
24
 
23
- Usage: meridian [options]
25
+ Usage: meridian [command] [options]
26
+
27
+ Commands:
28
+ (default) Start the proxy server
29
+ refresh-token Refresh the Claude Code OAuth token
24
30
 
25
31
  Options:
26
32
  -v, --version Show version
@@ -35,6 +41,17 @@ Environment variables:
35
41
  See https://github.com/rynfar/meridian for full documentation.`);
36
42
  process.exit(0);
37
43
  }
44
+ if (args[0] === "refresh-token") {
45
+ const { refreshOAuthToken } = await import("./tokenRefresh-wzn2bvrq.js");
46
+ const success = await refreshOAuthToken();
47
+ if (success) {
48
+ console.log("Token refreshed successfully");
49
+ process.exit(0);
50
+ } else {
51
+ console.error("Token refresh failed. If the problem persists, run: claude login");
52
+ process.exit(1);
53
+ }
54
+ }
38
55
  var exec = promisify(execCallback);
39
56
  process.on("uncaughtException", (err) => {
40
57
  console.error(`[PROXY] Uncaught exception (recovered): ${err.message}`);
@@ -11,6 +11,16 @@ export interface ClassifiedError {
11
11
  * Detect specific SDK errors and return helpful messages to the client.
12
12
  */
13
13
  export declare function classifyError(errMsg: string): ClassifiedError;
14
+ /**
15
+ * Detect errors caused by an expired or missing OAuth access token.
16
+ * Triggers an inline token refresh + retry in server.ts.
17
+ *
18
+ * Two distinct messages from the Claude Code CLI:
19
+ * - "OAuth token has expired" — CLI sent the token, Anthropic API rejected it
20
+ * - "Not logged in" — CLI checked expiresAt locally and refused to try
21
+ * Both are resolved by refreshing the token.
22
+ */
23
+ export declare function isExpiredTokenError(errMsg: string): boolean;
14
24
  /**
15
25
  * Detect errors caused by stale session/message UUIDs.
16
26
  * These happen when the upstream Claude session no longer contains
@@ -22,4 +32,10 @@ export declare function isStaleSessionError(error: unknown): boolean;
22
32
  * Used by server.ts to decide whether to retry with a smaller context window.
23
33
  */
24
34
  export declare function isRateLimitError(errMsg: string): boolean;
35
+ /**
36
+ * Detect errors caused by the 1M context window requiring Extra Usage.
37
+ * Max subscribers without Extra Usage enabled get this error when using
38
+ * sonnet[1m] or opus[1m]. The fix is to fall back to the base model.
39
+ */
40
+ export declare function isExtraUsageRequiredError(errMsg: string): boolean;
25
41
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAmG7D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA+G7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/proxy/models.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA0BD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAYlG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CAIpE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAE9D;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkCjF;AAOD;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,MAAM,CAAC,CAiCpE;AAED,2CAA2C;AAC3C,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED,kDAAkD;AAClD,wBAAgB,2BAA2B,IAAI,IAAI,CAMlD;AAED;;6DAE6D;AAC7D,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG/D"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/proxy/models.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAA;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA0BD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAYlG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CAIpE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAE9D;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkCjF;AAOD;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,MAAM,CAAC,CA4DpE;AAED,2CAA2C;AAC3C,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED,kDAAkD;AAClD,wBAAgB,2BAA2B,IAAI,IAAI,CAMlD;AAED;;6DAE6D;AAC7D,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAiBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAgB,MAAM,iBAAiB,CAAA;AAEnH,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAyF7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAosChF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0ChG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAkBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAgB,MAAM,iBAAiB,CAAA;AAEnH,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAyF7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAqwChF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0ChG"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Cross-platform OAuth token refresh for Claude Code credentials.
3
+ *
4
+ * Storage backends:
5
+ * macOS — system Keychain via /usr/bin/security (no prompt — pre-authorised)
6
+ * Linux — ~/.claude/.credentials.json
7
+ *
8
+ * The credential store is dependency-injectable for testing. Production code
9
+ * uses createPlatformCredentialStore() which picks the right backend
10
+ * automatically.
11
+ *
12
+ * Concurrent calls to refreshOAuthToken() are deduplicated: if a refresh is
13
+ * already in flight, subsequent callers wait for the same promise rather than
14
+ * issuing a second network request and racing on the write.
15
+ */
16
+ interface OAuthCredentials {
17
+ accessToken: string;
18
+ refreshToken: string;
19
+ expiresAt: number;
20
+ scopes?: string[];
21
+ subscriptionType?: string;
22
+ rateLimitTier?: string;
23
+ }
24
+ interface CredentialsFile {
25
+ claudeAiOauth: OAuthCredentials;
26
+ [key: string]: unknown;
27
+ }
28
+ export interface CredentialStore {
29
+ read(): Promise<CredentialsFile | null>;
30
+ write(credentials: CredentialsFile): Promise<boolean>;
31
+ }
32
+ /**
33
+ * Returns the appropriate credential store for the current platform.
34
+ */
35
+ export declare function createPlatformCredentialStore(): CredentialStore;
36
+ /**
37
+ * Refresh the Claude Code OAuth access token.
38
+ *
39
+ * Reads the stored refresh token, exchanges it for a new access token via
40
+ * Anthropic's OAuth endpoint, and writes the updated credentials back.
41
+ *
42
+ * Returns true on success, false on any failure. Concurrent calls share one
43
+ * in-flight request so only one network round-trip is made.
44
+ *
45
+ * @param store Override the credential store (for testing).
46
+ */
47
+ export declare function refreshOAuthToken(store?: CredentialStore): Promise<boolean>;
48
+ /** Reset in-flight state — for testing only. */
49
+ export declare function resetInflightRefresh(): void;
50
+ export {};
51
+ //# sourceMappingURL=tokenRefresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenRefresh.d.ts","sourceRoot":"","sources":["../../src/proxy/tokenRefresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAeH,UAAU,gBAAgB;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,UAAU,eAAe;IACvB,aAAa,EAAE,gBAAgB,CAAA;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAMD,MAAM,WAAW,eAAe;IAC9B,IAAI,IAAI,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;IACvC,KAAK,CAAC,WAAW,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACtD;AA2FD;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,eAAe,CAE/D;AASD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAQjF;AAiED,gDAAgD;AAChD,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
package/dist/server.js CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  getMaxSessionsLimit,
7
7
  hashMessage,
8
8
  startProxyServer
9
- } from "./cli-adpwqa7a.js";
9
+ } from "./cli-9pc43rfa.js";
10
+ import"./cli-jd4atcxs.js";
10
11
  export {
11
12
  startProxyServer,
12
13
  hashMessage,
@@ -0,0 +1,10 @@
1
+ import {
2
+ createPlatformCredentialStore,
3
+ refreshOAuthToken,
4
+ resetInflightRefresh
5
+ } from "./cli-jd4atcxs.js";
6
+ export {
7
+ resetInflightRefresh,
8
+ refreshOAuthToken,
9
+ createPlatformCredentialStore
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.24.0",
3
+ "version": "1.24.5",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -24,7 +24,7 @@
24
24
  "build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js' && tsc -p tsconfig.build.json",
25
25
  "postbuild": "node --check dist/cli.js && node --check dist/server.js && test -f dist/proxy/server.d.ts",
26
26
  "prepublishOnly": "bun run build",
27
- "test": "bun test --path-ignore-patterns '**/*session-store*' && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts",
27
+ "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "proxy:direct": "bun run ./bin/cli.ts"
30
30
  },