coding-tool-x 3.3.7 → 3.3.8

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/toggle-proxy.js +100 -5
  17. package/src/config/paths.js +102 -19
  18. package/src/server/api/channels.js +9 -0
  19. package/src/server/api/codex-channels.js +9 -0
  20. package/src/server/api/codex-proxy.js +22 -11
  21. package/src/server/api/gemini-proxy.js +22 -11
  22. package/src/server/api/oauth-credentials.js +163 -0
  23. package/src/server/api/opencode-proxy.js +22 -10
  24. package/src/server/api/plugins.js +3 -1
  25. package/src/server/api/proxy.js +39 -44
  26. package/src/server/api/skills.js +91 -13
  27. package/src/server/codex-proxy-server.js +1 -11
  28. package/src/server/index.js +1 -0
  29. package/src/server/services/channels.js +18 -22
  30. package/src/server/services/codex-channels.js +124 -175
  31. package/src/server/services/codex-config.js +2 -5
  32. package/src/server/services/codex-settings-manager.js +12 -348
  33. package/src/server/services/config-export-service.js +23 -2
  34. package/src/server/services/gemini-channels.js +11 -9
  35. package/src/server/services/mcp-service.js +33 -16
  36. package/src/server/services/native-keychain.js +243 -0
  37. package/src/server/services/native-oauth-adapters.js +890 -0
  38. package/src/server/services/oauth-credentials-service.js +786 -0
  39. package/src/server/services/oauth-utils.js +49 -0
  40. package/src/server/services/opencode-channels.js +13 -9
  41. package/src/server/services/opencode-settings-manager.js +169 -16
  42. package/src/server/services/plugins-service.js +22 -1
  43. package/src/server/services/settings-manager.js +13 -0
  44. package/src/server/services/skill-service.js +712 -332
  45. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  46. package/dist/web/assets/Home-obifg_9E.js +0 -1
  47. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  48. package/dist/web/assets/index-eEmjZKWP.css +0 -1
