@rynfar/meridian 1.41.0 → 1.42.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 +43 -4
- package/dist/{cli-0eky480v.js → cli-7k1fcprd.js} +69 -3
- package/dist/{cli-vdp9s10c.js → cli-cx463q74.js} +8 -0
- package/dist/{cli-swjr844z.js → cli-kvwnarfk.js} +247 -93
- package/dist/cli.js +15 -9
- package/dist/{profileCli-5f15dx7k.js → profileCli-wpb4qbjn.js} +105 -13
- package/dist/{profiles-edzz1ffd.js → profiles-rdd84b45.js} +1 -1
- package/dist/proxy/adapters/droid.d.ts.map +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 +61 -1
- 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/sdkFeatures.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/tokenRefresh.d.ts +52 -0
- package/dist/proxy/tokenRefresh.d.ts.map +1 -1
- package/dist/proxy/transforms/droid.d.ts.map +1 -1
- package/dist/server.js +3 -3
- package/dist/{tokenRefresh-3kh1e8q8.js → tokenRefresh-swetnf89.js} +12 -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
|
|
@@ -85,6 +85,9 @@ function configDirToKeychainService(claudeConfigDir) {
|
|
|
85
85
|
function configDirToCredentialsFile(claudeConfigDir) {
|
|
86
86
|
return join(resolve(claudeConfigDir), ".credentials.json");
|
|
87
87
|
}
|
|
88
|
+
function serializeCredentials(credentials) {
|
|
89
|
+
return JSON.stringify(credentials);
|
|
90
|
+
}
|
|
88
91
|
function parseKeychainValue(raw) {
|
|
89
92
|
const trimmed = raw.trim();
|
|
90
93
|
try {
|
|
@@ -113,7 +116,7 @@ function buildMacosStore(serviceName) {
|
|
|
113
116
|
}
|
|
114
117
|
},
|
|
115
118
|
async write(credentials) {
|
|
116
|
-
const json =
|
|
119
|
+
const json = serializeCredentials(credentials);
|
|
117
120
|
const wasHex = keychainWasHexByService.get(serviceName) ?? false;
|
|
118
121
|
const value = wasHex ? Buffer.from(json).toString("hex") : json;
|
|
119
122
|
try {
|
|
@@ -142,7 +145,7 @@ function buildFileStore(filePath) {
|
|
|
142
145
|
async write(credentials) {
|
|
143
146
|
try {
|
|
144
147
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
145
|
-
writeFileSync(filePath,
|
|
148
|
+
writeFileSync(filePath, serializeCredentials(credentials), "utf-8");
|
|
146
149
|
return true;
|
|
147
150
|
} catch (err) {
|
|
148
151
|
claudeLog("token_refresh.file_write_failed", { path: filePath, error: String(err) });
|
|
@@ -226,8 +229,71 @@ async function doRefresh(store) {
|
|
|
226
229
|
claudeLog("token_refresh.success", { expiresAt });
|
|
227
230
|
return true;
|
|
228
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
|
+
}
|
|
229
295
|
function resetInflightRefresh() {
|
|
230
296
|
inflightRefresh = null;
|
|
231
297
|
}
|
|
232
298
|
|
|
233
|
-
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, resetInflightRefresh };
|
|
299
|
+
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };
|
|
@@ -86,6 +86,14 @@ function resolveProfile(profiles, defaultProfile, requestedId) {
|
|
|
86
86
|
return buildResolvedProfile(profile);
|
|
87
87
|
}
|
|
88
88
|
function buildResolvedProfile(profile) {
|
|
89
|
+
if (profile.oauthToken || profile.type === "oauth-token") {
|
|
90
|
+
const env2 = {};
|
|
91
|
+
if (profile.oauthToken) {
|
|
92
|
+
env2.CLAUDE_CODE_OAUTH_TOKEN = profile.oauthToken;
|
|
93
|
+
env2.CLAUDE_CONFIG_DIR = join(homedir(), ".config", "meridian", "profiles", profile.id);
|
|
94
|
+
}
|
|
95
|
+
return { id: profile.id, type: "oauth-token", env: env2 };
|
|
96
|
+
}
|
|
89
97
|
const type = profile.type ?? "claude-max";
|
|
90
98
|
if (type === "api") {
|
|
91
99
|
const env2 = {};
|