claude-gemini-multimodal-bridge
Version:
Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities
269 lines (268 loc) • 9.51 kB
JavaScript
import { execSync, spawn } from 'child_process';
import { AuthenticationError, AuthErrorCode } from '../core/types.js';
import { logger } from '../utils/logger.js';
import { safeExecute } from '../utils/errorHandler.js';
import path from 'path';
import fs from 'fs';
import os from 'os';
export class OAuthManager {
authCache = new Map();
CACHE_TTL = 3600000;
async checkGeminiAuthentication() {
return safeExecute(async () => {
const cached = this.authCache.get('gemini');
if (cached && this.isCacheValid('gemini')) {
return cached;
}
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
if (fs.existsSync(oauthFile)) {
try {
const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf8'));
if (creds.access_token || creds.refresh_token) {
const status = {
isAuthenticated: true,
method: 'oauth',
userInfo: {
planType: 'free',
email: undefined,
quotaRemaining: undefined
}
};
this.updateCache('gemini', status);
return status;
}
}
catch (error) {
logger.warn('OAuth credentials file exists but could not be read', {
error: error instanceof Error ? error.message : String(error)
});
}
}
const apiKey = process.env.GEMINI_API_KEY;
if (apiKey && apiKey.length > 10) {
const status = {
isAuthenticated: true,
method: 'api_key',
userInfo: {
planType: 'free',
email: undefined,
quotaRemaining: undefined
}
};
this.updateCache('gemini', status);
return status;
}
return {
isAuthenticated: false,
method: 'oauth',
userInfo: undefined,
};
}, {
operationName: 'check-gemini-auth',
layer: 'gemini',
timeout: 10000,
});
}
async promptGeminiLogin() {
return safeExecute(async () => {
logger.info('Prompting Gemini authentication...');
const method = await this.detectAuthMethod();
if (method === 'api_key') {
logger.info('API key authentication detected. Please ensure GEMINI_API_KEY is set.');
const status = await this.checkApiKeyAuth();
return status.isAuthenticated;
}
logger.info('Starting OAuth authentication flow...');
try {
const result = await this.executeGeminiAuth();
if (result) {
logger.info('OAuth authentication successful');
const status = await this.checkGeminiAuthentication();
return status.isAuthenticated;
}
return false;
}
catch (error) {
logger.error('OAuth authentication failed', { error: error.message });
throw new AuthenticationError('OAuth authentication failed. Please try again or use API key method.', 'gemini', AuthErrorCode.OAUTH_FLOW_FAILED, {
method: 'oauth',
instructions: 'Run "gemini auth" manually or set GEMINI_API_KEY environment variable',
canRetry: true
});
}
}, {
operationName: 'prompt-gemini-login',
layer: 'gemini',
timeout: 60000,
});
}
async refreshGeminiToken() {
return safeExecute(async () => {
const method = await this.detectAuthMethod();
if (method === 'api_key') {
const status = await this.checkApiKeyAuth();
this.updateCache('gemini', status);
return status.isAuthenticated;
}
logger.info('Attempting to refresh OAuth token...');
try {
this.authCache.delete('gemini');
const currentStatus = await this.checkOAuthAuth();
if (currentStatus.isAuthenticated) {
this.updateCache('gemini', currentStatus);
return true;
}
logger.warn('OAuth token expired, re-authentication required');
return await this.promptGeminiLogin();
}
catch (error) {
logger.error('Token refresh failed', { error: error.message });
return false;
}
}, {
operationName: 'refresh-gemini-token',
layer: 'gemini',
timeout: 30000,
});
}
async detectAuthMethod() {
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
if (fs.existsSync(oauthFile)) {
return 'oauth';
}
const geminiApiKey = process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY;
if (geminiApiKey && geminiApiKey.length > 10) {
return 'api_key';
}
return 'none';
}
async checkApiKeyAuth() {
const apiKey = process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY;
if (!apiKey) {
return {
isAuthenticated: false,
method: 'api_key',
userInfo: undefined,
};
}
if (apiKey.length < 10) {
logger.warn('API key format appears invalid');
return {
isAuthenticated: false,
method: 'api_key',
userInfo: undefined,
};
}
return {
isAuthenticated: true,
method: 'api_key',
userInfo: {
email: undefined,
quotaRemaining: undefined,
planType: 'free',
},
};
}
async checkOAuthAuth() {
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
if (fs.existsSync(oauthFile)) {
try {
const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf8'));
if (creds.access_token || creds.refresh_token) {
return {
isAuthenticated: true,
method: 'oauth',
userInfo: {
email: undefined,
quotaRemaining: undefined,
planType: 'free',
},
};
}
}
catch (error) {
logger.debug('OAuth credentials file could not be parsed', { error: error.message });
}
}
return {
isAuthenticated: false,
method: 'oauth',
userInfo: undefined,
};
}
async executeGeminiAuth() {
return new Promise((resolve, reject) => {
logger.info('Executing: gemini auth');
const child = spawn('gemini', ['auth'], {
stdio: 'inherit',
});
const timeout = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error('Authentication timeout'));
}, 120000);
child.on('close', (code) => {
clearTimeout(timeout);
if (code === 0) {
resolve(true);
}
else {
reject(new Error(`Authentication failed with exit code: ${code}`));
}
});
child.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
async checkGeminiCLIAvailable() {
try {
execSync('which gemini', { stdio: 'ignore' });
return true;
}
catch {
try {
execSync('gemini --version', { stdio: 'ignore' });
return true;
}
catch {
return false;
}
}
}
updateCache(service, status) {
this.authCache.set(service, {
...status,
});
}
isCacheValid(_service) {
return false;
}
clearCache() {
this.authCache.clear();
logger.debug('Authentication cache cleared');
}
async getAuthMethod(service) {
if (service === 'gemini' || service === 'aistudio') {
return await this.detectAuthMethod();
}
if (service === 'claude') {
return 'session';
}
return 'none';
}
validateApiKey(apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
return false;
}
return apiKey.length >= 20 && apiKey.startsWith('AI');
}
maskApiKey(apiKey) {
if (!apiKey || apiKey.length < 8) {
return '***';
}
return apiKey.substring(0, 8) + '***';
}
}