agentgui 1.0.752 → 1.0.754

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/server.js CHANGED
@@ -10,7 +10,6 @@ import { execSync, spawn } from 'child_process';
10
10
  import { LRUCache } from 'lru-cache';
11
11
  import { createRequire } from 'module';
12
12
  const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
13
- import { OAuth2Client } from 'google-auth-library';
14
13
  import express from 'express';
15
14
  import Busboy from 'busboy';
16
15
  import fsbrowse from 'fsbrowse';
@@ -18,6 +17,9 @@ import { queries } from './database.js';
18
17
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
19
18
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
20
19
  import { findCommand, queryACPServerAgents, discoverAgents, discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
20
+ import { getGeminiOAuthCreds, startGeminiOAuth, exchangeGeminiOAuthCode, handleGeminiOAuthCallback, getGeminiOAuthStatus, getGeminiOAuthState } from './lib/oauth-gemini.js';
21
+ import { initSpeechManager, getSpeech, ensurePocketTtsSetup, voiceCacheManager, modelDownloadState, broadcastModelProgress, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
22
+ import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
21
23
  import { WSOptimizer } from './lib/ws-optimizer.js';
22
24
  import { WsRouter } from './lib/ws-protocol.js';
23
25
  import { encode as wsEncode } from './lib/codec.js';
@@ -68,232 +70,6 @@ process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - un
68
70
  process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
69
71
  process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
70
72
 
71
- const ttsTextAccumulators = new Map();
72
- const voiceCacheManager = {
73
- generating: new Map(),
74
- maxCacheSize: 10 * 1024 * 1024,
75
- async getOrGenerateCache(conversationId, text) {
76
- const cacheKey = `${conversationId}:${text}`;
77
- if (this.generating.has(cacheKey)) {
78
- return new Promise((resolve) => {
79
- const checkInterval = setInterval(() => {
80
- const cached = queries.getVoiceCache(conversationId, text);
81
- if (cached) {
82
- clearInterval(checkInterval);
83
- resolve(cached);
84
- }
85
- }, 50);
86
- });
87
- }
88
- const cached = queries.getVoiceCache(conversationId, text);
89
- if (cached) return cached;
90
- this.generating.set(cacheKey, true);
91
- try {
92
- const speech = await getSpeech();
93
- const audioBlob = await speech.synthesize(text, 'default');
94
- const saved = queries.saveVoiceCache(conversationId, text, audioBlob);
95
- const totalSize = queries.getVoiceCacheSize(conversationId);
96
- if (totalSize > this.maxCacheSize) {
97
- const needed = totalSize - this.maxCacheSize;
98
- queries.deleteOldestVoiceCache(conversationId, needed);
99
- }
100
- return saved;
101
- } finally {
102
- this.generating.delete(cacheKey);
103
- }
104
- }
105
- };
106
-
107
- let speechModule = null;
108
- async function getSpeech() {
109
- if (!speechModule) speechModule = await import('./lib/speech.js');
110
- return speechModule;
111
- }
112
-
113
- async function ensurePocketTtsSetup(onProgress) {
114
- const { createRequire: cr } = await import('module');
115
- const r = cr(import.meta.url);
116
- const serverTTS = r('webtalk/server-tts');
117
- return serverTTS.ensureInstalled(onProgress);
118
- }
119
-
120
- // Model download manager
121
- const modelDownloadState = {
122
- downloading: false,
123
- progress: null,
124
- error: null,
125
- complete: false,
126
- startTime: null,
127
- downloadMetrics: new Map()
128
- };
129
-
130
- function broadcastModelProgress(progress) {
131
- modelDownloadState.progress = progress;
132
- const broadcastData = {
133
- type: 'model_download_progress',
134
- modelId: progress.type || 'unknown',
135
- bytesDownloaded: progress.bytesDownloaded || 0,
136
- bytesRemaining: progress.bytesRemaining || 0,
137
- totalBytes: progress.totalBytes || 0,
138
- downloadSpeed: progress.downloadSpeed || 0,
139
- eta: progress.eta || 0,
140
- retryCount: progress.retryCount || 0,
141
- currentGateway: progress.currentGateway || '',
142
- status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
143
- percentComplete: progress.percentComplete || 0,
144
- completedFiles: progress.completedFiles || 0,
145
- totalFiles: progress.totalFiles || 0,
146
- timestamp: Date.now(),
147
- ...progress
148
- };
149
- broadcastSync(broadcastData);
150
- }
151
-
152
- async function validateAndCleanupModels(modelsDir) {
153
- try {
154
- const manifestPath = path.join(modelsDir, '.manifests.json');
155
- if (fs.existsSync(manifestPath)) {
156
- try {
157
- const content = fs.readFileSync(manifestPath, 'utf8');
158
- JSON.parse(content);
159
- } catch (e) {
160
- console.error('[MODELS] Manifest corrupted, removing:', e.message);
161
- fs.unlinkSync(manifestPath);
162
- }
163
- }
164
-
165
- const files = fs.readdirSync(modelsDir);
166
- for (const file of files) {
167
- if (file.endsWith('.tmp')) {
168
- try {
169
- fs.unlinkSync(path.join(modelsDir, file));
170
- console.log('[MODELS] Cleaned up temp file:', file);
171
- } catch (e) {
172
- console.warn('[MODELS] Failed to clean:', file);
173
- }
174
- }
175
- }
176
- } catch (e) {
177
- console.warn('[MODELS] Cleanup check failed:', e.message);
178
- }
179
- }
180
-
181
- async function ensureModelsDownloaded() {
182
- if (modelDownloadState.downloading) {
183
- while (modelDownloadState.downloading) {
184
- await new Promise(r => setTimeout(r, 100));
185
- }
186
- return modelDownloadState.complete;
187
- }
188
-
189
- modelDownloadState.downloading = true;
190
- modelDownloadState.error = null;
191
-
192
- try {
193
- const r = createRequire(import.meta.url);
194
- const { createConfig } = r('webtalk/config');
195
- const { ensureModel } = r('webtalk/whisper-models');
196
- const { ensureTTSModels } = r('webtalk/tts-models');
197
- const gmguiModels = path.join(os.homedir(), '.gmgui', 'models');
198
- const modelsBase = process.env.PORTABLE_EXE_DIR
199
- ? (fs.existsSync(path.join(process.env.PORTABLE_EXE_DIR, 'models', 'onnx-community')) ? path.join(process.env.PORTABLE_EXE_DIR, 'models') : gmguiModels)
200
- : gmguiModels;
201
-
202
- await validateAndCleanupModels(modelsBase);
203
-
204
- const config = createConfig({
205
- modelsDir: modelsBase,
206
- ttsModelsDir: path.join(modelsBase, 'tts'),
207
- });
208
-
209
- // Progress callback for broadcasting download progress
210
- const onProgress = (progress) => {
211
- broadcastModelProgress({
212
- ...progress,
213
- started: true,
214
- done: false,
215
- downloading: true
216
- });
217
- };
218
-
219
- broadcastModelProgress({ started: true, done: false, downloading: true, type: 'whisper', status: 'starting' });
220
- await ensureModel('onnx-community/whisper-base', config, onProgress);
221
-
222
- broadcastModelProgress({ started: true, done: false, downloading: true, type: 'tts', status: 'starting' });
223
- await ensureTTSModels(config, onProgress);
224
-
225
- modelDownloadState.complete = true;
226
- broadcastModelProgress({ started: true, done: true, complete: true, downloading: false });
227
- return true;
228
- } catch (err) {
229
- console.error('[MODELS] Download error:', err.message);
230
- modelDownloadState.error = err.message;
231
- broadcastModelProgress({ done: true, error: err.message });
232
- return false;
233
- } finally {
234
- modelDownloadState.downloading = false;
235
- }
236
- }
237
-
238
- function eagerTTS(text, conversationId, sessionId) {
239
- const key = `${conversationId}:${sessionId}`;
240
- let acc = ttsTextAccumulators.get(key);
241
- if (!acc) {
242
- acc = { text: '', timer: null };
243
- ttsTextAccumulators.set(key, acc);
244
- }
245
- acc.text += text;
246
- if (acc.timer) clearTimeout(acc.timer);
247
- acc.timer = setTimeout(() => flushTTSaccumulator(key, conversationId, sessionId), 600);
248
- }
249
-
250
- function flushTTSaccumulator(key, conversationId, sessionId) {
251
- const acc = ttsTextAccumulators.get(key);
252
- if (!acc || !acc.text) return;
253
- const text = acc.text.trim();
254
- acc.text = '';
255
- ttsTextAccumulators.delete(key);
256
-
257
- getSpeech().then(speech => {
258
- const status = speech.getStatus();
259
- if (!status.ttsReady || status.ttsError) return;
260
- const voices = new Set();
261
- for (const ws of syncClients) {
262
- const vid = ws.ttsVoiceId || 'default';
263
- const convKey = `conv-${conversationId}`;
264
- if (ws.subscriptions && (ws.subscriptions.has(sessionId) || ws.subscriptions.has(convKey))) {
265
- voices.add(vid);
266
- }
267
- }
268
- if (voices.size === 0) return;
269
- for (const vid of voices) {
270
- const cacheKey = speech.ttsCacheKey(text, vid);
271
- const cached = speech.ttsCacheGet(cacheKey);
272
- if (cached) {
273
- pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
274
- continue;
275
- }
276
- speech.synthesize(text, vid).then(wav => {
277
- if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
278
- pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
279
- }).catch(() => {});
280
- }
281
- }).catch(() => {});
282
- }
283
-
284
- function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
285
- const b64 = wav.toString('base64');
286
- broadcastSync({
287
- type: 'tts_audio',
288
- cacheKey,
289
- audio: b64,
290
- voiceId,
291
- conversationId,
292
- sessionId,
293
- timestamp: Date.now()
294
- });
295
- }
296
-
297
73
 
298
74
  function buildSystemPrompt(agentId, model, subAgent) {
299
75
  const parts = [];
@@ -483,551 +259,6 @@ async function getModelsForAgent(agentId) {
483
259
  return models;
484
260
  }
485
261
 
486
- const GEMINI_SCOPES = [
487
- 'https://www.googleapis.com/auth/cloud-platform',
488
- 'https://www.googleapis.com/auth/userinfo.email',
489
- 'https://www.googleapis.com/auth/userinfo.profile',
490
- ];
491
-
492
- function extractOAuthFromFile(oauth2Path) {
493
- try {
494
- const src = fs.readFileSync(oauth2Path, 'utf8');
495
- const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
496
- const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
497
- if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
498
- } catch {}
499
- return null;
500
- }
501
-
502
- function getGeminiOAuthCreds() {
503
- if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
504
- return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
505
- }
506
- const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
507
- try {
508
- const geminiPath = findCommand('gemini', rootDir);
509
- if (geminiPath) {
510
- const realPath = fs.realpathSync(geminiPath);
511
- const pkgRoot = path.resolve(path.dirname(realPath), '..');
512
- const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
513
- if (result) return result;
514
- }
515
- } catch (e) {
516
- console.error('[gemini-oauth] gemini lookup failed:', e.message);
517
- }
518
- try {
519
- const npmCacheDirs = new Set();
520
- const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
521
- addDir(path.join(os.homedir(), '.npm'));
522
- addDir(path.join(os.homedir(), '.cache', '.npm'));
523
- if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
524
- if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
525
- try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
526
- for (const cacheDir of npmCacheDirs) {
527
- if (!fs.existsSync(cacheDir)) continue;
528
- for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
529
- const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
530
- if (result) return result;
531
- }
532
- }
533
- } catch (e) {
534
- console.error('[gemini-oauth] npm cache scan failed:', e.message);
535
- }
536
- console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
537
- return null;
538
- }
539
- const GEMINI_DIR = path.join(os.homedir(), '.gemini');
540
- const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
541
- const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
542
-
543
- let geminiOAuthState = { status: 'idle', error: null, email: null };
544
- let geminiOAuthPending = null;
545
-
546
- function buildBaseUrl(req) {
547
- const override = process.env.AGENTGUI_BASE_URL;
548
- if (override) return override.replace(/\/+$/, '');
549
- const fwdProto = req.headers['x-forwarded-proto'];
550
- const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
551
- if (fwdHost) {
552
- const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
553
- const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
554
- return `${proto}://${cleanHost}`;
555
- }
556
- return `http://127.0.0.1:${PORT}`;
557
- }
558
-
559
- function saveGeminiCredentials(tokens, email) {
560
- if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
561
- fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
562
- try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
563
-
564
- let accounts = { active: null, old: [] };
565
- try {
566
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
567
- accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
568
- }
569
- } catch (_) {}
570
-
571
- if (email) {
572
- if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
573
- accounts.old.push(accounts.active);
574
- }
575
- accounts.active = email;
576
- }
577
- fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
578
- }
579
-
580
- function geminiOAuthResultPage(title, message, success) {
581
- const color = success ? '#10b981' : '#ef4444';
582
- const icon = success ? '✓' : '✗';
583
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
584
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
585
- <div style="text-align:center;max-width:400px;padding:2rem;">
586
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
587
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
588
- <p style="color:#9ca3af;">${message}</p>
589
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
590
- </div></body></html>`;
591
- }
592
-
593
- function encodeOAuthState(csrfToken, relayUrl) {
594
- const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
595
- return Buffer.from(payload).toString('base64url');
596
- }
597
-
598
- function decodeOAuthState(stateStr) {
599
- try {
600
- const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
601
- return { csrfToken: payload.t, relayUrl: payload.r };
602
- } catch (_) {
603
- return { csrfToken: stateStr, relayUrl: null };
604
- }
605
- }
606
-
607
- function geminiOAuthRelayPage(code, state, error) {
608
- const stateData = decodeOAuthState(state || '');
609
- const relayUrl = stateData.relayUrl || '';
610
- const escapedCode = (code || '').replace(/['"\\]/g, '');
611
- const escapedState = (state || '').replace(/['"\\]/g, '');
612
- const escapedError = (error || '').replace(/['"\\]/g, '');
613
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
614
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
615
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
616
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
617
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
618
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
619
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
620
- </div>
621
- <script>
622
- (function() {
623
- var code = '${escapedCode}';
624
- var state = '${escapedState}';
625
- var error = '${escapedError}';
626
- var relayUrl = '${escapedRelay}';
627
- function show(icon, title, msg, color) {
628
- document.getElementById('spinner').textContent = icon;
629
- document.getElementById('spinner').style.color = color;
630
- document.getElementById('title').textContent = title;
631
- document.getElementById('msg').textContent = msg;
632
- }
633
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
634
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
635
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
636
- fetch(relayUrl, {
637
- method: 'POST',
638
- headers: { 'Content-Type': 'application/json' },
639
- body: JSON.stringify({ code: code, state: state })
640
- }).then(function(r) { return r.json(); }).then(function(data) {
641
- if (data.success) {
642
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
643
- } else {
644
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
645
- }
646
- }).catch(function(e) {
647
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
648
- });
649
- })();
650
- </script>
651
- </body></html>`;
652
- }
653
-
654
- function isRemoteRequest(req) {
655
- return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
656
- }
657
-
658
- async function startGeminiOAuth(req) {
659
- const creds = getGeminiOAuthCreds();
660
- if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
661
-
662
- const useCustomClient = !!creds.custom;
663
- const remote = isRemoteRequest(req);
664
- let redirectUri;
665
- if (useCustomClient && req) {
666
- redirectUri = `${buildBaseUrl(req)}${BASE_URL}/oauth2callback`;
667
- } else {
668
- redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
669
- }
670
-
671
- const csrfToken = crypto.randomBytes(32).toString('hex');
672
- const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
673
- const state = encodeOAuthState(csrfToken, relayUrl);
674
-
675
- const client = new OAuth2Client({
676
- clientId: creds.clientId,
677
- clientSecret: creds.clientSecret,
678
- });
679
-
680
- const authUrl = client.generateAuthUrl({
681
- redirect_uri: redirectUri,
682
- access_type: 'offline',
683
- scope: GEMINI_SCOPES,
684
- state,
685
- });
686
-
687
- const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
688
- geminiOAuthPending = { client, redirectUri, state: csrfToken };
689
- geminiOAuthState = { status: 'pending', error: null, email: null };
690
-
691
- setTimeout(() => {
692
- if (geminiOAuthState.status === 'pending') {
693
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
694
- geminiOAuthPending = null;
695
- }
696
- }, 5 * 60 * 1000);
697
-
698
- return { authUrl, mode };
699
- }
700
-
701
- async function exchangeGeminiOAuthCode(code, stateParam) {
702
- if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
703
-
704
- const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
705
- const { csrfToken } = decodeOAuthState(stateParam);
706
-
707
- if (csrfToken !== expectedCsrf) {
708
- geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
709
- geminiOAuthPending = null;
710
- throw new Error('State mismatch - possible CSRF attack.');
711
- }
712
-
713
- if (!code) {
714
- geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
715
- geminiOAuthPending = null;
716
- throw new Error('No authorization code received.');
717
- }
718
-
719
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
720
- client.setCredentials(tokens);
721
-
722
- let email = '';
723
- try {
724
- const { token } = await client.getAccessToken();
725
- if (token) {
726
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
727
- headers: { Authorization: `Bearer ${token}` }
728
- });
729
- if (resp.ok) {
730
- const info = await resp.json();
731
- email = info.email || '';
732
- }
733
- }
734
- } catch (_) {}
735
-
736
- saveGeminiCredentials(tokens, email);
737
- geminiOAuthState = { status: 'success', error: null, email };
738
- geminiOAuthPending = null;
739
-
740
- return email;
741
- }
742
-
743
- async function handleGeminiOAuthCallback(req, res) {
744
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
745
- const code = reqUrl.searchParams.get('code');
746
- const state = reqUrl.searchParams.get('state');
747
- const error = reqUrl.searchParams.get('error');
748
- const errorDesc = reqUrl.searchParams.get('error_description');
749
-
750
- if (error) {
751
- const desc = errorDesc || error;
752
- geminiOAuthState = { status: 'error', error: desc, email: null };
753
- geminiOAuthPending = null;
754
- }
755
-
756
- const stateData = decodeOAuthState(state || '');
757
- if (stateData.relayUrl) {
758
- res.writeHead(200, { 'Content-Type': 'text/html' });
759
- res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
760
- return;
761
- }
762
-
763
- if (!geminiOAuthPending) {
764
- res.writeHead(200, { 'Content-Type': 'text/html' });
765
- res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
766
- return;
767
- }
768
-
769
- try {
770
- if (error) throw new Error(errorDesc || error);
771
- const email = await exchangeGeminiOAuthCode(code, state);
772
- res.writeHead(200, { 'Content-Type': 'text/html' });
773
- res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
774
- } catch (e) {
775
- res.writeHead(200, { 'Content-Type': 'text/html' });
776
- res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
777
- }
778
- }
779
-
780
- function getGeminiOAuthStatus() {
781
- try {
782
- if (fs.existsSync(GEMINI_OAUTH_FILE)) {
783
- const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
784
- if (creds.refresh_token || creds.access_token) {
785
- let email = '';
786
- try {
787
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
788
- const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
789
- email = accts.active || '';
790
- }
791
- } catch (_) {}
792
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
793
- }
794
- }
795
- } catch (_) {}
796
- return null;
797
- }
798
-
799
- const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
800
- const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
801
- const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
802
- const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
803
- const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
804
- const CODEX_OAUTH_PORT = 1455;
805
-
806
- let codexOAuthState = { status: 'idle', error: null, email: null };
807
- let codexOAuthPending = null;
808
-
809
- function generatePkce() {
810
- const verifierBytes = crypto.randomBytes(64);
811
- const codeVerifier = verifierBytes.toString('base64url');
812
- const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
813
- const codeChallenge = challengeBytes.toString('base64url');
814
- return { codeVerifier, codeChallenge };
815
- }
816
-
817
- function parseJwtEmail(jwt) {
818
- try {
819
- const parts = jwt.split('.');
820
- if (parts.length < 2) return '';
821
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
822
- return payload.email || payload['https://api.openai.com/profile']?.email || '';
823
- } catch (_) { return ''; }
824
- }
825
-
826
- function saveCodexCredentials(tokens) {
827
- if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
828
- const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
829
- fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
830
- try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
831
- }
832
-
833
- function getCodexOAuthStatus() {
834
- try {
835
- if (fs.existsSync(CODEX_AUTH_FILE)) {
836
- const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
837
- if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
838
- const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
839
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
840
- }
841
- }
842
- } catch (_) {}
843
- return null;
844
- }
845
-
846
- async function startCodexOAuth(req) {
847
- const remote = isRemoteRequest(req);
848
- const redirectUri = remote
849
- ? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
850
- : `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
851
-
852
- const pkce = generatePkce();
853
- const csrfToken = crypto.randomBytes(32).toString('hex');
854
- const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
855
- const state = encodeOAuthState(csrfToken, relayUrl);
856
-
857
- const params = new URLSearchParams({
858
- response_type: 'code',
859
- client_id: CODEX_CLIENT_ID,
860
- redirect_uri: redirectUri,
861
- scope: CODEX_SCOPES,
862
- code_challenge: pkce.codeChallenge,
863
- code_challenge_method: 'S256',
864
- id_token_add_organizations: 'true',
865
- codex_cli_simplified_flow: 'true',
866
- state,
867
- });
868
-
869
- const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
870
- const mode = remote ? 'remote' : 'local';
871
-
872
- codexOAuthPending = { pkce, redirectUri, state: csrfToken };
873
- codexOAuthState = { status: 'pending', error: null, email: null };
874
-
875
- setTimeout(() => {
876
- if (codexOAuthState.status === 'pending') {
877
- codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
878
- codexOAuthPending = null;
879
- }
880
- }, 5 * 60 * 1000);
881
-
882
- return { authUrl, mode };
883
- }
884
-
885
- async function exchangeCodexOAuthCode(code, stateParam) {
886
- if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
887
-
888
- const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
889
- const { csrfToken } = decodeOAuthState(stateParam);
890
-
891
- if (csrfToken !== expectedCsrf) {
892
- codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
893
- codexOAuthPending = null;
894
- throw new Error('State mismatch - possible CSRF attack.');
895
- }
896
-
897
- if (!code) {
898
- codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
899
- codexOAuthPending = null;
900
- throw new Error('No authorization code received.');
901
- }
902
-
903
- const body = new URLSearchParams({
904
- grant_type: 'authorization_code',
905
- code,
906
- redirect_uri: redirectUri,
907
- client_id: CODEX_CLIENT_ID,
908
- code_verifier: pkce.codeVerifier,
909
- });
910
-
911
- const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
912
- method: 'POST',
913
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
914
- body: body.toString(),
915
- });
916
-
917
- if (!resp.ok) {
918
- const text = await resp.text();
919
- codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
920
- codexOAuthPending = null;
921
- throw new Error(`Token exchange failed (${resp.status}): ${text}`);
922
- }
923
-
924
- const tokens = await resp.json();
925
- const email = parseJwtEmail(tokens.id_token || '');
926
-
927
- saveCodexCredentials(tokens);
928
- codexOAuthState = { status: 'success', error: null, email };
929
- codexOAuthPending = null;
930
-
931
- return email;
932
- }
933
-
934
- function codexOAuthResultPage(title, message, success) {
935
- const color = success ? '#10b981' : '#ef4444';
936
- const icon = success ? '&#10003;' : '&#10007;';
937
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
938
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
939
- <div style="text-align:center;max-width:400px;padding:2rem;">
940
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
941
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
942
- <p style="color:#9ca3af;">${message}</p>
943
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
944
- </div></body></html>`;
945
- }
946
-
947
- function codexOAuthRelayPage(code, state, error) {
948
- const stateData = decodeOAuthState(state || '');
949
- const relayUrl = stateData.relayUrl || '';
950
- const escapedCode = (code || '').replace(/['"\\]/g, '');
951
- const escapedState = (state || '').replace(/['"\\]/g, '');
952
- const escapedError = (error || '').replace(/['"\\]/g, '');
953
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
954
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
955
- <body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
956
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
957
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
958
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
959
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
960
- </div>
961
- <script>
962
- (function() {
963
- var code = '${escapedCode}';
964
- var state = '${escapedState}';
965
- var error = '${escapedError}';
966
- var relayUrl = '${escapedRelay}';
967
- function show(icon, title, msg, color) {
968
- document.getElementById('spinner').textContent = icon;
969
- document.getElementById('spinner').style.color = color;
970
- document.getElementById('title').textContent = title;
971
- document.getElementById('msg').textContent = msg;
972
- }
973
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
974
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
975
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
976
- fetch(relayUrl, {
977
- method: 'POST',
978
- headers: { 'Content-Type': 'application/json' },
979
- body: JSON.stringify({ code: code, state: state })
980
- }).then(function(r) { return r.json(); }).then(function(data) {
981
- if (data.success) {
982
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
983
- } else {
984
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
985
- }
986
- }).catch(function(e) {
987
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
988
- });
989
- })();
990
- </script>
991
- </body></html>`;
992
- }
993
-
994
- async function handleCodexOAuthCallback(req, res) {
995
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
996
- const code = reqUrl.searchParams.get('code');
997
- const state = reqUrl.searchParams.get('state');
998
- const error = reqUrl.searchParams.get('error');
999
- const errorDesc = reqUrl.searchParams.get('error_description');
1000
-
1001
- if (error) {
1002
- const desc = errorDesc || error;
1003
- codexOAuthState = { status: 'error', error: desc, email: null };
1004
- codexOAuthPending = null;
1005
- }
1006
-
1007
- const stateData = decodeOAuthState(state || '');
1008
- if (stateData.relayUrl) {
1009
- res.writeHead(200, { 'Content-Type': 'text/html' });
1010
- res.end(codexOAuthRelayPage(code, state, errorDesc || error));
1011
- return;
1012
- }
1013
-
1014
- if (!codexOAuthPending) {
1015
- res.writeHead(200, { 'Content-Type': 'text/html' });
1016
- res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
1017
- return;
1018
- }
1019
-
1020
- try {
1021
- if (error) throw new Error(errorDesc || error);
1022
- const email = await exchangeCodexOAuthCode(code, state);
1023
- res.writeHead(200, { 'Content-Type': 'text/html' });
1024
- res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
1025
- } catch (e) {
1026
- res.writeHead(200, { 'Content-Type': 'text/html' });
1027
- res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
1028
- }
1029
- }
1030
-
1031
262
  const PROVIDER_CONFIGS = {
1032
263
  'anthropic': {
1033
264
  name: 'Anthropic', configPaths: [
@@ -1281,12 +512,12 @@ const server = http.createServer(async (req, res) => {
1281
512
  const pathOnly = routePath.split('?')[0];
1282
513
 
1283
514
  if (pathOnly === '/oauth2callback' && req.method === 'GET') {
1284
- await handleGeminiOAuthCallback(req, res);
515
+ await handleGeminiOAuthCallback(req, res, PORT);
1285
516
  return;
1286
517
  }
1287
518
 
1288
519
  if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
1289
- await handleCodexOAuthCallback(req, res);
520
+ await handleCodexOAuthCallback(req, res, PORT);
1290
521
  return;
1291
522
  }
1292
523
 
@@ -2746,7 +1977,7 @@ const server = http.createServer(async (req, res) => {
2746
1977
 
2747
1978
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
2748
1979
  try {
2749
- const result = await startGeminiOAuth(req);
1980
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
2750
1981
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2751
1982
  } catch (e) {
2752
1983
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -2756,7 +1987,7 @@ const server = http.createServer(async (req, res) => {
2756
1987
  }
2757
1988
 
2758
1989
  if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
2759
- sendJSON(req, res, 200, geminiOAuthState);
1990
+ sendJSON(req, res, 200, getGeminiOAuthState());
2760
1991
  return;
2761
1992
  }
2762
1993
 
@@ -2771,8 +2002,6 @@ const server = http.createServer(async (req, res) => {
2771
2002
  const email = await exchangeGeminiOAuthCode(code, stateParam);
2772
2003
  sendJSON(req, res, 200, { success: true, email });
2773
2004
  } catch (e) {
2774
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2775
- geminiOAuthPending = null;
2776
2005
  sendJSON(req, res, 400, { error: e.message });
2777
2006
  }
2778
2007
  return;
@@ -2796,8 +2025,6 @@ const server = http.createServer(async (req, res) => {
2796
2025
  const error = parsed.searchParams.get('error');
2797
2026
  if (error) {
2798
2027
  const desc = parsed.searchParams.get('error_description') || error;
2799
- geminiOAuthState = { status: 'error', error: desc, email: null };
2800
- geminiOAuthPending = null;
2801
2028
  sendJSON(req, res, 200, { error: desc });
2802
2029
  return;
2803
2030
  }
@@ -2807,8 +2034,6 @@ const server = http.createServer(async (req, res) => {
2807
2034
  const email = await exchangeGeminiOAuthCode(code, state);
2808
2035
  sendJSON(req, res, 200, { success: true, email });
2809
2036
  } catch (e) {
2810
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2811
- geminiOAuthPending = null;
2812
2037
  sendJSON(req, res, 400, { error: e.message });
2813
2038
  }
2814
2039
  return;
@@ -2816,7 +2041,7 @@ const server = http.createServer(async (req, res) => {
2816
2041
 
2817
2042
  if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
2818
2043
  try {
2819
- const result = await startCodexOAuth(req);
2044
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
2820
2045
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2821
2046
  } catch (e) {
2822
2047
  console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
@@ -2826,7 +2051,7 @@ const server = http.createServer(async (req, res) => {
2826
2051
  }
2827
2052
 
2828
2053
  if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
2829
- sendJSON(req, res, 200, codexOAuthState);
2054
+ sendJSON(req, res, 200, getCodexOAuthState());
2830
2055
  return;
2831
2056
  }
2832
2057
 
@@ -2841,8 +2066,6 @@ const server = http.createServer(async (req, res) => {
2841
2066
  const email = await exchangeCodexOAuthCode(code, stateParam);
2842
2067
  sendJSON(req, res, 200, { success: true, email });
2843
2068
  } catch (e) {
2844
- codexOAuthState = { status: 'error', error: e.message, email: null };
2845
- codexOAuthPending = null;
2846
2069
  sendJSON(req, res, 400, { error: e.message });
2847
2070
  }
2848
2071
  return;
@@ -2864,8 +2087,6 @@ const server = http.createServer(async (req, res) => {
2864
2087
  const error = parsed.searchParams.get('error');
2865
2088
  if (error) {
2866
2089
  const desc = parsed.searchParams.get('error_description') || error;
2867
- codexOAuthState = { status: 'error', error: desc, email: null };
2868
- codexOAuthPending = null;
2869
2090
  sendJSON(req, res, 200, { error: desc });
2870
2091
  return;
2871
2092
  }
@@ -2874,8 +2095,6 @@ const server = http.createServer(async (req, res) => {
2874
2095
  const email = await exchangeCodexOAuthCode(code, state);
2875
2096
  sendJSON(req, res, 200, { success: true, email });
2876
2097
  } catch (e) {
2877
- codexOAuthState = { status: 'error', error: e.message, email: null };
2878
- codexOAuthPending = null;
2879
2098
  sendJSON(req, res, 400, { error: e.message });
2880
2099
  }
2881
2100
  return;
@@ -2889,21 +2108,21 @@ const server = http.createServer(async (req, res) => {
2889
2108
 
2890
2109
  if (agentId === 'codex' || agentId === 'cli-codex') {
2891
2110
  try {
2892
- const result = await startCodexOAuth(req);
2111
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
2893
2112
  const conversationId = '__agent_auth__';
2894
2113
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
2895
2114
  broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening OpenAI OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
2896
2115
 
2897
2116
  const pollId = setInterval(() => {
2898
- if (codexOAuthState.status === 'success') {
2117
+ if (getCodexOAuthState().status === 'success') {
2899
2118
  clearInterval(pollId);
2900
- const email = codexOAuthState.email || '';
2119
+ const email = getCodexOAuthState().email || '';
2901
2120
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
2902
2121
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2903
- } else if (codexOAuthState.status === 'error') {
2122
+ } else if (getCodexOAuthState().status === 'error') {
2904
2123
  clearInterval(pollId);
2905
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2906
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexOAuthState.error, timestamp: Date.now() });
2124
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2125
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
2907
2126
  }
2908
2127
  }, 1000);
2909
2128
 
@@ -2920,21 +2139,21 @@ const server = http.createServer(async (req, res) => {
2920
2139
 
2921
2140
  if (agentId === 'gemini') {
2922
2141
  try {
2923
- const result = await startGeminiOAuth(req);
2142
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
2924
2143
  const conversationId = '__agent_auth__';
2925
2144
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
2926
2145
  broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening Google OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
2927
2146
 
2928
2147
  const pollId = setInterval(() => {
2929
- if (geminiOAuthState.status === 'success') {
2148
+ if (getGeminiOAuthState().status === 'success') {
2930
2149
  clearInterval(pollId);
2931
- const email = geminiOAuthState.email || '';
2150
+ const email = getGeminiOAuthState().email || '';
2932
2151
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
2933
2152
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2934
- } else if (geminiOAuthState.status === 'error') {
2153
+ } else if (getGeminiOAuthState().status === 'error') {
2935
2154
  clearInterval(pollId);
2936
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${geminiOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2937
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: geminiOAuthState.error, timestamp: Date.now() });
2155
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2156
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
2938
2157
  }
2939
2158
  }, 1000);
2940
2159
 
@@ -4499,6 +3718,8 @@ function broadcastSync(event) {
4499
3718
  // WebSocket protocol router
4500
3719
  const wsRouter = new WsRouter();
4501
3720
 
3721
+ initSpeechManager({ broadcastSync, syncClients, queries });
3722
+
4502
3723
  registerConvHandlers(wsRouter, {
4503
3724
  queries, activeExecutions, rateLimitState,
4504
3725
  broadcastSync, processMessageWithStreaming, cleanupExecution,
@@ -4516,7 +3737,7 @@ console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.leng
4516
3737
  registerSessionHandlers(wsRouter, {
4517
3738
  db: queries, discoveredAgents, modelCache,
4518
3739
  getAgentDescriptor, activeScripts, broadcastSync,
4519
- startGeminiOAuth, geminiOAuthState: () => geminiOAuthState
3740
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
4520
3741
  });
4521
3742
  console.log('[INIT] registerSessionHandlers completed');
4522
3743
 
@@ -4536,10 +3757,12 @@ registerScriptHandlers(wsRouter, {
4536
3757
  });
4537
3758
 
4538
3759
  registerOAuthHandlers(wsRouter, {
4539
- startGeminiOAuth, exchangeGeminiOAuthCode,
4540
- geminiOAuthState: () => geminiOAuthState,
4541
- startCodexOAuth, exchangeCodexOAuthCode,
4542
- codexOAuthState: () => codexOAuthState,
3760
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
3761
+ exchangeGeminiOAuthCode,
3762
+ geminiOAuthState: getGeminiOAuthState,
3763
+ startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
3764
+ exchangeCodexOAuthCode,
3765
+ codexOAuthState: getCodexOAuthState,
4543
3766
  });
4544
3767
 
4545
3768
  wsRouter.onLegacy((data, ws) => {