@rynfar/meridian 1.24.1 → 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 +19 -1
- package/dist/{cli-bjpad5x9.js → cli-9pc43rfa.js} +76 -80
- package/dist/cli-jd4atcxs.js +220 -0
- package/dist/cli.js +20 -3
- package/dist/proxy/errors.d.ts +16 -0
- package/dist/proxy/errors.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/tokenRefresh.d.ts +51 -0
- package/dist/proxy/tokenRefresh.d.ts.map +1 -0
- package/dist/server.js +2 -1
- package/dist/tokenRefresh-wzn2bvrq.js +10 -0
- package/package.json +2 -2
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
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
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";
|
|
@@ -14352,6 +14297,7 @@ function createProxyServer(config = {}) {
|
|
|
14352
14297
|
const RATE_LIMIT_BASE_DELAY_MS = 1000;
|
|
14353
14298
|
const response = async function* () {
|
|
14354
14299
|
let rateLimitRetries = 0;
|
|
14300
|
+
let tokenRefreshed = false;
|
|
14355
14301
|
while (true) {
|
|
14356
14302
|
let didYieldContent = false;
|
|
14357
14303
|
try {
|
|
@@ -14414,6 +14360,27 @@ function createProxyServer(config = {}) {
|
|
|
14414
14360
|
}));
|
|
14415
14361
|
return;
|
|
14416
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
|
+
}
|
|
14417
14384
|
if (isRateLimitError(errMsg)) {
|
|
14418
14385
|
if (hasExtendedContext(model)) {
|
|
14419
14386
|
const from = model;
|
|
@@ -14622,6 +14589,7 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
14622
14589
|
const RATE_LIMIT_BASE_DELAY_MS = 1000;
|
|
14623
14590
|
const response = async function* () {
|
|
14624
14591
|
let rateLimitRetries = 0;
|
|
14592
|
+
let tokenRefreshed = false;
|
|
14625
14593
|
while (true) {
|
|
14626
14594
|
let didYieldClientEvent = false;
|
|
14627
14595
|
try {
|
|
@@ -14684,6 +14652,27 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
14684
14652
|
}));
|
|
14685
14653
|
return;
|
|
14686
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
|
+
}
|
|
14687
14676
|
if (isRateLimitError(errMsg)) {
|
|
14688
14677
|
if (hasExtendedContext(model)) {
|
|
14689
14678
|
const from = model;
|
|
@@ -15127,6 +15116,13 @@ data: ${JSON.stringify({
|
|
|
15127
15116
|
});
|
|
15128
15117
|
}
|
|
15129
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
|
+
});
|
|
15130
15126
|
app.all("*", (c) => {
|
|
15131
15127
|
console.error(`[PROXY] UNHANDLED ${c.req.method} ${c.req.url}`);
|
|
15132
15128
|
return c.json({ error: { type: "not_found", message: `Endpoint not supported: ${c.req.method} ${new URL(c.req.url).pathname}` } }, 404);
|
|
@@ -15177,4 +15173,4 @@ Or use a different port:`);
|
|
|
15177
15173
|
};
|
|
15178
15174
|
}
|
|
15179
15175
|
|
|
15180
|
-
export {
|
|
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-
|
|
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}`);
|
package/dist/proxy/errors.d.ts
CHANGED
|
@@ -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,
|
|
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":"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;
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynfar/meridian",
|
|
3
|
-
"version": "1.24.
|
|
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
|
},
|