codex-claude-proxy 1.0.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/LICENSE +21 -0
- package/README.md +206 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +274 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +201 -0
- package/docs/OPENCLAW.md +338 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +44 -0
- package/public/css/style.css +791 -0
- package/public/index.html +783 -0
- package/public/js/app.js +511 -0
- package/src/account-manager.js +483 -0
- package/src/claude-config.js +143 -0
- package/src/cli/accounts.js +413 -0
- package/src/cli/index.js +66 -0
- package/src/direct-api.js +123 -0
- package/src/format-converter.js +331 -0
- package/src/index.js +41 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +270 -0
- package/src/kilo-streamer.js +198 -0
- package/src/model-api.js +189 -0
- package/src/oauth.js +554 -0
- package/src/response-streamer.js +329 -0
- package/src/routes/api-routes.js +1035 -0
- package/src/server-settings.js +48 -0
- package/src/server.js +30 -0
- package/src/utils/logger.js +156 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Manager
|
|
3
|
+
* Manages multiple ChatGPT accounts with manual switching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { refreshAccessToken, extractAccountInfo } from './oauth.js';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.codex-claude-proxy');
|
|
12
|
+
const ACCOUNTS_FILE = join(CONFIG_DIR, 'accounts.json');
|
|
13
|
+
const ACCOUNTS_DIR = join(CONFIG_DIR, 'accounts');
|
|
14
|
+
|
|
15
|
+
const TOKEN_REFRESH_INTERVAL_MS = 55 * 60 * 1000;
|
|
16
|
+
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_ACCOUNTS = {
|
|
19
|
+
accounts: [],
|
|
20
|
+
activeAccount: null,
|
|
21
|
+
version: 1
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let autoRefreshIntervalId = null;
|
|
25
|
+
const tokenCache = new Map();
|
|
26
|
+
|
|
27
|
+
function ensureConfigDir() {
|
|
28
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
29
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(ACCOUNTS_DIR)) {
|
|
32
|
+
mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sanitizeEmailForPath(email) {
|
|
37
|
+
return email.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getAccountDir(email) {
|
|
41
|
+
const safeEmail = sanitizeEmailForPath(email);
|
|
42
|
+
return join(ACCOUNTS_DIR, safeEmail);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAccountAuthFile(email) {
|
|
46
|
+
return join(getAccountDir(email), 'auth.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadAccounts() {
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
|
|
52
|
+
if (!existsSync(ACCOUNTS_FILE)) {
|
|
53
|
+
return { ...DEFAULT_ACCOUNTS };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(readFileSync(ACCOUNTS_FILE, 'utf8'));
|
|
58
|
+
return { ...DEFAULT_ACCOUNTS, ...data };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('[AccountManager] Error loading accounts:', e.message);
|
|
61
|
+
return { ...DEFAULT_ACCOUNTS };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function saveAccounts(data) {
|
|
66
|
+
ensureConfigDir();
|
|
67
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(data, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getActiveAccount() {
|
|
71
|
+
const data = loadAccounts();
|
|
72
|
+
if (!data.activeAccount) return null;
|
|
73
|
+
return data.accounts.find(a => a.email === data.activeAccount) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function updateAccountAuth(account) {
|
|
77
|
+
if (!account) return;
|
|
78
|
+
|
|
79
|
+
const accountDir = getAccountDir(account.email);
|
|
80
|
+
const authFile = getAccountAuthFile(account.email);
|
|
81
|
+
|
|
82
|
+
if (!existsSync(accountDir)) {
|
|
83
|
+
mkdirSync(accountDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const authData = {
|
|
87
|
+
auth_mode: 'chatgpt',
|
|
88
|
+
OPENAI_API_KEY: null,
|
|
89
|
+
tokens: {
|
|
90
|
+
id_token: account.idToken,
|
|
91
|
+
access_token: account.accessToken,
|
|
92
|
+
refresh_token: account.refreshToken,
|
|
93
|
+
account_id: account.accountId
|
|
94
|
+
},
|
|
95
|
+
last_refresh: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
writeFileSync(authFile, JSON.stringify(authData, null, 2));
|
|
100
|
+
console.log(`[AccountManager] Updated auth for: ${account.email}`);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('[AccountManager] Failed to update auth:', e.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function setActiveAccount(email) {
|
|
107
|
+
const data = loadAccounts();
|
|
108
|
+
const account = data.accounts.find(a => a.email === email);
|
|
109
|
+
|
|
110
|
+
if (!account) {
|
|
111
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
data.activeAccount = email;
|
|
115
|
+
saveAccounts(data);
|
|
116
|
+
|
|
117
|
+
updateAccountAuth(account);
|
|
118
|
+
|
|
119
|
+
return { success: true, message: `Switched to account: ${email}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function removeAccount(email) {
|
|
123
|
+
const data = loadAccounts();
|
|
124
|
+
const index = data.accounts.findIndex(a => a.email === email);
|
|
125
|
+
|
|
126
|
+
if (index < 0) {
|
|
127
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const accountDir = getAccountDir(email);
|
|
131
|
+
try {
|
|
132
|
+
if (existsSync(accountDir)) {
|
|
133
|
+
rmSync(accountDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error('[AccountManager] Failed to remove account directory:', e.message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
data.accounts.splice(index, 1);
|
|
140
|
+
|
|
141
|
+
if (data.activeAccount === email) {
|
|
142
|
+
data.activeAccount = data.accounts[0]?.email || null;
|
|
143
|
+
|
|
144
|
+
if (data.activeAccount) {
|
|
145
|
+
const newActive = data.accounts.find(a => a.email === data.activeAccount);
|
|
146
|
+
updateAccountAuth(newActive);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
saveAccounts(data);
|
|
151
|
+
|
|
152
|
+
return { success: true, message: `Account removed: ${email}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listAccounts() {
|
|
156
|
+
const data = loadAccounts();
|
|
157
|
+
|
|
158
|
+
const accounts = data.accounts.map(account => {
|
|
159
|
+
const info = extractAccountInfo(account.accessToken);
|
|
160
|
+
return {
|
|
161
|
+
email: account.email,
|
|
162
|
+
accountId: account.accountId,
|
|
163
|
+
planType: info?.planType || account.planType || 'unknown',
|
|
164
|
+
addedAt: account.addedAt,
|
|
165
|
+
lastUsed: account.lastUsed,
|
|
166
|
+
isActive: account.email === data.activeAccount,
|
|
167
|
+
tokenExpired: info?.expiresAt ? info.expiresAt < Date.now() : false,
|
|
168
|
+
quota: account.quota || null
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
accounts,
|
|
174
|
+
activeAccount: data.activeAccount,
|
|
175
|
+
total: accounts.length
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateAccountQuota(email, quotaData) {
|
|
180
|
+
const data = loadAccounts();
|
|
181
|
+
const account = data.accounts.find(a => a.email === email);
|
|
182
|
+
|
|
183
|
+
if (!account) {
|
|
184
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
account.quota = {
|
|
188
|
+
...quotaData,
|
|
189
|
+
lastChecked: new Date().toISOString()
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
saveAccounts(data);
|
|
193
|
+
return { success: true, message: `Quota updated for: ${email}` };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getAccountQuota(email) {
|
|
197
|
+
const data = loadAccounts();
|
|
198
|
+
const account = data.accounts.find(a => a.email === email);
|
|
199
|
+
|
|
200
|
+
if (!account) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return account.quota || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isTokenExpiredOrExpiringSoon(account) {
|
|
208
|
+
if (!account.expiresAt) return true;
|
|
209
|
+
return Date.now() >= (account.expiresAt - TOKEN_EXPIRY_BUFFER_MS);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function refreshAccountToken(email) {
|
|
213
|
+
const data = loadAccounts();
|
|
214
|
+
const account = data.accounts.find(a => a.email === email);
|
|
215
|
+
|
|
216
|
+
if (!account) {
|
|
217
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!account.refreshToken) {
|
|
221
|
+
return { success: false, message: 'No refresh token available' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
226
|
+
const accountInfo = extractAccountInfo(tokens.accessToken);
|
|
227
|
+
|
|
228
|
+
const index = data.accounts.findIndex(a => a.email === email);
|
|
229
|
+
if (index >= 0) {
|
|
230
|
+
data.accounts[index].accessToken = tokens.accessToken;
|
|
231
|
+
data.accounts[index].refreshToken = tokens.refreshToken || data.accounts[index].refreshToken;
|
|
232
|
+
data.accounts[index].idToken = tokens.idToken || data.accounts[index].idToken;
|
|
233
|
+
data.accounts[index].expiresAt = accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000);
|
|
234
|
+
if (accountInfo?.planType) {
|
|
235
|
+
data.accounts[index].planType = accountInfo.planType;
|
|
236
|
+
}
|
|
237
|
+
saveAccounts(data);
|
|
238
|
+
|
|
239
|
+
tokenCache.set(email, {
|
|
240
|
+
token: tokens.accessToken,
|
|
241
|
+
extractedAt: Date.now()
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (data.activeAccount === email) {
|
|
245
|
+
updateAccountAuth(data.accounts[index]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`[AccountManager] Token refreshed for: ${email}`);
|
|
250
|
+
return { success: true, message: `Token refreshed for: ${email}` };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(`[AccountManager] Token refresh failed for ${email}:`, error.message);
|
|
253
|
+
return { success: false, message: `Token refresh failed: ${error.message}` };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function refreshAllAccounts() {
|
|
258
|
+
const data = loadAccounts();
|
|
259
|
+
const results = [];
|
|
260
|
+
|
|
261
|
+
for (const account of data.accounts) {
|
|
262
|
+
if (account.refreshToken) {
|
|
263
|
+
const result = await refreshAccountToken(account.email);
|
|
264
|
+
results.push({ email: account.email, ...result });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function startAutoRefresh() {
|
|
272
|
+
if (autoRefreshIntervalId) {
|
|
273
|
+
clearInterval(autoRefreshIntervalId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
setTimeout(async () => {
|
|
277
|
+
console.log('[AccountManager] Startup: refreshing all account tokens...');
|
|
278
|
+
const data = loadAccounts();
|
|
279
|
+
for (const account of data.accounts) {
|
|
280
|
+
if (account.refreshToken) {
|
|
281
|
+
console.log(`[AccountManager] Startup refresh for ${account.email}`);
|
|
282
|
+
await refreshAccountToken(account.email);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}, 2000);
|
|
286
|
+
|
|
287
|
+
autoRefreshIntervalId = setInterval(async () => {
|
|
288
|
+
const data = loadAccounts();
|
|
289
|
+
|
|
290
|
+
for (const account of data.accounts) {
|
|
291
|
+
if (account.refreshToken) {
|
|
292
|
+
console.log(`[AccountManager] Periodic refresh for ${account.email}`);
|
|
293
|
+
await refreshAccountToken(account.email);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}, TOKEN_REFRESH_INTERVAL_MS);
|
|
297
|
+
|
|
298
|
+
console.log('[AccountManager] Auto-refresh started (every 55 minutes)');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function stopAutoRefresh() {
|
|
302
|
+
if (autoRefreshIntervalId) {
|
|
303
|
+
clearInterval(autoRefreshIntervalId);
|
|
304
|
+
autoRefreshIntervalId = null;
|
|
305
|
+
console.log('[AccountManager] Auto-refresh stopped');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getCachedToken(email) {
|
|
310
|
+
const cached = tokenCache.get(email);
|
|
311
|
+
if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
|
|
312
|
+
return cached.token;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function setCachedToken(email, token) {
|
|
318
|
+
tokenCache.set(email, { token, extractedAt: Date.now() });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function refreshActiveAccount() {
|
|
322
|
+
const account = getActiveAccount();
|
|
323
|
+
if (!account) {
|
|
324
|
+
return { success: false, message: 'No active account' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!account.refreshToken) {
|
|
328
|
+
return { success: false, message: 'No refresh token available' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
333
|
+
const accountInfo = extractAccountInfo(tokens.accessToken);
|
|
334
|
+
|
|
335
|
+
const data = loadAccounts();
|
|
336
|
+
const index = data.accounts.findIndex(a => a.email === account.email);
|
|
337
|
+
|
|
338
|
+
if (index >= 0) {
|
|
339
|
+
data.accounts[index].accessToken = tokens.accessToken;
|
|
340
|
+
data.accounts[index].refreshToken = tokens.refreshToken || data.accounts[index].refreshToken;
|
|
341
|
+
data.accounts[index].idToken = tokens.idToken || data.accounts[index].idToken;
|
|
342
|
+
data.accounts[index].expiresAt = accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000);
|
|
343
|
+
if (accountInfo?.planType) {
|
|
344
|
+
data.accounts[index].planType = accountInfo.planType;
|
|
345
|
+
}
|
|
346
|
+
saveAccounts(data);
|
|
347
|
+
|
|
348
|
+
updateAccountAuth(data.accounts[index]);
|
|
349
|
+
console.log(`[AccountManager] Active account token refreshed: ${account.email}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { success: true, message: `Token refreshed for: ${account.email}` };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`[AccountManager] Token refresh failed for ${account.email}:`, error.message);
|
|
355
|
+
return { success: false, message: `Token refresh failed: ${error.message}` };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function importFromCodex() {
|
|
360
|
+
const codeAuthFile = join(homedir(), '.codex', 'auth.json');
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
if (!existsSync(codeAuthFile)) {
|
|
364
|
+
return { success: false, message: 'No Codex auth.json found' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const codexAuth = JSON.parse(readFileSync(codeAuthFile, 'utf8'));
|
|
368
|
+
|
|
369
|
+
if (!codexAuth.tokens?.access_token) {
|
|
370
|
+
return { success: false, message: 'No valid tokens in Codex auth.json' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const info = extractAccountInfo(codexAuth.tokens.access_token);
|
|
374
|
+
|
|
375
|
+
const newAccount = {
|
|
376
|
+
email: info?.email || 'imported@unknown.com',
|
|
377
|
+
accountId: codexAuth.tokens.account_id,
|
|
378
|
+
planType: info?.planType || 'unknown',
|
|
379
|
+
accessToken: codexAuth.tokens.access_token,
|
|
380
|
+
refreshToken: codexAuth.tokens.refresh_token,
|
|
381
|
+
idToken: codexAuth.tokens.id_token,
|
|
382
|
+
expiresAt: info?.expiresAt,
|
|
383
|
+
addedAt: new Date().toISOString(),
|
|
384
|
+
lastUsed: new Date().toISOString(),
|
|
385
|
+
source: 'imported'
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const data = loadAccounts();
|
|
389
|
+
|
|
390
|
+
const existingIndex = data.accounts.findIndex(a => a.email === newAccount.email);
|
|
391
|
+
if (existingIndex >= 0) {
|
|
392
|
+
data.accounts[existingIndex] = newAccount;
|
|
393
|
+
} else {
|
|
394
|
+
data.accounts.push(newAccount);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!data.activeAccount) {
|
|
398
|
+
data.activeAccount = newAccount.email;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
saveAccounts(data);
|
|
402
|
+
updateAccountAuth(newAccount);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
success: true,
|
|
406
|
+
message: `Imported account: ${newAccount.email} (${newAccount.planType})`
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return { success: false, message: `Import failed: ${error.message}` };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getStatus() {
|
|
414
|
+
const data = loadAccounts();
|
|
415
|
+
const accounts = data.accounts.map(a => ({
|
|
416
|
+
email: a.email,
|
|
417
|
+
planType: a.planType,
|
|
418
|
+
isActive: a.email === data.activeAccount
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
total: data.accounts.length,
|
|
423
|
+
active: data.activeAccount,
|
|
424
|
+
accounts
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function ensureAccountsPersist() {
|
|
429
|
+
const data = loadAccounts();
|
|
430
|
+
if (data.accounts.length > 0 && data.activeAccount) {
|
|
431
|
+
const active = data.accounts.find(a => a.email === data.activeAccount);
|
|
432
|
+
if (active) {
|
|
433
|
+
updateAccountAuth(active);
|
|
434
|
+
console.log(`[AccountManager] Restored active account: ${active.email}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export {
|
|
440
|
+
loadAccounts,
|
|
441
|
+
saveAccounts,
|
|
442
|
+
getActiveAccount,
|
|
443
|
+
setActiveAccount,
|
|
444
|
+
removeAccount,
|
|
445
|
+
listAccounts,
|
|
446
|
+
refreshActiveAccount,
|
|
447
|
+
refreshAccountToken,
|
|
448
|
+
refreshAllAccounts,
|
|
449
|
+
importFromCodex,
|
|
450
|
+
getStatus,
|
|
451
|
+
updateAccountAuth,
|
|
452
|
+
ensureAccountsPersist,
|
|
453
|
+
updateAccountQuota,
|
|
454
|
+
getAccountQuota,
|
|
455
|
+
startAutoRefresh,
|
|
456
|
+
stopAutoRefresh,
|
|
457
|
+
isTokenExpiredOrExpiringSoon,
|
|
458
|
+
getCachedToken,
|
|
459
|
+
setCachedToken,
|
|
460
|
+
TOKEN_REFRESH_INTERVAL_MS,
|
|
461
|
+
ACCOUNTS_FILE,
|
|
462
|
+
CONFIG_DIR
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export default {
|
|
466
|
+
getActiveAccount,
|
|
467
|
+
setActiveAccount,
|
|
468
|
+
removeAccount,
|
|
469
|
+
listAccounts,
|
|
470
|
+
refreshActiveAccount,
|
|
471
|
+
refreshAccountToken,
|
|
472
|
+
refreshAllAccounts,
|
|
473
|
+
importFromCodex,
|
|
474
|
+
getStatus,
|
|
475
|
+
ensureAccountsPersist,
|
|
476
|
+
updateAccountQuota,
|
|
477
|
+
getAccountQuota,
|
|
478
|
+
startAutoRefresh,
|
|
479
|
+
stopAutoRefresh,
|
|
480
|
+
isTokenExpiredOrExpiringSoon,
|
|
481
|
+
getCachedToken,
|
|
482
|
+
setCachedToken
|
|
483
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI Configuration Utility
|
|
3
|
+
* Handles reading and writing to the global Claude CLI settings file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import fsSync from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
export function getClaudeConfigPath() {
|
|
12
|
+
const configDir = process.env.CLAUDE_CONFIG_PATH;
|
|
13
|
+
if (configDir) {
|
|
14
|
+
return path.join(configDir, 'settings.json');
|
|
15
|
+
}
|
|
16
|
+
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readClaudeConfigSync() {
|
|
20
|
+
const configPath = getClaudeConfigPath();
|
|
21
|
+
try {
|
|
22
|
+
if (!fsSync.existsSync(configPath)) {
|
|
23
|
+
return { env: {} };
|
|
24
|
+
}
|
|
25
|
+
const content = fsSync.readFileSync(configPath, 'utf8');
|
|
26
|
+
if (!content.trim()) return { env: {} };
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('[ClaudeConfig] Error reading config:', error.message);
|
|
30
|
+
return { env: {} };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readClaudeConfig() {
|
|
35
|
+
const configPath = getClaudeConfigPath();
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
38
|
+
if (!content.trim()) return { env: {} };
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === 'ENOENT') {
|
|
42
|
+
return { env: {} };
|
|
43
|
+
}
|
|
44
|
+
console.error('[ClaudeConfig] Error reading config:', error.message);
|
|
45
|
+
return { env: {} };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function updateClaudeConfig(updates) {
|
|
50
|
+
const configPath = getClaudeConfigPath();
|
|
51
|
+
let currentConfig = {};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
currentConfig = await readClaudeConfig();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Ignore
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const newConfig = deepMerge(currentConfig, updates);
|
|
60
|
+
|
|
61
|
+
const configDir = path.dirname(configPath);
|
|
62
|
+
try {
|
|
63
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Ignore if exists
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
|
70
|
+
console.log(`[ClaudeConfig] Updated config at ${configPath}`);
|
|
71
|
+
return newConfig;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[ClaudeConfig] Failed to write config:', error.message);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function setProxyMode(proxyUrl, models = {}) {
|
|
79
|
+
const updates = {
|
|
80
|
+
env: {
|
|
81
|
+
ANTHROPIC_BASE_URL: proxyUrl,
|
|
82
|
+
ANTHROPIC_API_KEY: 'any-key',
|
|
83
|
+
ANTHROPIC_MODEL: models.default || 'claude-sonnet-4-5',
|
|
84
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus || 'claude-opus-4-5',
|
|
85
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet || 'claude-sonnet-4-5',
|
|
86
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku || 'claude-haiku-4'
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return await updateClaudeConfig(updates);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function setDirectMode(apiKey) {
|
|
94
|
+
const updates = {
|
|
95
|
+
env: {
|
|
96
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
97
|
+
ANTHROPIC_BASE_URL: undefined,
|
|
98
|
+
ANTHROPIC_MODEL: undefined
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Remove undefined values
|
|
103
|
+
Object.keys(updates.env).forEach(key => {
|
|
104
|
+
if (updates.env[key] === undefined) {
|
|
105
|
+
delete updates.env[key];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return await updateClaudeConfig(updates);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deepMerge(target, source) {
|
|
113
|
+
const output = { ...target };
|
|
114
|
+
|
|
115
|
+
if (isObject(target) && isObject(source)) {
|
|
116
|
+
Object.keys(source).forEach(key => {
|
|
117
|
+
if (isObject(source[key])) {
|
|
118
|
+
if (!(key in target)) {
|
|
119
|
+
Object.assign(output, { [key]: source[key] });
|
|
120
|
+
} else {
|
|
121
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
Object.assign(output, { [key]: source[key] });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isObject(item) {
|
|
133
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
getClaudeConfigPath,
|
|
138
|
+
readClaudeConfig,
|
|
139
|
+
readClaudeConfigSync,
|
|
140
|
+
updateClaudeConfig,
|
|
141
|
+
setProxyMode,
|
|
142
|
+
setDirectMode
|
|
143
|
+
};
|