@rynfar/meridian 1.42.1 → 1.44.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 +40 -7
- package/dist/{cli-7k1fcprd.js → cli-1kbcm3yn.js} +37 -20
- package/dist/{cli-8xzxm1cq.js → cli-fc6mt326.js} +246 -48
- package/dist/{cli-rtab0qa6.js → cli-je60fevk.js} +38 -20
- package/dist/{cli-3jqvrake.js → cli-yeazzt32.js} +1 -1
- package/dist/cli.js +26 -19
- package/dist/{profileCli-c7cvkv5q.js → profileCli-xcmmr5w4.js} +209 -47
- package/dist/proxy/adapters/detect.d.ts.map +1 -1
- package/dist/proxy/adapters/openai.d.ts +23 -0
- package/dist/proxy/adapters/openai.d.ts.map +1 -0
- package/dist/proxy/effort.d.ts +21 -0
- package/dist/proxy/effort.d.ts.map +1 -0
- package/dist/proxy/models.d.ts +1 -1
- package/dist/proxy/openai.d.ts +12 -0
- package/dist/proxy/openai.d.ts.map +1 -1
- package/dist/proxy/query.d.ts +5 -2
- package/dist/proxy/query.d.ts.map +1 -1
- package/dist/proxy/sdkFeatures.d.ts.map +1 -1
- package/dist/proxy/server.d.ts +1 -0
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/setup.d.ts +9 -0
- package/dist/proxy/setup.d.ts.map +1 -1
- package/dist/proxy/tokenRefresh.d.ts +2 -0
- package/dist/proxy/tokenRefresh.d.ts.map +1 -1
- package/dist/proxy/transforms/registry.d.ts.map +1 -1
- package/dist/proxy/types.d.ts +7 -0
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/server.js +6 -4
- package/dist/{setup-v5pnqe04.js → setup-6c11e8d6.js} +4 -2
- package/dist/{tokenRefresh-swetnf89.js → tokenRefresh-pvc2q8ea.js} +1 -1
- package/package.json +4 -3
|
@@ -3,6 +3,24 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { homedir, platform } from "os";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
+
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
7
|
+
|
|
8
|
+
class UnparseableConfigError extends Error {
|
|
9
|
+
configPath;
|
|
10
|
+
constructor(configPath) {
|
|
11
|
+
super(`Could not parse ${configPath} — it may contain a syntax error.`);
|
|
12
|
+
this.configPath = configPath;
|
|
13
|
+
this.name = "UnparseableConfigError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function parseOpencodeConfig(text) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
const parsed = parseJsonc(text, errors, { allowTrailingComma: true });
|
|
19
|
+
if (errors.length > 0 || parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
6
24
|
function findOpencodeConfigPath() {
|
|
7
25
|
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
8
26
|
return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json");
|
|
@@ -31,37 +49,37 @@ function checkPluginConfigured(configPath) {
|
|
|
31
49
|
const path = configPath ?? findOpencodeConfigPath();
|
|
32
50
|
if (!existsSync(path))
|
|
33
51
|
return false;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const config = JSON.parse(raw);
|
|
37
|
-
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
38
|
-
return plugins.some((p) => typeof p === "string" && isMeridianEntry(p));
|
|
39
|
-
} catch {
|
|
52
|
+
const config = parseOpencodeConfig(readFileSync(path, "utf-8"));
|
|
53
|
+
if (config === null)
|
|
40
54
|
return false;
|
|
41
|
-
|
|
55
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
56
|
+
return plugins.some((p) => typeof p === "string" && isMeridianEntry(p));
|
|
42
57
|
}
|
|
43
58
|
function runSetup(pluginPath, configPath) {
|
|
44
59
|
const path = configPath ?? findOpencodeConfigPath();
|
|
45
60
|
const dir = dirname(path);
|
|
46
|
-
|
|
47
|
-
let created = false;
|
|
48
|
-
if (existsSync(path)) {
|
|
49
|
-
try {
|
|
50
|
-
config = JSON.parse(readFileSync(path, "utf-8"));
|
|
51
|
-
} catch {}
|
|
52
|
-
} else {
|
|
53
|
-
created = true;
|
|
61
|
+
if (!existsSync(path)) {
|
|
54
62
|
if (!existsSync(dir))
|
|
55
63
|
mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(path, `${JSON.stringify({ plugin: [pluginPath] }, null, 2)}
|
|
65
|
+
`, "utf-8");
|
|
66
|
+
return { configPath: path, pluginPath, alreadyConfigured: false, removedStale: [], created: true };
|
|
67
|
+
}
|
|
68
|
+
const text = readFileSync(path, "utf-8");
|
|
69
|
+
const config = parseOpencodeConfig(text);
|
|
70
|
+
if (config === null) {
|
|
71
|
+
throw new UnparseableConfigError(path);
|
|
56
72
|
}
|
|
57
73
|
const existing = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
|
|
58
74
|
const removedStale = existing.filter(isMeridianEntry);
|
|
59
75
|
const others = existing.filter((p) => !isMeridianEntry(p));
|
|
60
76
|
const alreadyConfigured = removedStale.some((p) => p === pluginPath);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const newPlugins = [...others, pluginPath];
|
|
78
|
+
const edits = modify(text, ["plugin"], newPlugins, {
|
|
79
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
80
|
+
});
|
|
81
|
+
writeFileSync(path, applyEdits(text, edits), "utf-8");
|
|
82
|
+
return { configPath: path, pluginPath, alreadyConfigured, removedStale, created: false };
|
|
65
83
|
}
|
|
66
84
|
|
|
67
|
-
export { findOpencodeConfigPath, findPluginPath, checkPluginConfigured, runSetup };
|
|
85
|
+
export { UnparseableConfigError, findOpencodeConfigPath, findPluginPath, checkPluginConfigured, runSetup };
|
|
@@ -7,7 +7,7 @@ import { promisify } from "util";
|
|
|
7
7
|
var exec = promisify(execCallback);
|
|
8
8
|
var execFile = promisify(execFileCallback);
|
|
9
9
|
var STUB_SIZE_THRESHOLD = 4096;
|
|
10
|
-
var CANONICAL_OPUS_MODEL = "claude-opus-4-
|
|
10
|
+
var CANONICAL_OPUS_MODEL = "claude-opus-4-8";
|
|
11
11
|
var CANONICAL_SONNET_MODEL = "claude-sonnet-4-6";
|
|
12
12
|
var CANONICAL_HAIKU_MODEL = "claude-haiku-4-5";
|
|
13
13
|
function resolveSdkModelDefaults(env = process.env) {
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startProxyServer
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-fc6mt326.js";
|
|
5
5
|
import"./cli-cx463q74.js";
|
|
6
6
|
import"./cli-sry5aqdj.js";
|
|
7
7
|
import"./cli-4rqtm83g.js";
|
|
8
8
|
import {
|
|
9
9
|
resolveClaudeExecutableAsync
|
|
10
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-yeazzt32.js";
|
|
11
11
|
import"./cli-340h1chz.js";
|
|
12
|
-
import"./cli-
|
|
13
|
-
import"./cli-
|
|
12
|
+
import"./cli-je60fevk.js";
|
|
13
|
+
import"./cli-1kbcm3yn.js";
|
|
14
14
|
import {
|
|
15
15
|
__require
|
|
16
16
|
} from "./cli-p9swy5t3.js";
|
|
@@ -53,16 +53,17 @@ See https://github.com/rynfar/meridian for full documentation.`);
|
|
|
53
53
|
process.exit(0);
|
|
54
54
|
}
|
|
55
55
|
if (args[0] === "profile") {
|
|
56
|
-
const { profileAdd, profileAddOauthToken, profileList, profileRemove, profileSwitch, profileLogin, profileHelp } = await import("./profileCli-
|
|
56
|
+
const { profileAdd, profileAddOauthToken, profileList, profileRemove, profileSwitch, profileLogin, profileHelp } = await import("./profileCli-xcmmr5w4.js");
|
|
57
57
|
const subcommand = args[1];
|
|
58
58
|
const profileId = args[2];
|
|
59
|
+
const headless = args.includes("--headless");
|
|
59
60
|
if (subcommand === "add" && profileId) {
|
|
60
61
|
const oauthFlagIdx = args.indexOf("--oauth-token", 3);
|
|
61
62
|
if (oauthFlagIdx >= 0) {
|
|
62
63
|
const tokenArg = args[oauthFlagIdx + 1];
|
|
63
|
-
await profileAddOauthToken(profileId, tokenArg);
|
|
64
|
+
await profileAddOauthToken(profileId, tokenArg?.startsWith("--") ? undefined : tokenArg);
|
|
64
65
|
} else {
|
|
65
|
-
profileAdd(profileId);
|
|
66
|
+
await profileAdd(profileId, { headless });
|
|
66
67
|
}
|
|
67
68
|
} else if (subcommand === "list" || subcommand === "ls")
|
|
68
69
|
profileList();
|
|
@@ -71,15 +72,27 @@ if (args[0] === "profile") {
|
|
|
71
72
|
else if (subcommand === "switch" && profileId)
|
|
72
73
|
await profileSwitch(profileId);
|
|
73
74
|
else if (subcommand === "login" && profileId)
|
|
74
|
-
profileLogin(profileId);
|
|
75
|
+
await profileLogin(profileId, { headless });
|
|
75
76
|
else
|
|
76
77
|
profileHelp();
|
|
77
78
|
process.exit(0);
|
|
78
79
|
}
|
|
79
80
|
if (args[0] === "setup") {
|
|
80
|
-
const { findPluginPath, runSetup } = await import("./setup-
|
|
81
|
+
const { findPluginPath, runSetup, UnparseableConfigError } = await import("./setup-6c11e8d6.js");
|
|
81
82
|
const pluginPath = findPluginPath(import.meta.url);
|
|
82
|
-
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = runSetup(pluginPath);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof UnparseableConfigError) {
|
|
88
|
+
console.error(`\x1B[31m✗ Could not parse ${err.configPath}\x1B[0m`);
|
|
89
|
+
console.error(" Your config was left untouched. Fix the syntax error, then re-run");
|
|
90
|
+
console.error(` 'meridian setup' — or add this plugin manually:`);
|
|
91
|
+
console.error(` "plugin": ["${pluginPath}"]`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
83
96
|
if (result.alreadyConfigured) {
|
|
84
97
|
console.log(`\x1B[32m✓ Meridian plugin already configured\x1B[0m`);
|
|
85
98
|
console.log(` ${result.configPath}`);
|
|
@@ -98,7 +111,7 @@ Restart OpenCode for the plugin to take effect.`);
|
|
|
98
111
|
process.exit(0);
|
|
99
112
|
}
|
|
100
113
|
if (args[0] === "refresh-token") {
|
|
101
|
-
const { refreshOAuthToken } = await import("./tokenRefresh-
|
|
114
|
+
const { refreshOAuthToken } = await import("./tokenRefresh-pvc2q8ea.js");
|
|
102
115
|
const success = await refreshOAuthToken();
|
|
103
116
|
if (success) {
|
|
104
117
|
console.log("Token refreshed successfully");
|
|
@@ -110,12 +123,6 @@ if (args[0] === "refresh-token") {
|
|
|
110
123
|
}
|
|
111
124
|
var exec = promisify(execCallback);
|
|
112
125
|
var execFile = promisify(execFileCallback);
|
|
113
|
-
process.on("uncaughtException", (err) => {
|
|
114
|
-
console.error(`[PROXY] Uncaught exception (recovered): ${err.message}`);
|
|
115
|
-
});
|
|
116
|
-
process.on("unhandledRejection", (reason) => {
|
|
117
|
-
console.error(`[PROXY] Unhandled rejection (recovered): ${reason instanceof Error ? reason.message : reason}`);
|
|
118
|
-
});
|
|
119
126
|
var port = parseInt(process.env.MERIDIAN_PORT ?? process.env.CLAUDE_PROXY_PORT ?? "3456", 10);
|
|
120
127
|
var host = process.env.MERIDIAN_HOST ?? process.env.CLAUDE_PROXY_HOST ?? "127.0.0.1";
|
|
121
128
|
var idleTimeoutSeconds = parseInt(process.env.MERIDIAN_IDLE_TIMEOUT_SECONDS ?? process.env.CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS ?? "120", 10);
|
|
@@ -135,7 +142,7 @@ async function runCli(start = startProxyServer, runAuthCheck = async () => {
|
|
|
135
142
|
return execFile(claudePath, ["auth", "status"], { timeout: 5000 });
|
|
136
143
|
}) {
|
|
137
144
|
try {
|
|
138
|
-
const { findOpencodeConfigPath, checkPluginConfigured, findPluginPath } = await import("./setup-
|
|
145
|
+
const { findOpencodeConfigPath, checkPluginConfigured, findPluginPath } = await import("./setup-6c11e8d6.js");
|
|
139
146
|
const configPath = findOpencodeConfigPath();
|
|
140
147
|
const { existsSync } = await import("fs");
|
|
141
148
|
if (existsSync(configPath) && !checkPluginConfigured(configPath)) {
|
|
@@ -163,7 +170,7 @@ async function runCli(start = startProxyServer, runAuthCheck = async () => {
|
|
|
163
170
|
const { enableDiskProfileDiscovery } = await import("./profiles-rdd84b45.js");
|
|
164
171
|
enableDiskProfileDiscovery();
|
|
165
172
|
}
|
|
166
|
-
const proxy = await start({ port, host, idleTimeoutSeconds, profiles, defaultProfile, version });
|
|
173
|
+
const proxy = await start({ port, host, idleTimeoutSeconds, profiles, defaultProfile, version, installProcessErrorHandlers: true });
|
|
167
174
|
proxy.server.on("error", (error) => {
|
|
168
175
|
if (error.code === "EADDRINUSE") {
|
|
169
176
|
process.exit(1);
|
|
@@ -1,24 +1,82 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveClaudeExecutableSync
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-yeazzt32.js";
|
|
4
4
|
import {
|
|
5
5
|
setSetting
|
|
6
6
|
} from "./cli-340h1chz.js";
|
|
7
|
+
import {
|
|
8
|
+
createPlatformCredentialStore
|
|
9
|
+
} from "./cli-1kbcm3yn.js";
|
|
7
10
|
import"./cli-p9swy5t3.js";
|
|
8
11
|
|
|
9
12
|
// src/proxy/profileCli.ts
|
|
10
|
-
import { mkdirSync, existsSync, rmSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
13
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
14
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
15
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
13
16
|
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
14
18
|
var PROFILES_DIR = join(homedir(), ".config", "meridian", "profiles");
|
|
15
19
|
var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
|
|
20
|
+
var OAUTH_AUTHORIZE_URL = "https://claude.com/cai/oauth/authorize";
|
|
21
|
+
var OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
22
|
+
var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
23
|
+
var OAUTH_REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
|
|
24
|
+
var OAUTH_SCOPES = [
|
|
25
|
+
"org:create_api_key",
|
|
26
|
+
"user:profile",
|
|
27
|
+
"user:inference",
|
|
28
|
+
"user:sessions:claude_code",
|
|
29
|
+
"user:mcp_servers",
|
|
30
|
+
"user:file_upload"
|
|
31
|
+
];
|
|
16
32
|
function ensureProfilesDir() {
|
|
17
33
|
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
18
34
|
}
|
|
19
35
|
function getProfileDir(id) {
|
|
20
36
|
return join(PROFILES_DIR, id);
|
|
21
37
|
}
|
|
38
|
+
function buildAuthLoginEnv(configDir, _options = {}, baseEnv = process.env) {
|
|
39
|
+
const env = { ...baseEnv };
|
|
40
|
+
if (configDir)
|
|
41
|
+
env.CLAUDE_CONFIG_DIR = configDir;
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
44
|
+
function base64Url(bytes) {
|
|
45
|
+
return bytes.toString("base64url");
|
|
46
|
+
}
|
|
47
|
+
function createManualOAuthSession() {
|
|
48
|
+
const codeVerifier = base64Url(randomBytes(32));
|
|
49
|
+
const state = base64Url(randomBytes(32));
|
|
50
|
+
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
51
|
+
const url = new URL(OAUTH_AUTHORIZE_URL);
|
|
52
|
+
url.searchParams.set("code", "true");
|
|
53
|
+
url.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
54
|
+
url.searchParams.set("response_type", "code");
|
|
55
|
+
url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
|
|
56
|
+
url.searchParams.set("scope", OAUTH_SCOPES.join(" "));
|
|
57
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
58
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
59
|
+
url.searchParams.set("state", state);
|
|
60
|
+
return { authorizeUrl: url.toString(), codeVerifier, state };
|
|
61
|
+
}
|
|
62
|
+
function parseAuthorizationCodeInput(input) {
|
|
63
|
+
const trimmed = input.trim();
|
|
64
|
+
if (!trimmed)
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(trimmed);
|
|
68
|
+
const code2 = url.searchParams.get("code") ?? new URLSearchParams(url.hash.replace(/^#/, "")).get("code");
|
|
69
|
+
const state2 = url.searchParams.get("state") ?? new URLSearchParams(url.hash.replace(/^#/, "")).get("state") ?? undefined;
|
|
70
|
+
return code2 ? { code: code2, state: state2 } : null;
|
|
71
|
+
} catch {}
|
|
72
|
+
const [codePart, hashState] = trimmed.split("#", 2);
|
|
73
|
+
if (!codePart)
|
|
74
|
+
return null;
|
|
75
|
+
const ampersandParams = codePart.includes("&") ? new URLSearchParams(codePart.slice(codePart.indexOf("&") + 1)) : null;
|
|
76
|
+
const code = codePart.split("&", 1)[0]?.trim();
|
|
77
|
+
const state = ampersandParams?.get("state") ?? hashState?.trim() ?? undefined;
|
|
78
|
+
return code ? { code, state } : null;
|
|
79
|
+
}
|
|
22
80
|
function loadProfileConfig() {
|
|
23
81
|
if (!existsSync(CONFIG_FILE))
|
|
24
82
|
return [];
|
|
@@ -31,7 +89,7 @@ function loadProfileConfig() {
|
|
|
31
89
|
}
|
|
32
90
|
function saveProfileConfig(profiles) {
|
|
33
91
|
ensureProfilesDir();
|
|
34
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(profiles, null, 2)
|
|
92
|
+
writeFileSync(CONFIG_FILE, `${JSON.stringify(profiles, null, 2)}
|
|
35
93
|
`, { mode: 384 });
|
|
36
94
|
}
|
|
37
95
|
function getAuthStatus(configDir) {
|
|
@@ -52,7 +110,72 @@ function getAuthStatus(configDir) {
|
|
|
52
110
|
return { loggedIn: false };
|
|
53
111
|
}
|
|
54
112
|
}
|
|
55
|
-
function
|
|
113
|
+
async function completeManualOAuthLogin(configDir) {
|
|
114
|
+
const session = createManualOAuthSession();
|
|
115
|
+
console.log("\x1B[33m⚠ Headless OAuth login: open this URL in a browser:\x1B[0m");
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(session.authorizeUrl);
|
|
118
|
+
console.log();
|
|
119
|
+
console.log("After sign-in, paste the code shown by Claude below.");
|
|
120
|
+
const input = promptLine("Paste code:");
|
|
121
|
+
const parsed = parseAuthorizationCodeInput(input);
|
|
122
|
+
if (!parsed) {
|
|
123
|
+
console.error("\x1B[31m✗ No authorization code received.\x1B[0m");
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (parsed.state && parsed.state !== session.state) {
|
|
127
|
+
console.error("\x1B[31m✗ OAuth state mismatch. Please retry the login.\x1B[0m");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetch(OAUTH_TOKEN_URL, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
grant_type: "authorization_code",
|
|
137
|
+
client_id: OAUTH_CLIENT_ID,
|
|
138
|
+
code: parsed.code,
|
|
139
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
140
|
+
code_verifier: session.codeVerifier,
|
|
141
|
+
state: parsed.state ?? session.state
|
|
142
|
+
}),
|
|
143
|
+
signal: AbortSignal.timeout(30000)
|
|
144
|
+
});
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`\x1B[31m✗ OAuth token exchange failed: ${err instanceof Error ? err.message : err}\x1B[0m`);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const body = await response.text().catch(() => "");
|
|
151
|
+
console.error(`\x1B[31m✗ OAuth token exchange failed (${response.status}).\x1B[0m`);
|
|
152
|
+
if (body)
|
|
153
|
+
console.error(` ${body.slice(0, 300)}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
let tokenData;
|
|
157
|
+
try {
|
|
158
|
+
tokenData = await response.json();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(`\x1B[31m✗ OAuth token response was invalid: ${err instanceof Error ? err.message : err}\x1B[0m`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (!tokenData.access_token || !tokenData.refresh_token) {
|
|
164
|
+
console.error("\x1B[31m✗ OAuth token response did not include the required tokens.\x1B[0m");
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const expiresAt = tokenData.expires_at ?? Date.now() + (tokenData.expires_in ?? 8 * 60 * 60) * 1000;
|
|
168
|
+
const store = createPlatformCredentialStore({ claudeConfigDir: configDir });
|
|
169
|
+
return store.write({
|
|
170
|
+
claudeAiOauth: {
|
|
171
|
+
accessToken: tokenData.access_token,
|
|
172
|
+
refreshToken: tokenData.refresh_token,
|
|
173
|
+
expiresAt,
|
|
174
|
+
scopes: tokenData.scope?.split(" ").filter(Boolean) ?? OAUTH_SCOPES
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function profileAdd(id, options = {}) {
|
|
56
179
|
if (!id || /[^a-zA-Z0-9_-]/.test(id)) {
|
|
57
180
|
console.error("\x1B[31m✗ Invalid profile ID.\x1B[0m Use only letters, numbers, hyphens, underscores.");
|
|
58
181
|
process.exit(1);
|
|
@@ -95,33 +218,43 @@ function profileAdd(id) {
|
|
|
95
218
|
printEnvHint(profiles);
|
|
96
219
|
return;
|
|
97
220
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const resolvedAuth = resolveClaudeExecutableSync();
|
|
109
|
-
if (!resolvedAuth) {
|
|
110
|
-
console.error("\x1B[31m✗ Could not find a Claude executable to run auth login.\x1B[0m");
|
|
111
|
-
console.error(" Install via: npm install -g @anthropic-ai/claude-code, or set MERIDIAN_CLAUDE_PATH=/path/to/claude");
|
|
112
|
-
process.exit(1);
|
|
221
|
+
if (!options.headless) {
|
|
222
|
+
console.log("\x1B[33m⚠ Important: Before logging in, make sure you're signed into the");
|
|
223
|
+
console.log(` correct Claude account in your browser (the one for "${id}").\x1B[0m`);
|
|
224
|
+
console.log();
|
|
225
|
+
console.log(" If you're currently signed into a different account:");
|
|
226
|
+
console.log(" 1. Go to https://claude.ai and sign out");
|
|
227
|
+
console.log(" 2. Sign in with the account you want for this profile");
|
|
228
|
+
console.log(" 3. Come back here — the login will open your browser");
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(" Press Ctrl+C to cancel, or wait for the browser to open...");
|
|
113
231
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
232
|
+
console.log();
|
|
233
|
+
if (options.headless) {
|
|
234
|
+
const success = await completeManualOAuthLogin(configDir);
|
|
235
|
+
if (!success) {
|
|
236
|
+
console.error("\x1B[31m✗ Login failed.\x1B[0m");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const resolvedAuth = resolveClaudeExecutableSync();
|
|
241
|
+
if (!resolvedAuth) {
|
|
242
|
+
console.error("\x1B[31m✗ Could not find a Claude executable to run auth login.\x1B[0m");
|
|
243
|
+
console.error(" Install via: npm install -g @anthropic-ai/claude-code, or set MERIDIAN_CLAUDE_PATH=/path/to/claude");
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const result = spawnSync(resolvedAuth.path, ["auth", "login"], {
|
|
247
|
+
env: buildAuthLoginEnv(configDir, options),
|
|
248
|
+
stdio: "inherit"
|
|
249
|
+
});
|
|
250
|
+
if (result.status !== 0) {
|
|
251
|
+
console.error("\x1B[31m✗ Login failed.\x1B[0m");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
121
254
|
}
|
|
122
255
|
const auth = getAuthStatus(configDir);
|
|
123
256
|
if (!auth.loggedIn) {
|
|
124
|
-
console.error(
|
|
257
|
+
console.error(`\x1B[31m✗ Login did not complete. Try again: meridian profile add ${id}\x1B[0m`);
|
|
125
258
|
process.exit(1);
|
|
126
259
|
}
|
|
127
260
|
console.log();
|
|
@@ -180,7 +313,7 @@ function profileList() {
|
|
|
180
313
|
}
|
|
181
314
|
function dirsToRemoveOnProfileRemove(profile, profilesDir) {
|
|
182
315
|
const dirs = [];
|
|
183
|
-
if (profile.claudeConfigDir
|
|
316
|
+
if (profile.claudeConfigDir?.startsWith(profilesDir)) {
|
|
184
317
|
dirs.push(profile.claudeConfigDir);
|
|
185
318
|
}
|
|
186
319
|
if (profile.oauthToken || profile.type === "oauth-token") {
|
|
@@ -198,6 +331,10 @@ function profileRemove(id) {
|
|
|
198
331
|
process.exit(1);
|
|
199
332
|
}
|
|
200
333
|
const removed = profiles[idx];
|
|
334
|
+
if (!removed) {
|
|
335
|
+
console.error(`\x1B[31m✗ Profile "${id}" not found.\x1B[0m`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
201
338
|
const dirsToRemove = dirsToRemoveOnProfileRemove(removed, PROFILES_DIR);
|
|
202
339
|
profiles.splice(idx, 1);
|
|
203
340
|
saveProfileConfig(profiles);
|
|
@@ -233,7 +370,7 @@ async function profileSwitch(id) {
|
|
|
233
370
|
process.exit(1);
|
|
234
371
|
}
|
|
235
372
|
}
|
|
236
|
-
function profileLogin(id) {
|
|
373
|
+
async function profileLogin(id, options = {}) {
|
|
237
374
|
const profiles = loadProfileConfig();
|
|
238
375
|
const profile = profiles.find((p) => p.id === id);
|
|
239
376
|
if (!profile) {
|
|
@@ -247,21 +384,31 @@ function profileLogin(id) {
|
|
|
247
384
|
}
|
|
248
385
|
console.log(`\x1B[36mRe-authenticating profile: ${id}\x1B[0m`);
|
|
249
386
|
console.log();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const resolvedLogin = resolveClaudeExecutableSync();
|
|
253
|
-
if (!resolvedLogin) {
|
|
254
|
-
console.error("\x1B[31m✗ Could not find a Claude executable to run auth login.\x1B[0m");
|
|
255
|
-
console.error(" Install via: npm install -g @anthropic-ai/claude-code, or set MERIDIAN_CLAUDE_PATH=/path/to/claude");
|
|
256
|
-
process.exit(1);
|
|
387
|
+
if (!options.headless) {
|
|
388
|
+
console.log("\x1B[33m⚠ Make sure you're signed into the correct Claude account in your browser.\x1B[0m");
|
|
257
389
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
390
|
+
console.log();
|
|
391
|
+
if (options.headless) {
|
|
392
|
+
const success = await completeManualOAuthLogin(profile.claudeConfigDir ?? getProfileDir(id));
|
|
393
|
+
if (!success) {
|
|
394
|
+
console.error("\x1B[31m✗ Login failed.\x1B[0m");
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
const resolvedLogin = resolveClaudeExecutableSync();
|
|
399
|
+
if (!resolvedLogin) {
|
|
400
|
+
console.error("\x1B[31m✗ Could not find a Claude executable to run auth login.\x1B[0m");
|
|
401
|
+
console.error(" Install via: npm install -g @anthropic-ai/claude-code, or set MERIDIAN_CLAUDE_PATH=/path/to/claude");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const result = spawnSync(resolvedLogin.path, ["auth", "login"], {
|
|
405
|
+
env: buildAuthLoginEnv(profile.claudeConfigDir, options),
|
|
406
|
+
stdio: "inherit"
|
|
407
|
+
});
|
|
408
|
+
if (result.status !== 0) {
|
|
409
|
+
console.error("\x1B[31m✗ Login failed.\x1B[0m");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
265
412
|
}
|
|
266
413
|
const auth = getAuthStatus(profile.claudeConfigDir ?? "");
|
|
267
414
|
if (auth.loggedIn) {
|
|
@@ -279,6 +426,16 @@ function promptYesNo(question) {
|
|
|
279
426
|
const answer = (result.stdout?.toString().trim() ?? "").toLowerCase();
|
|
280
427
|
return answer !== "n" && answer !== "no";
|
|
281
428
|
}
|
|
429
|
+
function promptLine(question) {
|
|
430
|
+
process.stderr.write(`${question} `);
|
|
431
|
+
const result = spawnSync("node", ["-e", [
|
|
432
|
+
`const rl = require("readline").createInterface({ input: process.stdin });`,
|
|
433
|
+
`rl.once("line", (a) => { process.stdout.write(a); rl.close(); });`,
|
|
434
|
+
`rl.once("close", () => process.exit(0));`
|
|
435
|
+
].join(`
|
|
436
|
+
`)], { stdio: ["inherit", "pipe", "inherit"] });
|
|
437
|
+
return result.stdout?.toString().trim() ?? "";
|
|
438
|
+
}
|
|
282
439
|
function promptToken(question) {
|
|
283
440
|
process.stderr.write(`${question}
|
|
284
441
|
> `);
|
|
@@ -322,20 +479,22 @@ function profileHelp() {
|
|
|
322
479
|
console.log(`meridian profile — manage Claude account profiles
|
|
323
480
|
|
|
324
481
|
Commands:
|
|
325
|
-
meridian profile add <name>
|
|
482
|
+
meridian profile add <name> [--headless] Add a profile via Claude OAuth login
|
|
326
483
|
meridian profile add <name> --oauth-token [TOKEN] Add a profile from a \`claude setup-token\` value
|
|
327
484
|
(if TOKEN is omitted, you will be prompted; input is hidden)
|
|
328
485
|
meridian profile list List profiles and auth status
|
|
329
486
|
meridian profile remove <name> Remove a profile
|
|
330
487
|
meridian profile switch <name> Switch the active profile (requires running proxy)
|
|
331
|
-
meridian profile login <name>
|
|
488
|
+
meridian profile login <name> [--headless] Re-authenticate an existing profile (claude-max only)
|
|
332
489
|
|
|
333
490
|
Examples:
|
|
334
491
|
meridian profile add personal # Add personal account (browser login)
|
|
335
492
|
meridian profile add work # Add work account
|
|
493
|
+
meridian profile add work --headless # Print OAuth URL, prompt for returned code, store credentials
|
|
336
494
|
meridian profile add ci --oauth-token # Add headless CI profile (prompted, no echo)
|
|
337
495
|
meridian profile add ci --oauth-token sk-ant-oat01-...
|
|
338
496
|
# Add headless CI profile (token from CLI argument)
|
|
497
|
+
meridian profile login work --headless # Re-authenticate via OAuth URL/code prompt
|
|
339
498
|
meridian profile switch work # Switch to work account
|
|
340
499
|
meridian profile list # Show all profiles`);
|
|
341
500
|
}
|
|
@@ -347,5 +506,8 @@ export {
|
|
|
347
506
|
profileHelp,
|
|
348
507
|
profileAddOauthToken,
|
|
349
508
|
profileAdd,
|
|
350
|
-
|
|
509
|
+
parseAuthorizationCodeInput,
|
|
510
|
+
dirsToRemoveOnProfileRemove,
|
|
511
|
+
createManualOAuthSession,
|
|
512
|
+
buildAuthLoginEnv
|
|
351
513
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA8C9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAgDtD"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible endpoint adapter.
|
|
3
|
+
*
|
|
4
|
+
* `/v1/chat/completions` serves generic OpenAI chat clients (Open WebUI,
|
|
5
|
+
* LibreChat, curl, any OpenAI-compatible tool). These are NOT coding agents:
|
|
6
|
+
* they bring their own system prompt and don't want the ~28KB claude_code
|
|
7
|
+
* preset injected on top of it (which would override their intent with the
|
|
8
|
+
* Claude Code persona — see the #526 investigation).
|
|
9
|
+
*
|
|
10
|
+
* The handler tags the internal hop with `x-meridian-agent: openai` so this
|
|
11
|
+
* adapter is selected deterministically instead of falling through to the
|
|
12
|
+
* default `opencode` adapter (whose preset defaults ON). Behaviour is
|
|
13
|
+
* otherwise identical to `opencode` — same tools, MCP server, passthrough,
|
|
14
|
+
* and transforms — the ONLY difference is the system-prompt preset default,
|
|
15
|
+
* which is set OFF in sdkFeatures.ADAPTER_DEFAULTS. This mirrors the
|
|
16
|
+
* `passthrough` adapter precedent (#190).
|
|
17
|
+
*
|
|
18
|
+
* NOTE: agent-specific. Keep this as a thin re-identification of the OpenCode
|
|
19
|
+
* adapter; do not fork behaviour here.
|
|
20
|
+
*/
|
|
21
|
+
import type { AgentAdapter } from "../adapter";
|
|
22
|
+
export declare const openAiAdapter: AgentAdapter;
|
|
23
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C,eAAO,MAAM,aAAa,EAAE,YAG3B,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning effort levels and normalization.
|
|
3
|
+
*
|
|
4
|
+
* The Claude Code SDK `--effort` flag accepts a fixed vocabulary. Clients send
|
|
5
|
+
* the effort under several keys (`effort`, `x-opencode-effort`, the standard
|
|
6
|
+
* OpenAI `reasoning_effort`, or an Anthropic-style `output_config.effort`), and
|
|
7
|
+
* not every value they send is valid for Claude — OpenAI notably offers
|
|
8
|
+
* `"minimal"`, which the SDK rejects. normalizeEffort gates the value so an
|
|
9
|
+
* unknown effort falls back to the model default (undefined) rather than
|
|
10
|
+
* erroring the whole request at the SDK boundary.
|
|
11
|
+
*
|
|
12
|
+
* Pure module — no I/O, no imports from server/session.
|
|
13
|
+
*/
|
|
14
|
+
export declare const VALID_EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
15
|
+
export type Effort = (typeof VALID_EFFORTS)[number];
|
|
16
|
+
/**
|
|
17
|
+
* Return the value if it is a valid Claude effort level, else undefined.
|
|
18
|
+
* Case-sensitive: the SDK expects lowercase.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeEffort(value: unknown): Effort | undefined;
|
|
21
|
+
//# sourceMappingURL=effort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effort.d.ts","sourceRoot":"","sources":["../../src/proxy/effort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,aAAa,oDAAqD,CAAA;AAE/E,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIlE"}
|
package/dist/proxy/models.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type ClaudeModel = "sonnet" | "sonnet[1m]" | "opus" | "opus[1m]" | "haiku
|
|
|
18
18
|
* override via MERIDIAN_DEFAULT_{TYPE}_MODEL (proxy-side) or
|
|
19
19
|
* ANTHROPIC_DEFAULT_{TYPE}_MODEL (shell env, wins over Meridian's pin).
|
|
20
20
|
*/
|
|
21
|
-
export declare const CANONICAL_OPUS_MODEL = "claude-opus-4-
|
|
21
|
+
export declare const CANONICAL_OPUS_MODEL = "claude-opus-4-8";
|
|
22
22
|
export declare const CANONICAL_SONNET_MODEL = "claude-sonnet-4-6";
|
|
23
23
|
export declare const CANONICAL_HAIKU_MODEL = "claude-haiku-4-5";
|
|
24
24
|
/**
|
package/dist/proxy/openai.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ export interface OpenAiChatRequest {
|
|
|
61
61
|
temperature?: number;
|
|
62
62
|
top_p?: number;
|
|
63
63
|
tools?: OpenAiChatTool[];
|
|
64
|
+
/** Standard OpenAI reasoning level (low/medium/high/…). */
|
|
65
|
+
reasoning_effort?: string;
|
|
66
|
+
/** Anthropic-style nesting some clients use. */
|
|
67
|
+
output_config?: {
|
|
68
|
+
effort?: string;
|
|
69
|
+
};
|
|
64
70
|
}
|
|
65
71
|
export interface AnthropicTextBlock {
|
|
66
72
|
type: "text";
|
|
@@ -93,6 +99,12 @@ export interface AnthropicRequestBody {
|
|
|
93
99
|
temperature?: number;
|
|
94
100
|
top_p?: number;
|
|
95
101
|
tools?: AnthropicTool[];
|
|
102
|
+
/** Reasoning effort carried from the OpenAI request so the internal
|
|
103
|
+
* /v1/messages hop forwards it to the SDK (value gated by normalizeEffort). */
|
|
104
|
+
reasoning_effort?: string;
|
|
105
|
+
output_config?: {
|
|
106
|
+
effort?: string;
|
|
107
|
+
};
|
|
96
108
|
}
|
|
97
109
|
export interface AnthropicUsage {
|
|
98
110
|
input_tokens?: number;
|