@@ -0,0 +1,890 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const toml = require('toml');
6
+ const tomlStringify = require('@iarna/toml').stringify;
7
+ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
8
+ const claudeSettingsManager = require('./settings-manager');
9
+ const codexSettingsManager = require('./codex-settings-manager');
10
+ const geminiSettingsManager = require('./gemini-settings-manager');
11
+ const opencodeSettingsManager = require('./opencode-settings-manager');
12
+ const nativeKeychain = require('./native-keychain');
13
+ const { maskToken, decodeJwtPayload, removeFileIfExists, sha256 } = require('./oauth-utils');
14
+
15
+ const SUPPORTED_TOOLS = ['claude', 'codex', 'gemini', 'opencode'];
16
+ const GEMINI_MAIN_ACCOUNT_KEY = 'main-account';
17
+ const GEMINI_KEYCHAIN_SERVICE = 'gemini-cli-oauth';
18
+ const CODEX_KEYCHAIN_SERVICE = 'Codex Auth';
19
+
20
+ function ensureDir(dirPath) {
21
+ if (!dirPath) return;
22
+ fs.mkdirSync(dirPath, { recursive: true });
23
+ }
24
+
25
+ function ensureFileMode(filePath, mode = 0o600) {
26
+ if (process.platform === 'win32') {
27
+ return;
28
+ }
29
+ try {
30
+ fs.chmodSync(filePath, mode);
31
+ } catch {
32
+ // ignore chmod failures
33
+ }
34
+ }
35
+
36
+ function writeJsonFile(filePath, value) {
37
+ ensureDir(path.dirname(filePath));
38
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
39
+ ensureFileMode(filePath);
40
+ }
41
+
42
+ function readJsonFile(filePath, fallback = null) {
43
+ try {
44
+ if (!fs.existsSync(filePath)) {
45
+ return fallback;
46
+ }
47
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
48
+ } catch {
49
+ return fallback;
50
+ }
51
+ }
52
+
53
+ function fingerprintFor(tool, value) {
54
+ return sha256(`${tool}:${value || ''}`);
55
+ }
56
+
57
+ function loadChannelsFile(tool) {
58
+ const filePath = PATHS.channels?.[tool];
59
+ const payload = readJsonFile(filePath, { channels: [] });
60
+ return Array.isArray(payload?.channels) ? payload.channels : [];
61
+ }
62
+
63
+ function buildNativeSummary(data = {}) {
64
+ return {
65
+ providerId: data.providerId || '',
66
+ accountId: data.accountId || '',
67
+ accountEmail: data.accountEmail || '',
68
+ expiresAt: data.expiresAt || null,
69
+ storage: data.storage || '',
70
+ tokenPreview: maskToken(data.primaryToken || data.accessToken || ''),
71
+ lastRefresh: data.lastRefresh || null
72
+ };
73
+ }
74
+
75
+ function parseClaudeOAuthPayload(raw) {
76
+ const parsed = raw?.claudeAiOauth && typeof raw.claudeAiOauth === 'object'
77
+ ? raw.claudeAiOauth
78
+ : raw;
79
+
80
+ if (!parsed || typeof parsed !== 'object') {
81
+ return null;
82
+ }
83
+
84
+ const accessToken = String(
85
+ parsed.accessToken
86
+ || parsed.access_token
87
+ || parsed.authToken
88
+ || parsed.token
89
+ || ''
90
+ ).trim();
91
+
92
+ if (!accessToken) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ accessToken,
98
+ refreshToken: String(parsed.refreshToken || parsed.refresh_token || '').trim() || '',
99
+ expiresAt: Number(parsed.expiresAt || parsed.expiryDate || parsed.expiry_date || 0) || null,
100
+ primaryToken: accessToken
101
+ };
102
+ }
103
+
104
+ function getClaudeKeychainServiceName() {
105
+ const oauthSuffix = process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL ? '-custom-oauth' : '';
106
+ const customConfigDir = process.env.CLAUDE_CONFIG_DIR ? NATIVE_PATHS.claude.dir : '';
107
+ const suffix = customConfigDir ? `-${sha256(customConfigDir).slice(0, 8)}` : '';
108
+ return `Claude Code${oauthSuffix}-credentials${suffix}`;
109
+ }
110
+
111
+ function getClaudeKeychainAccountName() {
112
+ return process.env.USER || os.userInfo().username;
113
+ }
114
+
115
+ function readClaudeNativeOAuth() {
116
+ const keychainRaw = nativeKeychain.isSupported()
117
+ ? nativeKeychain.getPassword(getClaudeKeychainServiceName(), getClaudeKeychainAccountName())
118
+ : null;
119
+ if (keychainRaw) {
120
+ const parsed = parseClaudeOAuthPayload(readJsonFileFromString(keychainRaw));
121
+ if (parsed) {
122
+ return { ...parsed, storage: 'keychain' };
123
+ }
124
+ }
125
+
126
+ const filePayload = readJsonFile(NATIVE_PATHS.claude.credentials, null);
127
+ const parsedFile = parseClaudeOAuthPayload(filePayload);
128
+ if (parsedFile) {
129
+ return { ...parsedFile, storage: 'file' };
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ function clearClaudeOAuth() {
136
+ if (nativeKeychain.isSupported()) {
137
+ nativeKeychain.deletePassword(getClaudeKeychainServiceName(), getClaudeKeychainAccountName());
138
+ }
139
+ removeFileIfExists(NATIVE_PATHS.claude.credentials);
140
+
141
+ let settings = {};
142
+ try {
143
+ settings = claudeSettingsManager.settingsExists()
144
+ ? claudeSettingsManager.readSettings()
145
+ : {};
146
+ } catch {
147
+ settings = {};
148
+ }
149
+
150
+ settings.env = settings.env || {};
151
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
152
+ delete settings.env.CLAUDE_CODE_OAUTH_TOKEN;
153
+ claudeSettingsManager.writeSettings(settings);
154
+ }
155
+
156
+ function applyClaudeOAuth(credential) {
157
+ clearClaudeOAuth();
158
+
159
+ let settings = {};
160
+ try {
161
+ settings = claudeSettingsManager.settingsExists()
162
+ ? claudeSettingsManager.readSettings()
163
+ : {};
164
+ } catch {
165
+ settings = {};
166
+ }
167
+
168
+ settings.env = settings.env || {};
169
+ delete settings.env.ANTHROPIC_API_KEY;
170
+ delete settings.env.ANTHROPIC_BASE_URL;
171
+ delete settings.env.ANTHROPIC_MODEL;
172
+ delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
173
+ delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
174
+ delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
175
+ // 不删除 HTTP_PROXY / HTTPS_PROXY / NO_PROXY,这些是与 OAuth 无关的通用网络配置
176
+ delete settings.apiKeyHelper;
177
+
178
+ const wrappedPayload = {
179
+ claudeAiOauth: {
180
+ accessToken: credential.accessToken,
181
+ refreshToken: credential.refreshToken || undefined,
182
+ expiresAt: credential.expiresAt || undefined
183
+ }
184
+ };
185
+
186
+ const rawPayload = JSON.stringify(wrappedPayload);
187
+ const wroteKeychain = nativeKeychain.isSupported()
188
+ ? nativeKeychain.setPassword(getClaudeKeychainServiceName(), getClaudeKeychainAccountName(), rawPayload)
189
+ : false;
190
+
191
+ if (!wroteKeychain) {
192
+ writeJsonFile(NATIVE_PATHS.claude.credentials, wrappedPayload);
193
+ }
194
+
195
+ claudeSettingsManager.writeSettings(settings);
196
+ return { storage: wroteKeychain ? 'keychain' : 'file' };
197
+ }
198
+
199
+ function inspectClaudeState() {
200
+ const { getProxyStatus } = require('../proxy-server');
201
+ const proxyStatus = getProxyStatus();
202
+ const nativeOAuth = readClaudeNativeOAuth();
203
+
204
+ let channelConfigured = false;
205
+ try {
206
+ const settings = claudeSettingsManager.settingsExists()
207
+ ? claudeSettingsManager.readSettings()
208
+ : {};
209
+ const env = settings?.env || {};
210
+ channelConfigured = Boolean(
211
+ String(env.ANTHROPIC_API_KEY || '').trim()
212
+ || String(env.ANTHROPIC_BASE_URL || '').trim()
213
+ || String(settings.apiKeyHelper || '').trim()
214
+ );
215
+ } catch {
216
+ channelConfigured = false;
217
+ }
218
+
219
+ return {
220
+ tool: 'claude',
221
+ mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
222
+ proxyRunning: proxyStatus.running,
223
+ oauthPresent: Boolean(nativeOAuth),
224
+ channelConfigured,
225
+ nativeCredential: nativeOAuth ? buildNativeSummary(nativeOAuth) : null
226
+ };
227
+ }
228
+
229
+ function readJsonFileFromString(raw) {
230
+ try {
231
+ return JSON.parse(raw);
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+
237
+ function getCodexKeychainAccount() {
238
+ const codexHome = NATIVE_PATHS.codex.dir;
239
+ const resolvedPath = fs.existsSync(codexHome)
240
+ ? fs.realpathSync.native(codexHome)
241
+ : path.resolve(codexHome);
242
+ return `cli|${sha256(resolvedPath).slice(0, 16)}`;
243
+ }
244
+
245
+ function parseCodexAuthPayload(raw) {
246
+ if (!raw || typeof raw !== 'object') {
247
+ return null;
248
+ }
249
+
250
+ const auth = raw.tokens ? raw : {
251
+ auth_mode: 'chatgpt',
252
+ tokens: raw
253
+ };
254
+
255
+ const tokens = auth.tokens && typeof auth.tokens === 'object' ? auth.tokens : null;
256
+ if (!tokens || !tokens.access_token) {
257
+ return null;
258
+ }
259
+
260
+ const idTokenPayload = decodeJwtPayload(tokens.id_token);
261
+ const accessTokenPayload = decodeJwtPayload(tokens.access_token);
262
+ const exp = Number(idTokenPayload?.exp || accessTokenPayload?.exp || 0) || null;
263
+ return {
264
+ authMode: String(auth.auth_mode || 'chatgpt').trim() || 'chatgpt',
265
+ accessToken: String(tokens.access_token || '').trim(),
266
+ refreshToken: String(tokens.refresh_token || '').trim() || '',
267
+ idToken: String(tokens.id_token || '').trim() || '',
268
+ accountId: String(tokens.account_id || '').trim() || '',
269
+ accountEmail: String(idTokenPayload?.email || '').trim() || '',
270
+ expiresAt: exp ? exp * 1000 : null,
271
+ lastRefresh: auth.last_refresh || null,
272
+ primaryToken: String(tokens.access_token || '').trim()
273
+ };
274
+ }
275
+
276
+ function readCodexKeychainAuth() {
277
+ const raw = nativeKeychain.isSupported()
278
+ ? nativeKeychain.getPassword(CODEX_KEYCHAIN_SERVICE, getCodexKeychainAccount())
279
+ : null;
280
+ if (!raw) {
281
+ return null;
282
+ }
283
+ const parsed = parseCodexAuthPayload(readJsonFileFromString(raw));
284
+ return parsed ? { ...parsed, storage: 'keychain' } : null;
285
+ }
286
+
287
+ function readCodexFileAuth() {
288
+ const payload = readJsonFile(NATIVE_PATHS.codex.auth, null);
289
+ const parsed = parseCodexAuthPayload(payload);
290
+ return parsed ? { ...parsed, storage: 'auth-file' } : null;
291
+ }
292
+
293
+ function readCodexNativeOAuth() {
294
+ return readCodexKeychainAuth() || readCodexFileAuth();
295
+ }
296
+
297
+ function clearCodexOAuth() {
298
+ if (nativeKeychain.isSupported()) {
299
+ nativeKeychain.deletePassword(CODEX_KEYCHAIN_SERVICE, getCodexKeychainAccount());
300
+ }
301
+
302
+ let auth = {};
303
+ try {
304
+ auth = codexSettingsManager.readAuth();
305
+ } catch {
306
+ auth = {};
307
+ }
308
+ delete auth.tokens;
309
+ delete auth.auth_mode;
310
+ delete auth.last_refresh;
311
+ codexSettingsManager.writeAuth(auth);
312
+ }
313
+
314
+ function removeCodexChannelEnvVars() {
315
+ // auth.json 由 clearCodexChannelConfig 负责清理,Codex 优先读取 auth.json,无需操作 shell 配置文件
316
+ }
317
+
318
+ function clearCodexChannelConfig() {
319
+ removeCodexChannelEnvVars();
320
+
321
+ const configPath = NATIVE_PATHS.codex.config;
322
+ let config = {};
323
+ if (fs.existsSync(configPath)) {
324
+ try {
325
+ config = toml.parse(fs.readFileSync(configPath, 'utf8'));
326
+ } catch {
327
+ config = {};
328
+ }
329
+ }
330
+
331
+ const managedProviderKeys = new Set(['cc-proxy']);
332
+ loadChannelsFile('codex').forEach((channel) => {
333
+ if (channel?.providerKey) {
334
+ managedProviderKeys.add(channel.providerKey);
335
+ }
336
+ });
337
+
338
+ if (config.model_provider && managedProviderKeys.has(config.model_provider)) {
339
+ delete config.model_provider;
340
+ }
341
+
342
+ if (config.model_providers && typeof config.model_providers === 'object') {
343
+ Object.keys(config.model_providers).forEach((providerKey) => {
344
+ if (managedProviderKeys.has(providerKey)) {
345
+ delete config.model_providers[providerKey];
346
+ }
347
+ });
348
+ if (Object.keys(config.model_providers).length === 0) {
349
+ delete config.model_providers;
350
+ }
351
+ }
352
+
353
+ ensureDir(path.dirname(configPath));
354
+ fs.writeFileSync(configPath, tomlStringify(config), 'utf8');
355
+ }
356
+
357
+ function applyCodexOAuth(credential) {
358
+ clearCodexOAuth();
359
+ clearCodexChannelConfig();
360
+
361
+ const authPayload = {
362
+ auth_mode: credential.authMode || 'chatgpt',
363
+ tokens: {
364
+ access_token: credential.accessToken,
365
+ refresh_token: credential.refreshToken || '',
366
+ id_token: credential.idToken || '',
367
+ account_id: credential.accountId || ''
368
+ },
369
+ last_refresh: credential.lastRefresh || new Date().toISOString()
370
+ };
371
+
372
+ writeJsonFile(NATIVE_PATHS.codex.auth, authPayload);
373
+ const wroteKeychain = nativeKeychain.isSupported()
374
+ ? nativeKeychain.setPassword(CODEX_KEYCHAIN_SERVICE, getCodexKeychainAccount(), JSON.stringify(authPayload))
375
+ : false;
376
+ return { storage: wroteKeychain ? 'auth-file+keychain' : 'auth-file' };
377
+ }
378
+
379
+ function inspectCodexState() {
380
+ const { getCodexProxyStatus } = require('../codex-proxy-server');
381
+ const proxyStatus = getCodexProxyStatus();
382
+ const nativeOAuth = readCodexNativeOAuth();
383
+
384
+ let channelConfigured = false;
385
+ try {
386
+ const config = codexSettingsManager.readConfig();
387
+ channelConfigured = Boolean(
388
+ (config?.model_provider && config.model_provider !== 'cc-proxy')
389
+ || (config?.model_providers && Object.keys(config.model_providers).length > 0)
390
+ );
391
+ } catch {
392
+ channelConfigured = false;
393
+ }
394
+
395
+ return {
396
+ tool: 'codex',
397
+ mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
398
+ proxyRunning: proxyStatus.running,
399
+ oauthPresent: Boolean(nativeOAuth),
400
+ channelConfigured,
401
+ nativeCredential: nativeOAuth ? buildNativeSummary(nativeOAuth) : null
402
+ };
403
+ }
404
+
405
+ function deriveGeminiEncryptionKey() {
406
+ const salt = `${os.hostname()}-${os.userInfo().username}-gemini-cli`;
407
+ return crypto.scryptSync(GEMINI_KEYCHAIN_SERVICE, salt, 32);
408
+ }
409
+
410
+ function encryptGeminiPayload(value) {
411
+ const key = deriveGeminiEncryptionKey();
412
+ const iv = crypto.randomBytes(16);
413
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
414
+ let encrypted = cipher.update(value, 'utf8', 'hex');
415
+ encrypted += cipher.final('hex');
416
+ const authTag = cipher.getAuthTag();
417
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
418
+ }
419
+
420
+ function decryptGeminiPayload(value) {
421
+ const parts = String(value || '').split(':');
422
+ if (parts.length !== 3) {
423
+ throw new Error('Invalid encrypted data format');
424
+ }
425
+
426
+ const key = deriveGeminiEncryptionKey();
427
+ const iv = Buffer.from(parts[0], 'hex');
428
+ const authTag = Buffer.from(parts[1], 'hex');
429
+ const encrypted = parts[2];
430
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
431
+ decipher.setAuthTag(authTag);
432
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
433
+ decrypted += decipher.final('utf8');
434
+ return decrypted;
435
+ }
436
+
437
+ function parseGeminiCredential(raw, googleAccounts = null) {
438
+ if (!raw || typeof raw !== 'object') {
439
+ return null;
440
+ }
441
+
442
+ const credential = raw.token && typeof raw.token === 'object'
443
+ ? raw
444
+ : raw.access_token
445
+ ? {
446
+ serverName: GEMINI_MAIN_ACCOUNT_KEY,
447
+ token: {
448
+ accessToken: raw.access_token,
449
+ refreshToken: raw.refresh_token || '',
450
+ tokenType: raw.token_type || 'Bearer',
451
+ scope: raw.scope || '',
452
+ expiresAt: raw.expiry_date || null
453
+ },
454
+ updatedAt: Date.now()
455
+ }
456
+ : null;
457
+
458
+ if (!credential?.token?.accessToken) {
459
+ return null;
460
+ }
461
+
462
+ return {
463
+ accessToken: String(credential.token.accessToken || '').trim(),
464
+ refreshToken: String(credential.token.refreshToken || '').trim() || '',
465
+ tokenType: String(credential.token.tokenType || 'Bearer').trim() || 'Bearer',
466
+ scope: String(credential.token.scope || '').trim() || '',
467
+ expiresAt: Number(credential.token.expiresAt || 0) || null,
468
+ accountEmail: String(googleAccounts?.active || '').trim() || '',
469
+ primaryToken: String(credential.token.accessToken || '').trim()
470
+ };
471
+ }
472
+
473
+ function readGeminiEncryptedCredential() {
474
+ try {
475
+ if (!fs.existsSync(NATIVE_PATHS.gemini.oauthCredentialsEncrypted)) {
476
+ return null;
477
+ }
478
+ const encrypted = fs.readFileSync(NATIVE_PATHS.gemini.oauthCredentialsEncrypted, 'utf8');
479
+ const decrypted = decryptGeminiPayload(encrypted);
480
+ const payload = JSON.parse(decrypted);
481
+ const googleAccounts = readJsonFile(NATIVE_PATHS.gemini.googleAccounts, null);
482
+ return parseGeminiCredential(payload[GEMINI_MAIN_ACCOUNT_KEY], googleAccounts)
483
+ || parseGeminiCredential(payload, googleAccounts);
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+
489
+ function readGeminiKeychainCredential() {
490
+ const raw = nativeKeychain.isSupported()
491
+ ? nativeKeychain.getPassword(GEMINI_KEYCHAIN_SERVICE, GEMINI_MAIN_ACCOUNT_KEY)
492
+ : null;
493
+ if (!raw) {
494
+ return null;
495
+ }
496
+
497
+ const googleAccounts = readJsonFile(NATIVE_PATHS.gemini.googleAccounts, null);
498
+ const parsed = parseGeminiCredential(readJsonFileFromString(raw), googleAccounts);
499
+ return parsed ? { ...parsed, storage: 'keychain' } : null;
500
+ }
501
+
502
+ function readGeminiLegacyCredential() {
503
+ const payload = readJsonFile(NATIVE_PATHS.gemini.oauthCredentialsLegacy, null);
504
+ const googleAccounts = readJsonFile(NATIVE_PATHS.gemini.googleAccounts, null);
505
+ const parsed = parseGeminiCredential(payload, googleAccounts);
506
+ return parsed ? { ...parsed, storage: 'legacy-file' } : null;
507
+ }
508
+
509
+ function readGeminiNativeOAuth() {
510
+ const keychain = readGeminiKeychainCredential();
511
+ if (keychain) {
512
+ return keychain;
513
+ }
514
+
515
+ const encrypted = readGeminiEncryptedCredential();
516
+ if (encrypted) {
517
+ return { ...encrypted, storage: 'encrypted-file' };
518
+ }
519
+ return readGeminiLegacyCredential();
520
+ }
521
+
522
+ function clearGeminiOAuth() {
523
+ if (nativeKeychain.isSupported()) {
524
+ nativeKeychain.deletePassword(GEMINI_KEYCHAIN_SERVICE, GEMINI_MAIN_ACCOUNT_KEY);
525
+ }
526
+ removeFileIfExists(NATIVE_PATHS.gemini.oauthCredentialsEncrypted);
527
+ removeFileIfExists(NATIVE_PATHS.gemini.oauthCredentialsLegacy);
528
+
529
+ const googleAccounts = readJsonFile(NATIVE_PATHS.gemini.googleAccounts, null);
530
+ if (googleAccounts && typeof googleAccounts === 'object') {
531
+ googleAccounts.active = null;
532
+ writeJsonFile(NATIVE_PATHS.gemini.googleAccounts, googleAccounts);
533
+ }
534
+ }
535
+
536
+ function clearGeminiChannelConfig() {
537
+ const env = geminiSettingsManager.configExists()
538
+ ? geminiSettingsManager.readEnv()
539
+ : {};
540
+ delete env.GOOGLE_GEMINI_BASE_URL;
541
+ delete env.GEMINI_API_KEY;
542
+ delete env.GEMINI_MODEL;
543
+ geminiSettingsManager.writeEnv(env);
544
+ }
545
+
546
+ function applyGeminiOAuth(credential) {
547
+ clearGeminiOAuth();
548
+ clearGeminiChannelConfig();
549
+
550
+ const payload = {
551
+ [GEMINI_MAIN_ACCOUNT_KEY]: {
552
+ serverName: GEMINI_MAIN_ACCOUNT_KEY,
553
+ token: {
554
+ accessToken: credential.accessToken,
555
+ refreshToken: credential.refreshToken || undefined,
556
+ tokenType: credential.tokenType || 'Bearer',
557
+ scope: credential.scope || undefined,
558
+ expiresAt: credential.expiresAt || undefined
559
+ },
560
+ updatedAt: Date.now()
561
+ }
562
+ };
563
+
564
+ ensureDir(path.dirname(NATIVE_PATHS.gemini.oauthCredentialsEncrypted));
565
+ fs.writeFileSync(
566
+ NATIVE_PATHS.gemini.oauthCredentialsEncrypted,
567
+ encryptGeminiPayload(JSON.stringify(payload, null, 2)),
568
+ 'utf8'
569
+ );
570
+ ensureFileMode(NATIVE_PATHS.gemini.oauthCredentialsEncrypted);
571
+ const wroteKeychain = nativeKeychain.isSupported()
572
+ ? nativeKeychain.setPassword(
573
+ GEMINI_KEYCHAIN_SERVICE,
574
+ GEMINI_MAIN_ACCOUNT_KEY,
575
+ JSON.stringify(payload[GEMINI_MAIN_ACCOUNT_KEY])
576
+ )
577
+ : false;
578
+
579
+ const settings = geminiSettingsManager.settingsExists()
580
+ ? geminiSettingsManager.readSettings()
581
+ : {};
582
+ settings.security = settings.security || {};
583
+ settings.security.auth = settings.security.auth || {};
584
+ settings.security.auth.selectedType = 'oauth-personal';
585
+ geminiSettingsManager.writeSettings(settings);
586
+
587
+ if (credential.accountEmail) {
588
+ writeJsonFile(NATIVE_PATHS.gemini.googleAccounts, {
589
+ active: credential.accountEmail,
590
+ old: []
591
+ });
592
+ }
593
+
594
+ return { storage: wroteKeychain ? 'encrypted-file+keychain' : 'encrypted-file' };
595
+ }
596
+
597
+ function inspectGeminiState() {
598
+ const { getGeminiProxyStatus } = require('../gemini-proxy-server');
599
+ const proxyStatus = getGeminiProxyStatus();
600
+ const nativeOAuth = readGeminiNativeOAuth();
601
+
602
+ let channelConfigured = false;
603
+ try {
604
+ const env = geminiSettingsManager.configExists()
605
+ ? geminiSettingsManager.readEnv()
606
+ : {};
607
+ const settings = geminiSettingsManager.settingsExists()
608
+ ? geminiSettingsManager.readSettings()
609
+ : {};
610
+ channelConfigured = Boolean(
611
+ String(env.GOOGLE_GEMINI_BASE_URL || '').trim()
612
+ || String(env.GEMINI_API_KEY || '').trim()
613
+ || settings?.security?.auth?.selectedType === 'gemini-api-key'
614
+ );
615
+ } catch {
616
+ channelConfigured = false;
617
+ }
618
+
619
+ return {
620
+ tool: 'gemini',
621
+ mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
622
+ proxyRunning: proxyStatus.running,
623
+ oauthPresent: Boolean(nativeOAuth),
624
+ channelConfigured,
625
+ nativeCredential: nativeOAuth ? buildNativeSummary(nativeOAuth) : null
626
+ };
627
+ }
628
+
629
+ function parseOpenCodeOAuthPayload(raw) {
630
+ if (!raw || typeof raw !== 'object') {
631
+ return null;
632
+ }
633
+
634
+ if (raw.type === 'oauth' || raw.access) {
635
+ return parseOpenCodeOAuthEntry(raw.providerId, raw);
636
+ }
637
+
638
+ if (raw.openai && typeof raw.openai === 'object') {
639
+ return parseOpenCodeOAuthEntry('openai', raw.openai);
640
+ }
641
+
642
+ for (const [providerId, value] of Object.entries(raw)) {
643
+ const parsed = parseOpenCodeOAuthEntry(providerId, value);
644
+ if (parsed) {
645
+ return parsed;
646
+ }
647
+ }
648
+
649
+ return null;
650
+ }
651
+
652
+ function parseOpenCodeOAuthEntry(providerId, target) {
653
+ const normalizedProviderId = String(providerId || 'openai').trim() || 'openai';
654
+
655
+ if (!target || target.type !== 'oauth' || !target.access) {
656
+ return null;
657
+ }
658
+
659
+ return {
660
+ providerId: normalizedProviderId,
661
+ accessToken: String(target.access || '').trim(),
662
+ refreshToken: String(target.refresh || '').trim() || '',
663
+ expiresAt: Number(target.expires || 0) || null,
664
+ accountId: String(target.accountId || '').trim() || '',
665
+ enterpriseUrl: String(target.enterpriseUrl || '').trim() || '',
666
+ primaryToken: String(target.access || '').trim()
667
+ };
668
+ }
669
+
670
+ function getActiveOpenCodeProviderId() {
671
+ try {
672
+ const configPath = opencodeSettingsManager.selectConfigPath();
673
+ if (!configPath || !fs.existsSync(configPath)) {
674
+ return '';
675
+ }
676
+
677
+ const config = opencodeSettingsManager.readConfig(configPath);
678
+ const modelRef = String(config?.model || '').trim();
679
+ if (modelRef.includes('/')) {
680
+ return modelRef.split('/')[0].trim();
681
+ }
682
+
683
+ const providerIds = config?.provider && typeof config.provider === 'object'
684
+ ? Object.keys(config.provider).filter(Boolean)
685
+ : [];
686
+ return providerIds.length === 1 ? providerIds[0] : '';
687
+ } catch {
688
+ return '';
689
+ }
690
+ }
691
+
692
+ function readAllOpenCodeNativeOAuth() {
693
+ const payload = readJsonFile(NATIVE_PATHS.opencode.auth, null);
694
+ if (!payload || typeof payload !== 'object') {
695
+ return [];
696
+ }
697
+
698
+ const activeProviderId = getActiveOpenCodeProviderId();
699
+ const credentials = Object.entries(payload)
700
+ .map(([providerId, value]) => parseOpenCodeOAuthEntry(providerId, value))
701
+ .filter(Boolean)
702
+ .map((entry) => ({ ...entry, storage: 'auth-file' }));
703
+
704
+ if (!activeProviderId || credentials.length <= 1) {
705
+ return credentials;
706
+ }
707
+
708
+ return credentials.sort((left, right) => {
709
+ if (left.providerId === activeProviderId) return -1;
710
+ if (right.providerId === activeProviderId) return 1;
711
+ return left.providerId.localeCompare(right.providerId);
712
+ });
713
+ }
714
+
715
+ function readOpenCodeNativeOAuth() {
716
+ return readAllOpenCodeNativeOAuth()[0] || null;
717
+ }
718
+
719
+ function clearOpenCodeOAuth() {
720
+ const payload = readJsonFile(NATIVE_PATHS.opencode.auth, {});
721
+ if (!payload || typeof payload !== 'object') {
722
+ return;
723
+ }
724
+
725
+ Object.keys(payload).forEach((providerId) => {
726
+ if (payload[providerId]?.type === 'oauth') {
727
+ delete payload[providerId];
728
+ }
729
+ });
730
+
731
+ if (Object.keys(payload).length === 0) {
732
+ removeFileIfExists(NATIVE_PATHS.opencode.auth);
733
+ return;
734
+ }
735
+
736
+ writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
737
+ }
738
+
739
+ function applyOpenCodeOAuth(credential) {
740
+ clearOpenCodeOAuth();
741
+ opencodeSettingsManager.clearManagedChannelConfig();
742
+
743
+ const providerId = String(credential.providerId || 'openai').trim() || 'openai';
744
+ const payload = readJsonFile(NATIVE_PATHS.opencode.auth, {});
745
+ payload[providerId] = {
746
+ type: 'oauth',
747
+ access: credential.accessToken,
748
+ refresh: credential.refreshToken || '',
749
+ expires: credential.expiresAt || null,
750
+ accountId: credential.accountId || undefined,
751
+ enterpriseUrl: credential.enterpriseUrl || undefined
752
+ };
753
+ writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
754
+
755
+ const configPath = opencodeSettingsManager.selectConfigPath();
756
+ const config = fs.existsSync(configPath)
757
+ ? opencodeSettingsManager.readConfig(configPath)
758
+ : {};
759
+ config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
760
+ if (!config.provider[providerId]) {
761
+ config.provider[providerId] = {};
762
+ }
763
+ opencodeSettingsManager.writeConfig(configPath, config);
764
+
765
+ return { storage: 'auth-file' };
766
+ }
767
+
768
+ function inspectOpenCodeState() {
769
+ const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
770
+ const proxyStatus = getOpenCodeProxyStatus();
771
+ const nativeOAuth = readOpenCodeNativeOAuth();
772
+
773
+ let channelConfigured = false;
774
+ try {
775
+ const configPath = opencodeSettingsManager.selectConfigPath();
776
+ const config = fs.existsSync(configPath)
777
+ ? opencodeSettingsManager.readConfig(configPath)
778
+ : {};
779
+ const providers = config?.provider && typeof config.provider === 'object'
780
+ ? Object.values(config.provider)
781
+ : [];
782
+ channelConfigured = providers.some(provider => provider?.__ctx_managed__ === true);
783
+ } catch {
784
+ channelConfigured = false;
785
+ }
786
+
787
+ return {
788
+ tool: 'opencode',
789
+ mode: proxyStatus.running ? 'proxy' : (nativeOAuth ? 'oauth' : (channelConfigured ? 'channel' : 'idle')),
790
+ proxyRunning: proxyStatus.running,
791
+ oauthPresent: Boolean(nativeOAuth),
792
+ channelConfigured,
793
+ nativeCredential: nativeOAuth ? buildNativeSummary(nativeOAuth) : null
794
+ };
795
+ }
796
+
797
+ function inspectTool(tool) {
798
+ switch (tool) {
799
+ case 'claude':
800
+ return inspectClaudeState();
801
+ case 'codex':
802
+ return inspectCodexState();
803
+ case 'gemini':
804
+ return inspectGeminiState();
805
+ case 'opencode':
806
+ return inspectOpenCodeState();
807
+ default:
808
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
809
+ }
810
+ }
811
+
812
+ function readNativeOAuth(tool) {
813
+ switch (tool) {
814
+ case 'claude':
815
+ return readClaudeNativeOAuth();
816
+ case 'codex':
817
+ return readCodexNativeOAuth();
818
+ case 'gemini':
819
+ return readGeminiNativeOAuth();
820
+ case 'opencode':
821
+ return readOpenCodeNativeOAuth();
822
+ default:
823
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
824
+ }
825
+ }
826
+
827
+ function readAllNativeOAuth(tool) {
828
+ switch (tool) {
829
+ case 'claude': {
830
+ const credential = readClaudeNativeOAuth();
831
+ return credential ? [credential] : [];
832
+ }
833
+ case 'codex': {
834
+ const credential = readCodexNativeOAuth();
835
+ return credential ? [credential] : [];
836
+ }
837
+ case 'gemini': {
838
+ const credential = readGeminiNativeOAuth();
839
+ return credential ? [credential] : [];
840
+ }
841
+ case 'opencode':
842
+ return readAllOpenCodeNativeOAuth();
843
+ default:
844
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
845
+ }
846
+ }
847
+
848
+ function clearNativeOAuth(tool) {
849
+ switch (tool) {
850
+ case 'claude':
851
+ clearClaudeOAuth();
852
+ return;
853
+ case 'codex':
854
+ clearCodexOAuth();
855
+ return;
856
+ case 'gemini':
857
+ clearGeminiOAuth();
858
+ return;
859
+ case 'opencode':
860
+ clearOpenCodeOAuth();
861
+ return;
862
+ default:
863
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
864
+ }
865
+ }
866
+
867
+ function applyOAuthCredential(tool, credential) {
868
+ switch (tool) {
869
+ case 'claude':
870
+ return applyClaudeOAuth(credential);
871
+ case 'codex':
872
+ return applyCodexOAuth(credential);
873
+ case 'gemini':
874
+ return applyGeminiOAuth(credential);
875
+ case 'opencode':
876
+ return applyOpenCodeOAuth(credential);
877
+ default:
878
+ throw new Error(`Unsupported OAuth tool: ${tool}`);
879
+ }
880
+ }
881
+
882
+ module.exports = {
883
+ SUPPORTED_TOOLS,
884
+ fingerprintFor,
885
+ inspectTool,
886
+ readNativeOAuth,
887
+ readAllNativeOAuth,
888
+ clearNativeOAuth,
889
+ applyOAuthCredential
890
+ };