@rynfar/meridian 1.41.1 → 1.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -4
- package/dist/cli-3jqvrake.js +279 -0
- package/dist/{cli-e289rj3k.js → cli-7k1fcprd.js} +64 -1
- package/dist/{cli-51a9sav0.js → cli-8xzxm1cq.js} +115 -283
- package/dist/{cli-vdp9s10c.js → cli-cx463q74.js} +8 -0
- package/dist/cli.js +25 -12
- package/dist/{profileCli-5f15dx7k.js → profileCli-c7cvkv5q.js} +129 -17
- package/dist/{profiles-edzz1ffd.js → profiles-rdd84b45.js} +1 -1
- package/dist/proxy/cwd.d.ts +42 -0
- package/dist/proxy/cwd.d.ts.map +1 -0
- package/dist/proxy/errors.d.ts +14 -4
- package/dist/proxy/errors.d.ts.map +1 -1
- package/dist/proxy/models.d.ts +57 -4
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/oauthUsage.d.ts +17 -7
- package/dist/proxy/oauthUsage.d.ts.map +1 -1
- package/dist/proxy/plugins/loader.d.ts.map +1 -1
- package/dist/proxy/profiles.d.ts +12 -4
- package/dist/proxy/profiles.d.ts.map +1 -1
- package/dist/proxy/query.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/tokenRefresh.d.ts +41 -0
- package/dist/proxy/tokenRefresh.d.ts.map +1 -1
- package/dist/server.js +4 -3
- package/dist/{tokenRefresh-psq94r54.js → tokenRefresh-swetnf89.js} +10 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -239,6 +239,20 @@ meridian profile add work
|
|
|
239
239
|
|
|
240
240
|
> **⚠ Important:** Claude's OAuth reuses your browser session. Before adding a second account, sign out of claude.ai and sign into the other account first.
|
|
241
241
|
|
|
242
|
+
#### Headless / CI: register an OAuth token
|
|
243
|
+
|
|
244
|
+
When a browser isn't available (containers, CI runners, remote shells), generate a long-lived OAuth token with `claude setup-token` and register it as a profile:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Prompt for the token (input is hidden — paste the value from `claude setup-token`)
|
|
248
|
+
meridian profile add ci --oauth-token
|
|
249
|
+
|
|
250
|
+
# Or pass it inline
|
|
251
|
+
meridian profile add ci --oauth-token sk-ant-oat01-...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
OAuth-token profiles store the token in `profiles.json` and feed it to the SDK via `CLAUDE_CODE_OAUTH_TOKEN` — no Keychain entry, no browser handshake. To prevent the SDK's 401-recovery from silently falling back to the host's `~/.claude` credentials, OAuth-token profiles also pin `CLAUDE_CONFIG_DIR` to an isolated per-profile directory under `~/.config/meridian/profiles/<name>/`. That directory holds only SDK state (sessions, settings) — never `.credentials.json`, since the token is delivered through the env.
|
|
255
|
+
|
|
242
256
|
### Switching profiles
|
|
243
257
|
|
|
244
258
|
```bash
|
|
@@ -256,14 +270,15 @@ You can also switch profiles from the web UI at `http://127.0.0.1:3456/profiles`
|
|
|
256
270
|
| Command | Description |
|
|
257
271
|
|---------|-------------|
|
|
258
272
|
| `meridian profile add <name>` | Add a profile and authenticate via browser |
|
|
273
|
+
| `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
|
|
259
274
|
| `meridian profile list` | List profiles and auth status |
|
|
260
275
|
| `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
|
|
261
|
-
| `meridian profile login <name>` | Re-authenticate an expired profile |
|
|
276
|
+
| `meridian profile login <name>` | Re-authenticate an expired profile (browser-login profiles only) |
|
|
262
277
|
| `meridian profile remove <name>` | Remove a profile and its credentials |
|
|
263
278
|
|
|
264
279
|
### How it works
|
|
265
280
|
|
|
266
|
-
Each profile stores its credentials in an isolated `CLAUDE_CONFIG_DIR` under `~/.config/meridian/profiles/<name>/`. When a request arrives, Meridian resolves the profile in priority order:
|
|
281
|
+
Each profile stores its credentials in an isolated `CLAUDE_CONFIG_DIR` under `~/.config/meridian/profiles/<name>/`. OAuth-token profiles use the same isolated directory layout — but the token itself lives in `~/.config/meridian/profiles.json` and is fed to the SDK via `CLAUDE_CODE_OAUTH_TOKEN`, so the per-profile dir holds only SDK state (sessions, settings) and never the credential. When a request arrives, Meridian resolves the profile in priority order:
|
|
267
282
|
|
|
268
283
|
1. `x-meridian-profile` request header (per-request override)
|
|
269
284
|
2. Active profile (set via `meridian profile switch` or the web UI)
|
|
@@ -276,11 +291,21 @@ Session state is scoped per profile — switching accounts won't cross-contamina
|
|
|
276
291
|
For advanced setups (CI, Docker), profiles can also be provided via environment variable:
|
|
277
292
|
|
|
278
293
|
```bash
|
|
279
|
-
export MERIDIAN_PROFILES='[
|
|
294
|
+
export MERIDIAN_PROFILES='[
|
|
295
|
+
{"id":"personal","claudeConfigDir":"/path/to/config1"},
|
|
296
|
+
{"id":"work","claudeConfigDir":"/path/to/config2"},
|
|
297
|
+
{"id":"ci","oauthToken":"sk-ant-oat01-..."}
|
|
298
|
+
]'
|
|
280
299
|
export MERIDIAN_DEFAULT_PROFILE=personal
|
|
281
300
|
meridian
|
|
282
301
|
```
|
|
283
302
|
|
|
303
|
+
Profile shapes:
|
|
304
|
+
|
|
305
|
+
- `claudeConfigDir` — points at a `~/.claude`-style directory; uses Claude Max OAuth from that dir
|
|
306
|
+
- `apiKey` (with optional `baseUrl`) — direct Anthropic API access; sets `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`
|
|
307
|
+
- `oauthToken` — long-lived token from `claude setup-token`; sets `CLAUDE_CODE_OAUTH_TOKEN`, no config dir needed
|
|
308
|
+
|
|
284
309
|
When `MERIDIAN_PROFILES` is set, it takes precedence over disk-configured profiles. When unset, Meridian auto-discovers profiles from `~/.config/meridian/profiles.json` on each request.
|
|
285
310
|
|
|
286
311
|
## Agent Setup
|
|
@@ -710,9 +735,10 @@ export default {
|
|
|
710
735
|
| `meridian` | Start the proxy server |
|
|
711
736
|
| `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
|
|
712
737
|
| `meridian profile add <name>` | Add a profile and authenticate via browser |
|
|
738
|
+
| `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
|
|
713
739
|
| `meridian profile list` | List all profiles and their auth status |
|
|
714
740
|
| `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
|
|
715
|
-
| `meridian profile login <name>` | Re-authenticate an expired profile |
|
|
741
|
+
| `meridian profile login <name>` | Re-authenticate an expired profile (browser-login profiles only) |
|
|
716
742
|
| `meridian profile remove <name>` | Remove a profile and its credentials |
|
|
717
743
|
| `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
|
|
718
744
|
|
|
@@ -767,6 +793,19 @@ docker run \
|
|
|
767
793
|
|
|
768
794
|
Switch profiles at runtime via the `x-meridian-profile` header or `meridian profile switch` (see [Multi-Profile Support](#multi-profile-support)).
|
|
769
795
|
|
|
796
|
+
### OAuth-token profiles in Docker (no volume mount)
|
|
797
|
+
|
|
798
|
+
If you'd rather not mount a credential directory, generate a long-lived OAuth token on the host with `claude setup-token` and pass it as a profile. There's nothing to mount — the token alone is the credential:
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
docker run \
|
|
802
|
+
-e 'MERIDIAN_PROFILES=[{"id":"ci","oauthToken":"sk-ant-oat01-..."}]' \
|
|
803
|
+
-e MERIDIAN_DEFAULT_PROFILE=ci \
|
|
804
|
+
-p 3456:3456 meridian
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
This is the recommended path for CI runners, ephemeral containers, and cross-host deployments where browser-based login isn't reachable. Treat the token like any other secret — inject it via your platform's secret store rather than committing it to your image or compose file.
|
|
808
|
+
|
|
770
809
|
## Testing
|
|
771
810
|
|
|
772
811
|
```bash
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// src/proxy/models.ts
|
|
2
|
+
import { exec as execCallback, execFile as execFileCallback } from "child_process";
|
|
3
|
+
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
var exec = promisify(execCallback);
|
|
8
|
+
var execFile = promisify(execFileCallback);
|
|
9
|
+
var STUB_SIZE_THRESHOLD = 4096;
|
|
10
|
+
var CANONICAL_OPUS_MODEL = "claude-opus-4-7";
|
|
11
|
+
var CANONICAL_SONNET_MODEL = "claude-sonnet-4-6";
|
|
12
|
+
var CANONICAL_HAIKU_MODEL = "claude-haiku-4-5";
|
|
13
|
+
function resolveSdkModelDefaults(env = process.env) {
|
|
14
|
+
return {
|
|
15
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: env.MERIDIAN_DEFAULT_OPUS_MODEL ?? CANONICAL_OPUS_MODEL,
|
|
16
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: env.MERIDIAN_DEFAULT_SONNET_MODEL ?? CANONICAL_SONNET_MODEL,
|
|
17
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: env.MERIDIAN_DEFAULT_HAIKU_MODEL ?? CANONICAL_HAIKU_MODEL
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
var AUTH_STATUS_CACHE_TTL_MS = 60000;
|
|
21
|
+
var AUTH_STATUS_FAILURE_TTL_MS = 5000;
|
|
22
|
+
var cachedAuthStatus = null;
|
|
23
|
+
var lastKnownGoodAuthStatus = null;
|
|
24
|
+
var cachedAuthStatusAt = 0;
|
|
25
|
+
var cachedAuthStatusIsFailure = false;
|
|
26
|
+
var cachedAuthStatusPromise = null;
|
|
27
|
+
function supports1mContext(model) {
|
|
28
|
+
if (model.includes("4-5") || model.includes("4.5"))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function mapModelToClaudeModel(model, subscriptionType, agentMode) {
|
|
33
|
+
if (model.includes("haiku"))
|
|
34
|
+
return "haiku";
|
|
35
|
+
const use1m = supports1mContext(model);
|
|
36
|
+
const isSubagent = agentMode === "subagent";
|
|
37
|
+
if (model.includes("opus")) {
|
|
38
|
+
if (use1m && !isSubagent && !isExtendedContextKnownUnavailable())
|
|
39
|
+
return "opus[1m]";
|
|
40
|
+
return "opus";
|
|
41
|
+
}
|
|
42
|
+
const sonnetOverride = process.env.MERIDIAN_SONNET_MODEL ?? process.env.CLAUDE_PROXY_SONNET_MODEL;
|
|
43
|
+
if (sonnetOverride === "sonnet[1m]") {
|
|
44
|
+
if (!use1m || isSubagent || isExtendedContextKnownUnavailable())
|
|
45
|
+
return "sonnet";
|
|
46
|
+
return "sonnet[1m]";
|
|
47
|
+
}
|
|
48
|
+
return "sonnet";
|
|
49
|
+
}
|
|
50
|
+
var EXTRA_USAGE_RETRY_MS = 60 * 60 * 1000;
|
|
51
|
+
var extraUsageUnavailableAt = 0;
|
|
52
|
+
function recordExtendedContextUnavailable() {
|
|
53
|
+
extraUsageUnavailableAt = Date.now();
|
|
54
|
+
}
|
|
55
|
+
function isExtendedContextKnownUnavailable() {
|
|
56
|
+
return extraUsageUnavailableAt > 0 && Date.now() - extraUsageUnavailableAt < EXTRA_USAGE_RETRY_MS;
|
|
57
|
+
}
|
|
58
|
+
function stripExtendedContext(model) {
|
|
59
|
+
if (model === "opus[1m]")
|
|
60
|
+
return "opus";
|
|
61
|
+
if (model === "sonnet[1m]")
|
|
62
|
+
return "sonnet";
|
|
63
|
+
return model;
|
|
64
|
+
}
|
|
65
|
+
function hasExtendedContext(model) {
|
|
66
|
+
return model.endsWith("[1m]");
|
|
67
|
+
}
|
|
68
|
+
var profileAuthCaches = new Map;
|
|
69
|
+
function getAuthCacheInfo(profileId) {
|
|
70
|
+
if (!profileId) {
|
|
71
|
+
return { lastCheckedAt: cachedAuthStatusAt, lastSuccessAt: cachedAuthStatusIsFailure ? 0 : cachedAuthStatusAt, isFailure: cachedAuthStatusIsFailure };
|
|
72
|
+
}
|
|
73
|
+
const cache = profileAuthCaches.get(profileId);
|
|
74
|
+
if (!cache)
|
|
75
|
+
return { lastCheckedAt: 0, lastSuccessAt: 0, isFailure: false };
|
|
76
|
+
return { lastCheckedAt: cache.at, lastSuccessAt: cache.lastSuccessAt, isFailure: cache.isFailure };
|
|
77
|
+
}
|
|
78
|
+
function getAuthCache(key) {
|
|
79
|
+
let cache = profileAuthCaches.get(key);
|
|
80
|
+
if (!cache) {
|
|
81
|
+
cache = { status: null, lastKnownGood: null, at: 0, isFailure: false, promise: null, lastSuccessAt: 0 };
|
|
82
|
+
profileAuthCaches.set(key, cache);
|
|
83
|
+
}
|
|
84
|
+
return cache;
|
|
85
|
+
}
|
|
86
|
+
async function getClaudeAuthStatusAsync(profileId, envOverrides) {
|
|
87
|
+
const isDefault = !profileId;
|
|
88
|
+
const cache = isDefault ? null : getAuthCache(profileId);
|
|
89
|
+
const c_status = cache ? cache.status : cachedAuthStatus;
|
|
90
|
+
const c_lastKnownGood = cache ? cache.lastKnownGood : lastKnownGoodAuthStatus;
|
|
91
|
+
const c_at = cache ? cache.at : cachedAuthStatusAt;
|
|
92
|
+
const c_isFailure = cache ? cache.isFailure : cachedAuthStatusIsFailure;
|
|
93
|
+
let c_promise = cache ? cache.promise : cachedAuthStatusPromise;
|
|
94
|
+
const ttl = c_isFailure ? AUTH_STATUS_FAILURE_TTL_MS : AUTH_STATUS_CACHE_TTL_MS;
|
|
95
|
+
if (c_at > 0 && Date.now() - c_at < ttl) {
|
|
96
|
+
return c_status ?? c_lastKnownGood;
|
|
97
|
+
}
|
|
98
|
+
if (c_promise)
|
|
99
|
+
return c_promise;
|
|
100
|
+
c_promise = (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const claudePath = await resolveClaudeExecutableAsync();
|
|
103
|
+
const { stdout } = await execFile(claudePath, ["auth", "status"], {
|
|
104
|
+
timeout: 5000,
|
|
105
|
+
...envOverrides ? { env: { ...process.env, ...envOverrides } } : {}
|
|
106
|
+
});
|
|
107
|
+
const parsed = JSON.parse(stdout);
|
|
108
|
+
if (cache) {
|
|
109
|
+
cache.status = parsed;
|
|
110
|
+
cache.lastKnownGood = parsed;
|
|
111
|
+
cache.at = Date.now();
|
|
112
|
+
cache.isFailure = false;
|
|
113
|
+
cache.lastSuccessAt = Date.now();
|
|
114
|
+
} else {
|
|
115
|
+
cachedAuthStatus = parsed;
|
|
116
|
+
lastKnownGoodAuthStatus = parsed;
|
|
117
|
+
cachedAuthStatusAt = Date.now();
|
|
118
|
+
cachedAuthStatusIsFailure = false;
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
} catch {
|
|
122
|
+
if (cache) {
|
|
123
|
+
cache.isFailure = true;
|
|
124
|
+
cache.at = Date.now();
|
|
125
|
+
cache.status = null;
|
|
126
|
+
return cache.lastKnownGood;
|
|
127
|
+
} else {
|
|
128
|
+
cachedAuthStatusIsFailure = true;
|
|
129
|
+
cachedAuthStatusAt = Date.now();
|
|
130
|
+
cachedAuthStatus = null;
|
|
131
|
+
return lastKnownGoodAuthStatus;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
if (cache)
|
|
136
|
+
cache.promise = c_promise;
|
|
137
|
+
else
|
|
138
|
+
cachedAuthStatusPromise = c_promise;
|
|
139
|
+
try {
|
|
140
|
+
return await c_promise;
|
|
141
|
+
} finally {
|
|
142
|
+
if (cache)
|
|
143
|
+
cache.promise = null;
|
|
144
|
+
else
|
|
145
|
+
cachedAuthStatusPromise = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var cachedClaudeInfo = null;
|
|
149
|
+
var cachedClaudePathPromise = null;
|
|
150
|
+
var DEFAULT_DEPS = {
|
|
151
|
+
existsSync,
|
|
152
|
+
statSync: (p) => statSync(p),
|
|
153
|
+
exec,
|
|
154
|
+
resolvePackage: (specifier) => fileURLToPath(import.meta.resolve(specifier)),
|
|
155
|
+
envGet: (name) => process.env[name],
|
|
156
|
+
platform: process.platform,
|
|
157
|
+
arch: process.arch,
|
|
158
|
+
isBun: typeof process.versions.bun !== "undefined"
|
|
159
|
+
};
|
|
160
|
+
function tryEnvOverride(deps) {
|
|
161
|
+
const explicit = deps.envGet("MERIDIAN_CLAUDE_PATH");
|
|
162
|
+
if (!explicit)
|
|
163
|
+
return null;
|
|
164
|
+
return deps.existsSync(explicit) ? explicit : null;
|
|
165
|
+
}
|
|
166
|
+
function tryBundledBinary(deps) {
|
|
167
|
+
try {
|
|
168
|
+
const pkgPath = deps.resolvePackage("@anthropic-ai/claude-code/package.json");
|
|
169
|
+
const bundled = join(dirname(pkgPath), "bin", "claude.exe");
|
|
170
|
+
if (!deps.existsSync(bundled))
|
|
171
|
+
return null;
|
|
172
|
+
const size = deps.statSync(bundled).size;
|
|
173
|
+
if (size <= STUB_SIZE_THRESHOLD)
|
|
174
|
+
return null;
|
|
175
|
+
return bundled;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function tryPlatformPackage(deps) {
|
|
181
|
+
const binName = deps.platform === "win32" ? "claude.exe" : "claude";
|
|
182
|
+
const candidates = [`@anthropic-ai/claude-code-${deps.platform}-${deps.arch}`];
|
|
183
|
+
if (deps.platform === "linux") {
|
|
184
|
+
candidates.push(`@anthropic-ai/claude-code-${deps.platform}-${deps.arch}-musl`);
|
|
185
|
+
}
|
|
186
|
+
for (const pkg of candidates) {
|
|
187
|
+
try {
|
|
188
|
+
const pkgJson = deps.resolvePackage(`${pkg}/package.json`);
|
|
189
|
+
const candidate = join(dirname(pkgJson), binName);
|
|
190
|
+
if (deps.existsSync(candidate))
|
|
191
|
+
return candidate;
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async function tryPathLookup(deps) {
|
|
197
|
+
const cmd = deps.platform === "win32" ? "where claude" : "which claude";
|
|
198
|
+
try {
|
|
199
|
+
const { stdout } = await deps.exec(cmd);
|
|
200
|
+
const candidates = stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
201
|
+
for (const candidate of candidates) {
|
|
202
|
+
if (deps.platform === "win32" && candidate.startsWith("/"))
|
|
203
|
+
continue;
|
|
204
|
+
if (deps.existsSync(candidate))
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function tryLegacySdkCliJs(deps) {
|
|
211
|
+
if (!deps.isBun)
|
|
212
|
+
return null;
|
|
213
|
+
try {
|
|
214
|
+
const sdkPath = deps.resolvePackage("@anthropic-ai/claude-agent-sdk");
|
|
215
|
+
const cliJs = join(dirname(sdkPath), "cli.js");
|
|
216
|
+
return deps.existsSync(cliJs) ? cliJs : null;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function resolveClaudeExecutableWithSource(deps = DEFAULT_DEPS) {
|
|
222
|
+
const env = tryEnvOverride(deps);
|
|
223
|
+
if (env)
|
|
224
|
+
return { path: env, source: "env" };
|
|
225
|
+
const bundled = tryBundledBinary(deps);
|
|
226
|
+
if (bundled)
|
|
227
|
+
return { path: bundled, source: "bundled" };
|
|
228
|
+
const platformPkg = tryPlatformPackage(deps);
|
|
229
|
+
if (platformPkg)
|
|
230
|
+
return { path: platformPkg, source: "platform-package" };
|
|
231
|
+
const pathLookup = await tryPathLookup(deps);
|
|
232
|
+
if (pathLookup)
|
|
233
|
+
return { path: pathLookup, source: "path-lookup" };
|
|
234
|
+
const legacy = tryLegacySdkCliJs(deps);
|
|
235
|
+
if (legacy)
|
|
236
|
+
return { path: legacy, source: "legacy-cli-js" };
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function resolveClaudeExecutableSync(deps = DEFAULT_DEPS) {
|
|
240
|
+
const env = tryEnvOverride(deps);
|
|
241
|
+
if (env)
|
|
242
|
+
return { path: env, source: "env" };
|
|
243
|
+
const bundled = tryBundledBinary(deps);
|
|
244
|
+
if (bundled)
|
|
245
|
+
return { path: bundled, source: "bundled" };
|
|
246
|
+
const platformPkg = tryPlatformPackage(deps);
|
|
247
|
+
if (platformPkg)
|
|
248
|
+
return { path: platformPkg, source: "platform-package" };
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function getResolvedClaudeExecutableInfo() {
|
|
252
|
+
return cachedClaudeInfo;
|
|
253
|
+
}
|
|
254
|
+
async function resolveClaudeExecutableAsync() {
|
|
255
|
+
if (cachedClaudeInfo)
|
|
256
|
+
return cachedClaudeInfo.path;
|
|
257
|
+
if (cachedClaudePathPromise)
|
|
258
|
+
return cachedClaudePathPromise;
|
|
259
|
+
cachedClaudePathPromise = (async () => {
|
|
260
|
+
const resolved = await resolveClaudeExecutableWithSource();
|
|
261
|
+
if (resolved) {
|
|
262
|
+
cachedClaudeInfo = resolved;
|
|
263
|
+
return resolved.path;
|
|
264
|
+
}
|
|
265
|
+
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.");
|
|
266
|
+
})();
|
|
267
|
+
try {
|
|
268
|
+
return await cachedClaudePathPromise;
|
|
269
|
+
} finally {
|
|
270
|
+
cachedClaudePathPromise = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function isClosedControllerError(error) {
|
|
274
|
+
if (!(error instanceof Error))
|
|
275
|
+
return false;
|
|
276
|
+
return error.message.includes("Controller is already closed");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { resolveSdkModelDefaults, mapModelToClaudeModel, recordExtendedContextUnavailable, stripExtendedContext, hasExtendedContext, getAuthCacheInfo, getClaudeAuthStatusAsync, resolveClaudeExecutableSync, getResolvedClaudeExecutableInfo, resolveClaudeExecutableAsync, isClosedControllerError };
|
|
@@ -229,8 +229,71 @@ async function doRefresh(store) {
|
|
|
229
229
|
claudeLog("token_refresh.success", { expiresAt });
|
|
230
230
|
return true;
|
|
231
231
|
}
|
|
232
|
+
async function ensureFreshToken(store, bufferMs = 5 * 60 * 1000) {
|
|
233
|
+
const s = store ?? createPlatformCredentialStore();
|
|
234
|
+
const credentials = await s.read();
|
|
235
|
+
const expiresAt = credentials?.claudeAiOauth?.expiresAt;
|
|
236
|
+
if (!expiresAt)
|
|
237
|
+
return false;
|
|
238
|
+
if (expiresAt - Date.now() > bufferMs)
|
|
239
|
+
return true;
|
|
240
|
+
return refreshOAuthToken(s);
|
|
241
|
+
}
|
|
242
|
+
var scheduledRefreshTimer = null;
|
|
243
|
+
var scheduledRefreshActive = false;
|
|
244
|
+
var scheduledRefreshGeneration = 0;
|
|
245
|
+
function startBackgroundRefresh(store, bufferMs = 5 * 60 * 1000, failureRetryMs = 5 * 60 * 1000) {
|
|
246
|
+
if (scheduledRefreshActive)
|
|
247
|
+
return;
|
|
248
|
+
scheduledRefreshActive = true;
|
|
249
|
+
const gen = ++scheduledRefreshGeneration;
|
|
250
|
+
scheduleNext(store ?? createPlatformCredentialStore(), bufferMs, failureRetryMs, gen);
|
|
251
|
+
}
|
|
252
|
+
function stopBackgroundRefresh() {
|
|
253
|
+
scheduledRefreshActive = false;
|
|
254
|
+
scheduledRefreshGeneration++;
|
|
255
|
+
if (scheduledRefreshTimer)
|
|
256
|
+
clearTimeout(scheduledRefreshTimer);
|
|
257
|
+
scheduledRefreshTimer = null;
|
|
258
|
+
}
|
|
259
|
+
async function scheduleNext(store, bufferMs, failureRetryMs, gen) {
|
|
260
|
+
if (!scheduledRefreshActive || gen !== scheduledRefreshGeneration)
|
|
261
|
+
return;
|
|
262
|
+
const credentials = await store.read().catch(() => null);
|
|
263
|
+
if (!scheduledRefreshActive || gen !== scheduledRefreshGeneration)
|
|
264
|
+
return;
|
|
265
|
+
const expiresAt = credentials?.claudeAiOauth?.expiresAt;
|
|
266
|
+
if (!expiresAt) {
|
|
267
|
+
armTimer(failureRetryMs, store, bufferMs, failureRetryMs, gen);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const dueIn = expiresAt - Date.now() - bufferMs;
|
|
271
|
+
if (dueIn <= 0) {
|
|
272
|
+
const ok = await refreshOAuthToken(store);
|
|
273
|
+
if (!scheduledRefreshActive || gen !== scheduledRefreshGeneration)
|
|
274
|
+
return;
|
|
275
|
+
claudeLog("token_refresh.scheduled", { ok, immediate: true });
|
|
276
|
+
console.error(`[token_refresh] scheduled refresh (immediate) ok=${ok}`);
|
|
277
|
+
armTimer(ok ? 0 : failureRetryMs, store, bufferMs, failureRetryMs, gen);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
armTimer(dueIn, store, bufferMs, failureRetryMs, gen);
|
|
281
|
+
}
|
|
282
|
+
function armTimer(delayMs, store, bufferMs, failureRetryMs, gen) {
|
|
283
|
+
scheduledRefreshTimer = setTimeout(async () => {
|
|
284
|
+
if (!scheduledRefreshActive || gen !== scheduledRefreshGeneration)
|
|
285
|
+
return;
|
|
286
|
+
scheduleNext(store, bufferMs, failureRetryMs, gen);
|
|
287
|
+
}, delayMs);
|
|
288
|
+
if (scheduledRefreshTimer && scheduledRefreshTimer.unref) {
|
|
289
|
+
scheduledRefreshTimer.unref();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function isBackgroundRefreshActive() {
|
|
293
|
+
return scheduledRefreshActive;
|
|
294
|
+
}
|
|
232
295
|
function resetInflightRefresh() {
|
|
233
296
|
inflightRefresh = null;
|
|
234
297
|
}
|
|
235
298
|
|
|
236
|
-
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, resetInflightRefresh };
|
|
299
|
+
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };
|