@rynfar/meridian 1.24.1 → 1.25.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.
@@ -0,0 +1,203 @@
1
+ // src/proxy/tokenRefresh.ts
2
+ import { execFile as execFileCb } from "child_process";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir, platform, userInfo } from "os";
5
+ import { promisify } from "util";
6
+
7
+ // src/logger.ts
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
+ var contextStore = new AsyncLocalStorage;
10
+ var shouldLog = () => process.env["OPENCODE_CLAUDE_PROVIDER_DEBUG"];
11
+ var shouldLogStreamDebug = () => process.env["OPENCODE_CLAUDE_PROVIDER_STREAM_DEBUG"];
12
+ var isVerboseStreamEvent = (event) => {
13
+ return event.startsWith("stream.") || event === "response.empty_stream";
14
+ };
15
+ var REDACTED_KEYS = new Set([
16
+ "authorization",
17
+ "cookie",
18
+ "x-api-key",
19
+ "apiKey",
20
+ "apikey",
21
+ "prompt",
22
+ "messages",
23
+ "content"
24
+ ]);
25
+ var sanitize = (value) => {
26
+ if (value === null || value === undefined)
27
+ return value;
28
+ if (typeof value === "string") {
29
+ if (value.length > 512) {
30
+ return `${value.slice(0, 512)}... [truncated=${value.length}]`;
31
+ }
32
+ return value;
33
+ }
34
+ if (Array.isArray(value)) {
35
+ return value.map(sanitize);
36
+ }
37
+ if (typeof value === "object") {
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(value)) {
40
+ if (REDACTED_KEYS.has(k)) {
41
+ if (typeof v === "string") {
42
+ out[k] = `[redacted len=${v.length}]`;
43
+ } else if (Array.isArray(v)) {
44
+ out[k] = `[redacted array len=${v.length}]`;
45
+ } else {
46
+ out[k] = "[redacted]";
47
+ }
48
+ } else {
49
+ out[k] = sanitize(v);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ return value;
55
+ };
56
+ var withClaudeLogContext = (context, fn) => {
57
+ return contextStore.run(context, fn);
58
+ };
59
+ var claudeLog = (event, extra) => {
60
+ if (!shouldLog())
61
+ return;
62
+ if (isVerboseStreamEvent(event) && !shouldLogStreamDebug())
63
+ return;
64
+ const context = contextStore.getStore() || {};
65
+ const payload = sanitize({ ts: new Date().toISOString(), event, ...context, ...extra || {} });
66
+ console.debug(`[opencode-claude-code-provider] ${JSON.stringify(payload)}`);
67
+ };
68
+
69
+ // src/proxy/tokenRefresh.ts
70
+ var execFile = promisify(execFileCb);
71
+ var OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
72
+ var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
73
+ var KEYCHAIN_SERVICE = "Claude Code-credentials";
74
+ var CREDENTIALS_FILE = `${homedir()}/.claude/.credentials.json`;
75
+ function parseKeychainValue(raw) {
76
+ const trimmed = raw.trim();
77
+ try {
78
+ return { credentials: JSON.parse(trimmed), wasHex: false };
79
+ } catch {}
80
+ try {
81
+ const decoded = Buffer.from(trimmed, "hex").toString("utf-8");
82
+ return { credentials: JSON.parse(decoded), wasHex: true };
83
+ } catch {}
84
+ return null;
85
+ }
86
+ var keychainWasHex = false;
87
+ var macosStore = {
88
+ async read() {
89
+ try {
90
+ const { stdout } = await execFile("/usr/bin/security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", userInfo().username, "-w"], { timeout: 5000 });
91
+ const parsed = parseKeychainValue(stdout);
92
+ if (!parsed)
93
+ throw new Error("Could not parse keychain value as JSON or hex-encoded JSON");
94
+ keychainWasHex = parsed.wasHex;
95
+ return parsed.credentials;
96
+ } catch (err) {
97
+ claudeLog("token_refresh.keychain_read_failed", { error: String(err) });
98
+ return null;
99
+ }
100
+ },
101
+ async write(credentials) {
102
+ const json = JSON.stringify(credentials, null, 2);
103
+ const value = keychainWasHex ? Buffer.from(json).toString("hex") : json;
104
+ try {
105
+ await execFile("/usr/bin/security", ["add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", userInfo().username, "-w", value], { timeout: 5000 });
106
+ return true;
107
+ } catch (err) {
108
+ claudeLog("token_refresh.keychain_write_failed", { error: String(err) });
109
+ return false;
110
+ }
111
+ }
112
+ };
113
+ var fileStore = {
114
+ async read() {
115
+ try {
116
+ if (!existsSync(CREDENTIALS_FILE))
117
+ return null;
118
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
119
+ } catch (err) {
120
+ claudeLog("token_refresh.file_read_failed", { error: String(err) });
121
+ return null;
122
+ }
123
+ },
124
+ async write(credentials) {
125
+ try {
126
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), "utf-8");
127
+ return true;
128
+ } catch (err) {
129
+ claudeLog("token_refresh.file_write_failed", { error: String(err) });
130
+ return false;
131
+ }
132
+ }
133
+ };
134
+ function createPlatformCredentialStore() {
135
+ return platform() === "darwin" ? macosStore : fileStore;
136
+ }
137
+ var inflightRefresh = null;
138
+ async function refreshOAuthToken(store) {
139
+ if (inflightRefresh)
140
+ return inflightRefresh;
141
+ inflightRefresh = doRefresh(store ?? createPlatformCredentialStore()).finally(() => {
142
+ inflightRefresh = null;
143
+ });
144
+ return inflightRefresh;
145
+ }
146
+ async function doRefresh(store) {
147
+ const credentials = await store.read();
148
+ if (!credentials) {
149
+ claudeLog("token_refresh.no_credentials", {});
150
+ return false;
151
+ }
152
+ const { refreshToken } = credentials.claudeAiOauth;
153
+ if (!refreshToken) {
154
+ claudeLog("token_refresh.no_refresh_token", {});
155
+ return false;
156
+ }
157
+ let response;
158
+ try {
159
+ response = await fetch(OAUTH_TOKEN_URL, {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({
163
+ grant_type: "refresh_token",
164
+ client_id: OAUTH_CLIENT_ID,
165
+ refresh_token: refreshToken
166
+ }),
167
+ signal: AbortSignal.timeout(15000)
168
+ });
169
+ } catch (err) {
170
+ claudeLog("token_refresh.request_failed", { error: String(err) });
171
+ return false;
172
+ }
173
+ if (!response.ok) {
174
+ const body = await response.text().catch(() => "");
175
+ claudeLog("token_refresh.bad_response", { status: response.status, body });
176
+ return false;
177
+ }
178
+ let tokenData;
179
+ try {
180
+ tokenData = await response.json();
181
+ } catch (err) {
182
+ claudeLog("token_refresh.parse_failed", { error: String(err) });
183
+ return false;
184
+ }
185
+ const now = Date.now();
186
+ const expiresAt = tokenData.expires_at ?? (tokenData.expires_in ? now + tokenData.expires_in * 1000 : now + 8 * 60 * 60 * 1000);
187
+ credentials.claudeAiOauth = {
188
+ ...credentials.claudeAiOauth,
189
+ accessToken: tokenData.access_token,
190
+ refreshToken: tokenData.refresh_token ?? refreshToken,
191
+ expiresAt
192
+ };
193
+ const written = await store.write(credentials);
194
+ if (!written)
195
+ return false;
196
+ claudeLog("token_refresh.success", { expiresAt });
197
+ return true;
198
+ }
199
+ function resetInflightRefresh() {
200
+ inflightRefresh = null;
201
+ }
202
+
203
+ export { withClaudeLogContext, claudeLog, createPlatformCredentialStore, refreshOAuthToken, resetInflightRefresh };
@@ -0,0 +1,67 @@
1
+ // src/proxy/setup.ts
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir, platform } from "os";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ function findOpencodeConfigPath() {
7
+ if (process.env.OPENCODE_CONFIG_DIR) {
8
+ return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json");
9
+ }
10
+ if (process.env.XDG_CONFIG_HOME) {
11
+ return join(process.env.XDG_CONFIG_HOME, "opencode", "opencode.json");
12
+ }
13
+ if (platform() === "win32" && process.env.APPDATA) {
14
+ return join(process.env.APPDATA, "opencode", "opencode.json");
15
+ }
16
+ return join(homedir(), ".config", "opencode", "opencode.json");
17
+ }
18
+ function findPluginPath(fromUrl) {
19
+ const dir = dirname(fileURLToPath(fromUrl));
20
+ return join(dir, "..", "plugin", "meridian.ts");
21
+ }
22
+ var STALE_PATTERNS = [
23
+ "opencode-claude-max-proxy",
24
+ "claude-max-headers",
25
+ "meridian-agent-mode"
26
+ ];
27
+ function isMeridianEntry(entry) {
28
+ return STALE_PATTERNS.some((p) => entry.includes(p)) || entry.includes("meridian.ts") || entry.includes("@rynfar/meridian");
29
+ }
30
+ function checkPluginConfigured(configPath) {
31
+ const path = configPath ?? findOpencodeConfigPath();
32
+ if (!existsSync(path))
33
+ return false;
34
+ try {
35
+ const raw = readFileSync(path, "utf-8");
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 {
40
+ return false;
41
+ }
42
+ }
43
+ function runSetup(pluginPath, configPath) {
44
+ const path = configPath ?? findOpencodeConfigPath();
45
+ const dir = dirname(path);
46
+ let config = {};
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;
54
+ if (!existsSync(dir))
55
+ mkdirSync(dir, { recursive: true });
56
+ }
57
+ const existing = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
58
+ const removedStale = existing.filter(isMeridianEntry);
59
+ const others = existing.filter((p) => !isMeridianEntry(p));
60
+ const alreadyConfigured = removedStale.some((p) => p === pluginPath);
61
+ config.plugin = [...others, pluginPath];
62
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
63
+ `, "utf-8");
64
+ return { configPath: path, pluginPath, alreadyConfigured, removedStale, created };
65
+ }
66
+
67
+ export { findOpencodeConfigPath, findPluginPath, checkPluginConfigured, runSetup };