agentgui 1.0.751 → 1.0.753

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,13 +10,15 @@ 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';
17
16
  import { queries } from './database.js';
18
17
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
19
18
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
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 { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
20
22
  import { WSOptimizer } from './lib/ws-optimizer.js';
21
23
  import { WsRouter } from './lib/ws-protocol.js';
22
24
  import { encode as wsEncode } from './lib/codec.js';
@@ -49,14 +51,20 @@ process.on('unhandledRejection', (reason, promise) => {
49
51
  if (reason instanceof Error) console.error(reason.stack);
50
52
  });
51
53
 
52
- process.on('SIGINT', () => {
53
- console.log('[SIGNAL] SIGINT received - graceful shutdown');
54
+ function gracefulShutdown(signal) {
55
+ console.log(`[SIGNAL] ${signal} received - graceful shutdown`);
54
56
  try { pm2Manager.disconnect(); } catch (_) {}
55
57
  if (jsonlWatcher) try { jsonlWatcher.stop(); } catch (_) {}
58
+ for (const [convId, entry] of activeExecutions) {
59
+ try { if (entry.pid) process.kill(entry.pid, 'SIGTERM'); } catch (_) {}
60
+ }
56
61
  stopACPTools().catch(() => {}).finally(() => {
57
62
  try { wss.close(() => server.close(() => process.exit(0))); } catch (_) { process.exit(0); }
63
+ setTimeout(() => process.exit(1), 5000);
58
64
  });
59
- });
65
+ }
66
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
67
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
60
68
  process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - uncrashable)'); });
61
69
  process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
62
70
  process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
@@ -444,180 +452,12 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
444
452
  router(req, res, next);
445
453
  });
446
454
 
447
- function findCommand(cmd) {
448
- const isWindows = os.platform() === 'win32';
449
- const localBin = path.join(path.dirname(fileURLToPath(import.meta.url)), 'node_modules', '.bin', isWindows ? cmd + '.cmd' : cmd);
450
- if (fs.existsSync(localBin)) {
451
- console.log(`[agent-discovery] Found ${cmd} in local node_modules`);
452
- return localBin;
453
- }
454
- try {
455
- // Increase timeout to 10 seconds to handle slower systems and running agents
456
- const timeoutMs = 10000;
457
- if (isWindows) {
458
- const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
459
- if (result) {
460
- console.log(`[agent-discovery] Found ${cmd} in PATH`);
461
- return result.split('\n')[0].trim();
462
- }
463
- } else {
464
- const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: timeoutMs }).trim();
465
- if (result) {
466
- console.log(`[agent-discovery] Found ${cmd} in PATH`);
467
- return result;
468
- }
469
- }
470
- } catch (err) {
471
- console.log(`[agent-discovery] ${cmd} not found or timed out`);
472
- return null;
473
- }
474
- return null;
475
- }
476
-
477
- async function queryACPServerAgents(baseUrl) {
478
- const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
479
- try {
480
- const response = await fetch(endpoint, {
481
- method: 'POST',
482
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
483
- body: JSON.stringify({}),
484
- signal: AbortSignal.timeout(5000)
485
- });
486
- if (!response.ok) {
487
- console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
488
- return [];
489
- }
490
- const data = await response.json();
491
- if (!data?.agents || !Array.isArray(data.agents)) {
492
- console.error(`Invalid agents response from ${baseUrl}`);
493
- return [];
494
- }
495
- return data.agents.map(agent => ({
496
- id: agent.agent_id || agent.id,
497
- name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
498
- metadata: {
499
- ref: {
500
- name: agent.metadata?.ref?.name,
501
- version: agent.metadata?.ref?.version,
502
- url: agent.metadata?.ref?.url,
503
- tags: agent.metadata?.ref?.tags
504
- },
505
- description: agent.metadata?.description,
506
- author: agent.metadata?.author,
507
- license: agent.metadata?.license
508
- },
509
- specs: agent.specs ? {
510
- capabilities: agent.specs.capabilities,
511
- input_schema: agent.specs.input_schema || agent.specs.input,
512
- output_schema: agent.specs.output_schema || agent.specs.output,
513
- thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
514
- config_schema: agent.specs.config_schema || agent.specs.config,
515
- custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
516
- } : null,
517
- custom_data: agent.custom_data,
518
- icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
519
- protocol: 'acp',
520
- path: baseUrl
521
- }));
522
- } catch (error) {
523
- console.error(`ACP agents query failed for ${baseUrl}: ${error.message}`);
524
- return [];
525
- }
526
- }
527
-
528
- function discoverAgents() {
529
- const agents = [];
530
- const binaries = [
531
- { cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C', protocol: 'cli' },
532
- { cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O', protocol: 'acp', npxPackage: 'opencode-ai' },
533
- { cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G', protocol: 'acp', npxPackage: '@google/gemini-cli' },
534
- { cmd: 'kilo', id: 'kilo', name: 'Kilo Code', icon: 'K', protocol: 'acp', npxPackage: '@kilocode/cli' },
535
- { cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g', protocol: 'acp' },
536
- { cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H', protocol: 'acp' },
537
- { cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A', protocol: 'acp' },
538
- { cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c', protocol: 'acp' },
539
- { cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K', protocol: 'acp' },
540
- { cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q', protocol: 'acp' },
541
- { cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X', protocol: 'acp', npxPackage: '@openai/codex' },
542
- { cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M', protocol: 'acp' },
543
- { cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k', protocol: 'acp' },
544
- { cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F', protocol: 'acp' },
545
- ];
546
- for (const bin of binaries) {
547
- const result = findCommand(bin.cmd);
548
- if (result) {
549
- agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result, protocol: bin.protocol });
550
- } else if (bin.npxPackage) {
551
- // For npx-launchable packages (including claude-code as fallback)
552
- agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: bin.npxPackage, npxLaunchable: true });
553
- } else if (bin.id === 'claude-code') {
554
- // Ensure Claude Code is always available as an npx-launchable agent
555
- agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: null, protocol: bin.protocol, npxPackage: '@anthropic-ai/claude-code', npxLaunchable: true });
556
- }
557
- }
558
-
559
- // Add CLI tool wrappers for ACP agents (these map CLI commands to plugin sub-agents)
560
- // These allow users to select "OpenCode", "Gemini", etc. and then pick a model/variant
561
- const cliWrappers = [
562
- { id: 'cli-opencode', name: 'OpenCode', icon: 'O', protocol: 'cli-wrapper', acpId: 'opencode' },
563
- { id: 'cli-gemini', name: 'Gemini', icon: 'G', protocol: 'cli-wrapper', acpId: 'gemini' },
564
- { id: 'cli-kilo', name: 'Kilo', icon: 'K', protocol: 'cli-wrapper', acpId: 'kilo' },
565
- { id: 'cli-codex', name: 'Codex', icon: 'X', protocol: 'cli-wrapper', acpId: 'codex' },
566
- ];
567
-
568
- // Only add CLI wrappers for agents that are already discovered
569
- console.log('[discoverAgents] Found agents:', agents.map(a => a.id).join(', '));
570
- for (const wrapper of cliWrappers) {
571
- if (agents.some(a => a.id === wrapper.acpId)) {
572
- console.log(`[discoverAgents] Adding CLI wrapper for ${wrapper.id}`);
573
- agents.push(wrapper);
574
- console.log(`[discoverAgents] After push, agents.length = ${agents.length}, includes ${wrapper.id}? ${agents.some(a => a.id === wrapper.id)}`);
575
- } else {
576
- console.log(`[discoverAgents] Skipping CLI wrapper ${wrapper.id} (ACP agent ${wrapper.acpId} not found)`);
577
- }
578
- }
579
- // Remove raw ACP agents that have a cli-wrapper (avoid duplicates in UI)
580
- const wrappedAcpIds = new Set(cliWrappers.filter(w => agents.some(a => a.id === w.acpId)).map(w => w.acpId));
581
- const filtered = agents.filter(a => !wrappedAcpIds.has(a.id));
582
- console.log('[discoverAgents] Final agent count:', filtered.length, 'Agent IDs:', filtered.map(a => a.id).join(', '));
583
-
584
- return filtered;
585
- }
586
-
587
- // Function to discover agents from external ACP servers
588
- async function discoverExternalACPServers() {
589
- const externalAgents = [];
590
- for (const agent of discoveredAgents.filter(a => a.protocol === 'acp' && a.acpPort)) {
591
- try {
592
- const agents = await queryACPServerAgents(`http://localhost:${agent.acpPort}`);
593
- externalAgents.push(...agents);
594
- } catch (_) {}
595
- }
596
- return externalAgents;
597
- }
598
-
599
455
  let discoveredAgents = [];
600
456
  initializeDescriptors(discoveredAgents);
601
457
 
602
- // Agent discovery happens asynchronously in background to not block startup
603
- async function initializeAgentDiscovery() {
604
- try {
605
- const agents = discoverAgents();
606
- // Mutate the existing array instead of reassigning to preserve closure references in handlers
607
- discoveredAgents.length = 0;
608
- discoveredAgents.push(...agents);
609
- initializeDescriptors(discoveredAgents);
610
- console.log('[AGENTS] Discovered:', discoveredAgents.map(a => ({ id: a.id, found: !!a.path, protocol: a.protocol })));
611
- console.log('[AGENTS] Total count:', discoveredAgents.length);
612
- } catch (err) {
613
- console.error('[AGENTS] Discovery error:', err.message);
614
- logError('initializeAgentDiscovery', err);
615
- }
616
- }
617
-
618
- // Start immediately but don't wait for it
619
458
  const startTime = Date.now();
620
- initializeAgentDiscovery().then(() => {
459
+ initializeAgentDiscovery(discoveredAgents, rootDir, logError).then(() => {
460
+ initializeDescriptors(discoveredAgents);
621
461
  console.log('[INIT] initializeAgentDiscovery completed in', Date.now() - startTime, 'ms');
622
462
  }).catch(() => {});
623
463
 
@@ -644,551 +484,6 @@ async function getModelsForAgent(agentId) {
644
484
  return models;
645
485
  }
646
486
 
647
- const GEMINI_SCOPES = [
648
- 'https://www.googleapis.com/auth/cloud-platform',
649
- 'https://www.googleapis.com/auth/userinfo.email',
650
- 'https://www.googleapis.com/auth/userinfo.profile',
651
- ];
652
-
653
- function extractOAuthFromFile(oauth2Path) {
654
- try {
655
- const src = fs.readFileSync(oauth2Path, 'utf8');
656
- const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
657
- const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
658
- if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
659
- } catch {}
660
- return null;
661
- }
662
-
663
- function getGeminiOAuthCreds() {
664
- if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
665
- return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
666
- }
667
- const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
668
- try {
669
- const geminiPath = findCommand('gemini');
670
- if (geminiPath) {
671
- const realPath = fs.realpathSync(geminiPath);
672
- const pkgRoot = path.resolve(path.dirname(realPath), '..');
673
- const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
674
- if (result) return result;
675
- }
676
- } catch (e) {
677
- console.error('[gemini-oauth] gemini lookup failed:', e.message);
678
- }
679
- try {
680
- const npmCacheDirs = new Set();
681
- const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
682
- addDir(path.join(os.homedir(), '.npm'));
683
- addDir(path.join(os.homedir(), '.cache', '.npm'));
684
- if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
685
- if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
686
- try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
687
- for (const cacheDir of npmCacheDirs) {
688
- if (!fs.existsSync(cacheDir)) continue;
689
- for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
690
- const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
691
- if (result) return result;
692
- }
693
- }
694
- } catch (e) {
695
- console.error('[gemini-oauth] npm cache scan failed:', e.message);
696
- }
697
- console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
698
- return null;
699
- }
700
- const GEMINI_DIR = path.join(os.homedir(), '.gemini');
701
- const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
702
- const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
703
-
704
- let geminiOAuthState = { status: 'idle', error: null, email: null };
705
- let geminiOAuthPending = null;
706
-
707
- function buildBaseUrl(req) {
708
- const override = process.env.AGENTGUI_BASE_URL;
709
- if (override) return override.replace(/\/+$/, '');
710
- const fwdProto = req.headers['x-forwarded-proto'];
711
- const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
712
- if (fwdHost) {
713
- const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
714
- const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
715
- return `${proto}://${cleanHost}`;
716
- }
717
- return `http://127.0.0.1:${PORT}`;
718
- }
719
-
720
- function saveGeminiCredentials(tokens, email) {
721
- if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
722
- fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
723
- try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
724
-
725
- let accounts = { active: null, old: [] };
726
- try {
727
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
728
- accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
729
- }
730
- } catch (_) {}
731
-
732
- if (email) {
733
- if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
734
- accounts.old.push(accounts.active);
735
- }
736
- accounts.active = email;
737
- }
738
- fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
739
- }
740
-
741
- function geminiOAuthResultPage(title, message, success) {
742
- const color = success ? '#10b981' : '#ef4444';
743
- const icon = success ? '✓' : '✗';
744
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
745
- <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;">
746
- <div style="text-align:center;max-width:400px;padding:2rem;">
747
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
748
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
749
- <p style="color:#9ca3af;">${message}</p>
750
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
751
- </div></body></html>`;
752
- }
753
-
754
- function encodeOAuthState(csrfToken, relayUrl) {
755
- const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
756
- return Buffer.from(payload).toString('base64url');
757
- }
758
-
759
- function decodeOAuthState(stateStr) {
760
- try {
761
- const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
762
- return { csrfToken: payload.t, relayUrl: payload.r };
763
- } catch (_) {
764
- return { csrfToken: stateStr, relayUrl: null };
765
- }
766
- }
767
-
768
- function geminiOAuthRelayPage(code, state, error) {
769
- const stateData = decodeOAuthState(state || '');
770
- const relayUrl = stateData.relayUrl || '';
771
- const escapedCode = (code || '').replace(/['"\\]/g, '');
772
- const escapedState = (state || '').replace(/['"\\]/g, '');
773
- const escapedError = (error || '').replace(/['"\\]/g, '');
774
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
775
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
776
- <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;">
777
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
778
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
779
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
780
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
781
- </div>
782
- <script>
783
- (function() {
784
- var code = '${escapedCode}';
785
- var state = '${escapedState}';
786
- var error = '${escapedError}';
787
- var relayUrl = '${escapedRelay}';
788
- function show(icon, title, msg, color) {
789
- document.getElementById('spinner').textContent = icon;
790
- document.getElementById('spinner').style.color = color;
791
- document.getElementById('title').textContent = title;
792
- document.getElementById('msg').textContent = msg;
793
- }
794
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
795
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
796
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
797
- fetch(relayUrl, {
798
- method: 'POST',
799
- headers: { 'Content-Type': 'application/json' },
800
- body: JSON.stringify({ code: code, state: state })
801
- }).then(function(r) { return r.json(); }).then(function(data) {
802
- if (data.success) {
803
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
804
- } else {
805
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
806
- }
807
- }).catch(function(e) {
808
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
809
- });
810
- })();
811
- </script>
812
- </body></html>`;
813
- }
814
-
815
- function isRemoteRequest(req) {
816
- return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
817
- }
818
-
819
- async function startGeminiOAuth(req) {
820
- const creds = getGeminiOAuthCreds();
821
- if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
822
-
823
- const useCustomClient = !!creds.custom;
824
- const remote = isRemoteRequest(req);
825
- let redirectUri;
826
- if (useCustomClient && req) {
827
- redirectUri = `${buildBaseUrl(req)}${BASE_URL}/oauth2callback`;
828
- } else {
829
- redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
830
- }
831
-
832
- const csrfToken = crypto.randomBytes(32).toString('hex');
833
- const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
834
- const state = encodeOAuthState(csrfToken, relayUrl);
835
-
836
- const client = new OAuth2Client({
837
- clientId: creds.clientId,
838
- clientSecret: creds.clientSecret,
839
- });
840
-
841
- const authUrl = client.generateAuthUrl({
842
- redirect_uri: redirectUri,
843
- access_type: 'offline',
844
- scope: GEMINI_SCOPES,
845
- state,
846
- });
847
-
848
- const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
849
- geminiOAuthPending = { client, redirectUri, state: csrfToken };
850
- geminiOAuthState = { status: 'pending', error: null, email: null };
851
-
852
- setTimeout(() => {
853
- if (geminiOAuthState.status === 'pending') {
854
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
855
- geminiOAuthPending = null;
856
- }
857
- }, 5 * 60 * 1000);
858
-
859
- return { authUrl, mode };
860
- }
861
-
862
- async function exchangeGeminiOAuthCode(code, stateParam) {
863
- if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
864
-
865
- const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
866
- const { csrfToken } = decodeOAuthState(stateParam);
867
-
868
- if (csrfToken !== expectedCsrf) {
869
- geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
870
- geminiOAuthPending = null;
871
- throw new Error('State mismatch - possible CSRF attack.');
872
- }
873
-
874
- if (!code) {
875
- geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
876
- geminiOAuthPending = null;
877
- throw new Error('No authorization code received.');
878
- }
879
-
880
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
881
- client.setCredentials(tokens);
882
-
883
- let email = '';
884
- try {
885
- const { token } = await client.getAccessToken();
886
- if (token) {
887
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
888
- headers: { Authorization: `Bearer ${token}` }
889
- });
890
- if (resp.ok) {
891
- const info = await resp.json();
892
- email = info.email || '';
893
- }
894
- }
895
- } catch (_) {}
896
-
897
- saveGeminiCredentials(tokens, email);
898
- geminiOAuthState = { status: 'success', error: null, email };
899
- geminiOAuthPending = null;
900
-
901
- return email;
902
- }
903
-
904
- async function handleGeminiOAuthCallback(req, res) {
905
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
906
- const code = reqUrl.searchParams.get('code');
907
- const state = reqUrl.searchParams.get('state');
908
- const error = reqUrl.searchParams.get('error');
909
- const errorDesc = reqUrl.searchParams.get('error_description');
910
-
911
- if (error) {
912
- const desc = errorDesc || error;
913
- geminiOAuthState = { status: 'error', error: desc, email: null };
914
- geminiOAuthPending = null;
915
- }
916
-
917
- const stateData = decodeOAuthState(state || '');
918
- if (stateData.relayUrl) {
919
- res.writeHead(200, { 'Content-Type': 'text/html' });
920
- res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
921
- return;
922
- }
923
-
924
- if (!geminiOAuthPending) {
925
- res.writeHead(200, { 'Content-Type': 'text/html' });
926
- res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
927
- return;
928
- }
929
-
930
- try {
931
- if (error) throw new Error(errorDesc || error);
932
- const email = await exchangeGeminiOAuthCode(code, state);
933
- res.writeHead(200, { 'Content-Type': 'text/html' });
934
- res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
935
- } catch (e) {
936
- res.writeHead(200, { 'Content-Type': 'text/html' });
937
- res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
938
- }
939
- }
940
-
941
- function getGeminiOAuthStatus() {
942
- try {
943
- if (fs.existsSync(GEMINI_OAUTH_FILE)) {
944
- const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
945
- if (creds.refresh_token || creds.access_token) {
946
- let email = '';
947
- try {
948
- if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
949
- const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
950
- email = accts.active || '';
951
- }
952
- } catch (_) {}
953
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
954
- }
955
- }
956
- } catch (_) {}
957
- return null;
958
- }
959
-
960
- const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
961
- const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
962
- const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
963
- const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
964
- const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
965
- const CODEX_OAUTH_PORT = 1455;
966
-
967
- let codexOAuthState = { status: 'idle', error: null, email: null };
968
- let codexOAuthPending = null;
969
-
970
- function generatePkce() {
971
- const verifierBytes = crypto.randomBytes(64);
972
- const codeVerifier = verifierBytes.toString('base64url');
973
- const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
974
- const codeChallenge = challengeBytes.toString('base64url');
975
- return { codeVerifier, codeChallenge };
976
- }
977
-
978
- function parseJwtEmail(jwt) {
979
- try {
980
- const parts = jwt.split('.');
981
- if (parts.length < 2) return '';
982
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
983
- return payload.email || payload['https://api.openai.com/profile']?.email || '';
984
- } catch (_) { return ''; }
985
- }
986
-
987
- function saveCodexCredentials(tokens) {
988
- if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
989
- const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
990
- fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
991
- try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
992
- }
993
-
994
- function getCodexOAuthStatus() {
995
- try {
996
- if (fs.existsSync(CODEX_AUTH_FILE)) {
997
- const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
998
- if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
999
- const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
1000
- return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
1001
- }
1002
- }
1003
- } catch (_) {}
1004
- return null;
1005
- }
1006
-
1007
- async function startCodexOAuth(req) {
1008
- const remote = isRemoteRequest(req);
1009
- const redirectUri = remote
1010
- ? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
1011
- : `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
1012
-
1013
- const pkce = generatePkce();
1014
- const csrfToken = crypto.randomBytes(32).toString('hex');
1015
- const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
1016
- const state = encodeOAuthState(csrfToken, relayUrl);
1017
-
1018
- const params = new URLSearchParams({
1019
- response_type: 'code',
1020
- client_id: CODEX_CLIENT_ID,
1021
- redirect_uri: redirectUri,
1022
- scope: CODEX_SCOPES,
1023
- code_challenge: pkce.codeChallenge,
1024
- code_challenge_method: 'S256',
1025
- id_token_add_organizations: 'true',
1026
- codex_cli_simplified_flow: 'true',
1027
- state,
1028
- });
1029
-
1030
- const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
1031
- const mode = remote ? 'remote' : 'local';
1032
-
1033
- codexOAuthPending = { pkce, redirectUri, state: csrfToken };
1034
- codexOAuthState = { status: 'pending', error: null, email: null };
1035
-
1036
- setTimeout(() => {
1037
- if (codexOAuthState.status === 'pending') {
1038
- codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
1039
- codexOAuthPending = null;
1040
- }
1041
- }, 5 * 60 * 1000);
1042
-
1043
- return { authUrl, mode };
1044
- }
1045
-
1046
- async function exchangeCodexOAuthCode(code, stateParam) {
1047
- if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
1048
-
1049
- const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
1050
- const { csrfToken } = decodeOAuthState(stateParam);
1051
-
1052
- if (csrfToken !== expectedCsrf) {
1053
- codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
1054
- codexOAuthPending = null;
1055
- throw new Error('State mismatch - possible CSRF attack.');
1056
- }
1057
-
1058
- if (!code) {
1059
- codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
1060
- codexOAuthPending = null;
1061
- throw new Error('No authorization code received.');
1062
- }
1063
-
1064
- const body = new URLSearchParams({
1065
- grant_type: 'authorization_code',
1066
- code,
1067
- redirect_uri: redirectUri,
1068
- client_id: CODEX_CLIENT_ID,
1069
- code_verifier: pkce.codeVerifier,
1070
- });
1071
-
1072
- const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
1073
- method: 'POST',
1074
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1075
- body: body.toString(),
1076
- });
1077
-
1078
- if (!resp.ok) {
1079
- const text = await resp.text();
1080
- codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
1081
- codexOAuthPending = null;
1082
- throw new Error(`Token exchange failed (${resp.status}): ${text}`);
1083
- }
1084
-
1085
- const tokens = await resp.json();
1086
- const email = parseJwtEmail(tokens.id_token || '');
1087
-
1088
- saveCodexCredentials(tokens);
1089
- codexOAuthState = { status: 'success', error: null, email };
1090
- codexOAuthPending = null;
1091
-
1092
- return email;
1093
- }
1094
-
1095
- function codexOAuthResultPage(title, message, success) {
1096
- const color = success ? '#10b981' : '#ef4444';
1097
- const icon = success ? '&#10003;' : '&#10007;';
1098
- return `<!DOCTYPE html><html><head><title>${title}</title></head>
1099
- <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;">
1100
- <div style="text-align:center;max-width:400px;padding:2rem;">
1101
- <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
1102
- <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
1103
- <p style="color:#9ca3af;">${message}</p>
1104
- <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
1105
- </div></body></html>`;
1106
- }
1107
-
1108
- function codexOAuthRelayPage(code, state, error) {
1109
- const stateData = decodeOAuthState(state || '');
1110
- const relayUrl = stateData.relayUrl || '';
1111
- const escapedCode = (code || '').replace(/['"\\]/g, '');
1112
- const escapedState = (state || '').replace(/['"\\]/g, '');
1113
- const escapedError = (error || '').replace(/['"\\]/g, '');
1114
- const escapedRelay = relayUrl.replace(/['"\\]/g, '');
1115
- return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
1116
- <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;">
1117
- <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
1118
- <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
1119
- <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
1120
- <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
1121
- </div>
1122
- <script>
1123
- (function() {
1124
- var code = '${escapedCode}';
1125
- var state = '${escapedState}';
1126
- var error = '${escapedError}';
1127
- var relayUrl = '${escapedRelay}';
1128
- function show(icon, title, msg, color) {
1129
- document.getElementById('spinner').textContent = icon;
1130
- document.getElementById('spinner').style.color = color;
1131
- document.getElementById('title').textContent = title;
1132
- document.getElementById('msg').textContent = msg;
1133
- }
1134
- if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
1135
- if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
1136
- if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
1137
- fetch(relayUrl, {
1138
- method: 'POST',
1139
- headers: { 'Content-Type': 'application/json' },
1140
- body: JSON.stringify({ code: code, state: state })
1141
- }).then(function(r) { return r.json(); }).then(function(data) {
1142
- if (data.success) {
1143
- show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
1144
- } else {
1145
- show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
1146
- }
1147
- }).catch(function(e) {
1148
- show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
1149
- });
1150
- })();
1151
- </script>
1152
- </body></html>`;
1153
- }
1154
-
1155
- async function handleCodexOAuthCallback(req, res) {
1156
- const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
1157
- const code = reqUrl.searchParams.get('code');
1158
- const state = reqUrl.searchParams.get('state');
1159
- const error = reqUrl.searchParams.get('error');
1160
- const errorDesc = reqUrl.searchParams.get('error_description');
1161
-
1162
- if (error) {
1163
- const desc = errorDesc || error;
1164
- codexOAuthState = { status: 'error', error: desc, email: null };
1165
- codexOAuthPending = null;
1166
- }
1167
-
1168
- const stateData = decodeOAuthState(state || '');
1169
- if (stateData.relayUrl) {
1170
- res.writeHead(200, { 'Content-Type': 'text/html' });
1171
- res.end(codexOAuthRelayPage(code, state, errorDesc || error));
1172
- return;
1173
- }
1174
-
1175
- if (!codexOAuthPending) {
1176
- res.writeHead(200, { 'Content-Type': 'text/html' });
1177
- res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
1178
- return;
1179
- }
1180
-
1181
- try {
1182
- if (error) throw new Error(errorDesc || error);
1183
- const email = await exchangeCodexOAuthCode(code, state);
1184
- res.writeHead(200, { 'Content-Type': 'text/html' });
1185
- res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
1186
- } catch (e) {
1187
- res.writeHead(200, { 'Content-Type': 'text/html' });
1188
- res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
1189
- }
1190
- }
1191
-
1192
487
  const PROVIDER_CONFIGS = {
1193
488
  'anthropic': {
1194
489
  name: 'Anthropic', configPaths: [
@@ -1364,6 +659,9 @@ function sendJSON(req, res, statusCode, data) {
1364
659
  compressAndSend(req, res, statusCode, 'application/json', JSON.stringify(data));
1365
660
  }
1366
661
 
662
+ const _rateLimitMap = new LRUCache({ max: 1000, ttl: 60000 });
663
+ const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '300', 10);
664
+
1367
665
  const server = http.createServer(async (req, res) => {
1368
666
  res.setHeader('Access-Control-Allow-Origin', '*');
1369
667
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
@@ -1371,6 +669,17 @@ const server = http.createServer(async (req, res) => {
1371
669
  if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
1372
670
  if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return;
1373
671
 
672
+ const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress;
673
+ const hits = (_rateLimitMap.get(clientIp) || 0) + 1;
674
+ _rateLimitMap.set(clientIp, hits);
675
+ res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
676
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits));
677
+ if (hits > RATE_LIMIT_MAX) {
678
+ res.writeHead(429, { 'Retry-After': '60' });
679
+ res.end('Too Many Requests');
680
+ return;
681
+ }
682
+
1374
683
  const _pwd = process.env.PASSWORD;
1375
684
  if (_pwd) {
1376
685
  const _auth = req.headers['authorization'] || '';
@@ -1428,12 +737,26 @@ const server = http.createServer(async (req, res) => {
1428
737
  const pathOnly = routePath.split('?')[0];
1429
738
 
1430
739
  if (pathOnly === '/oauth2callback' && req.method === 'GET') {
1431
- await handleGeminiOAuthCallback(req, res);
740
+ await handleGeminiOAuthCallback(req, res, PORT);
1432
741
  return;
1433
742
  }
1434
743
 
1435
744
  if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
1436
- await handleCodexOAuthCallback(req, res);
745
+ await handleCodexOAuthCallback(req, res, PORT);
746
+ return;
747
+ }
748
+
749
+ if (pathOnly === '/api/health' && req.method === 'GET') {
750
+ sendJSON(req, res, 200, {
751
+ status: 'ok',
752
+ version: PKG_VERSION,
753
+ uptime: process.uptime(),
754
+ agents: discoveredAgents.length,
755
+ activeExecutions: activeExecutions.size,
756
+ wsClients: wss.clients.size,
757
+ memory: process.memoryUsage(),
758
+ acp: getACPStatus()
759
+ });
1437
760
  return;
1438
761
  }
1439
762
 
@@ -2426,7 +1749,7 @@ const server = http.createServer(async (req, res) => {
2426
1749
  const localResult = queries.searchAgents(discoveredAgents, body);
2427
1750
 
2428
1751
  // Get external agents from ACP servers
2429
- const externalAgents = await discoverExternalACPServers();
1752
+ const externalAgents = await discoverExternalACPServers(discoveredAgents);
2430
1753
  const externalResult = queries.searchAgents(externalAgents, body);
2431
1754
 
2432
1755
  // Combine results
@@ -2879,7 +2202,7 @@ const server = http.createServer(async (req, res) => {
2879
2202
 
2880
2203
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
2881
2204
  try {
2882
- const result = await startGeminiOAuth(req);
2205
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
2883
2206
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2884
2207
  } catch (e) {
2885
2208
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -2889,7 +2212,7 @@ const server = http.createServer(async (req, res) => {
2889
2212
  }
2890
2213
 
2891
2214
  if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
2892
- sendJSON(req, res, 200, geminiOAuthState);
2215
+ sendJSON(req, res, 200, getGeminiOAuthState());
2893
2216
  return;
2894
2217
  }
2895
2218
 
@@ -2904,8 +2227,6 @@ const server = http.createServer(async (req, res) => {
2904
2227
  const email = await exchangeGeminiOAuthCode(code, stateParam);
2905
2228
  sendJSON(req, res, 200, { success: true, email });
2906
2229
  } catch (e) {
2907
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2908
- geminiOAuthPending = null;
2909
2230
  sendJSON(req, res, 400, { error: e.message });
2910
2231
  }
2911
2232
  return;
@@ -2929,8 +2250,6 @@ const server = http.createServer(async (req, res) => {
2929
2250
  const error = parsed.searchParams.get('error');
2930
2251
  if (error) {
2931
2252
  const desc = parsed.searchParams.get('error_description') || error;
2932
- geminiOAuthState = { status: 'error', error: desc, email: null };
2933
- geminiOAuthPending = null;
2934
2253
  sendJSON(req, res, 200, { error: desc });
2935
2254
  return;
2936
2255
  }
@@ -2940,8 +2259,6 @@ const server = http.createServer(async (req, res) => {
2940
2259
  const email = await exchangeGeminiOAuthCode(code, state);
2941
2260
  sendJSON(req, res, 200, { success: true, email });
2942
2261
  } catch (e) {
2943
- geminiOAuthState = { status: 'error', error: e.message, email: null };
2944
- geminiOAuthPending = null;
2945
2262
  sendJSON(req, res, 400, { error: e.message });
2946
2263
  }
2947
2264
  return;
@@ -2949,7 +2266,7 @@ const server = http.createServer(async (req, res) => {
2949
2266
 
2950
2267
  if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
2951
2268
  try {
2952
- const result = await startCodexOAuth(req);
2269
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
2953
2270
  sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2954
2271
  } catch (e) {
2955
2272
  console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
@@ -2959,7 +2276,7 @@ const server = http.createServer(async (req, res) => {
2959
2276
  }
2960
2277
 
2961
2278
  if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
2962
- sendJSON(req, res, 200, codexOAuthState);
2279
+ sendJSON(req, res, 200, getCodexOAuthState());
2963
2280
  return;
2964
2281
  }
2965
2282
 
@@ -2974,8 +2291,6 @@ const server = http.createServer(async (req, res) => {
2974
2291
  const email = await exchangeCodexOAuthCode(code, stateParam);
2975
2292
  sendJSON(req, res, 200, { success: true, email });
2976
2293
  } catch (e) {
2977
- codexOAuthState = { status: 'error', error: e.message, email: null };
2978
- codexOAuthPending = null;
2979
2294
  sendJSON(req, res, 400, { error: e.message });
2980
2295
  }
2981
2296
  return;
@@ -2997,8 +2312,6 @@ const server = http.createServer(async (req, res) => {
2997
2312
  const error = parsed.searchParams.get('error');
2998
2313
  if (error) {
2999
2314
  const desc = parsed.searchParams.get('error_description') || error;
3000
- codexOAuthState = { status: 'error', error: desc, email: null };
3001
- codexOAuthPending = null;
3002
2315
  sendJSON(req, res, 200, { error: desc });
3003
2316
  return;
3004
2317
  }
@@ -3007,8 +2320,6 @@ const server = http.createServer(async (req, res) => {
3007
2320
  const email = await exchangeCodexOAuthCode(code, state);
3008
2321
  sendJSON(req, res, 200, { success: true, email });
3009
2322
  } catch (e) {
3010
- codexOAuthState = { status: 'error', error: e.message, email: null };
3011
- codexOAuthPending = null;
3012
2323
  sendJSON(req, res, 400, { error: e.message });
3013
2324
  }
3014
2325
  return;
@@ -3022,21 +2333,21 @@ const server = http.createServer(async (req, res) => {
3022
2333
 
3023
2334
  if (agentId === 'codex' || agentId === 'cli-codex') {
3024
2335
  try {
3025
- const result = await startCodexOAuth(req);
2336
+ const result = await startCodexOAuth(req, { PORT, BASE_URL });
3026
2337
  const conversationId = '__agent_auth__';
3027
2338
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
3028
2339
  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() });
3029
2340
 
3030
2341
  const pollId = setInterval(() => {
3031
- if (codexOAuthState.status === 'success') {
2342
+ if (getCodexOAuthState().status === 'success') {
3032
2343
  clearInterval(pollId);
3033
- const email = codexOAuthState.email || '';
2344
+ const email = getCodexOAuthState().email || '';
3034
2345
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
3035
2346
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
3036
- } else if (codexOAuthState.status === 'error') {
2347
+ } else if (getCodexOAuthState().status === 'error') {
3037
2348
  clearInterval(pollId);
3038
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
3039
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexOAuthState.error, timestamp: Date.now() });
2349
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2350
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
3040
2351
  }
3041
2352
  }, 1000);
3042
2353
 
@@ -3053,21 +2364,21 @@ const server = http.createServer(async (req, res) => {
3053
2364
 
3054
2365
  if (agentId === 'gemini') {
3055
2366
  try {
3056
- const result = await startGeminiOAuth(req);
2367
+ const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
3057
2368
  const conversationId = '__agent_auth__';
3058
2369
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
3059
2370
  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() });
3060
2371
 
3061
2372
  const pollId = setInterval(() => {
3062
- if (geminiOAuthState.status === 'success') {
2373
+ if (getGeminiOAuthState().status === 'success') {
3063
2374
  clearInterval(pollId);
3064
- const email = geminiOAuthState.email || '';
2375
+ const email = getGeminiOAuthState().email || '';
3065
2376
  broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
3066
2377
  broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
3067
- } else if (geminiOAuthState.status === 'error') {
2378
+ } else if (getGeminiOAuthState().status === 'error') {
3068
2379
  clearInterval(pollId);
3069
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${geminiOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
3070
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: geminiOAuthState.error, timestamp: Date.now() });
2380
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2381
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
3071
2382
  }
3072
2383
  }, 1000);
3073
2384
 
@@ -4649,7 +3960,7 @@ console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.leng
4649
3960
  registerSessionHandlers(wsRouter, {
4650
3961
  db: queries, discoveredAgents, modelCache,
4651
3962
  getAgentDescriptor, activeScripts, broadcastSync,
4652
- startGeminiOAuth, geminiOAuthState: () => geminiOAuthState
3963
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
4653
3964
  });
4654
3965
  console.log('[INIT] registerSessionHandlers completed');
4655
3966
 
@@ -4669,10 +3980,12 @@ registerScriptHandlers(wsRouter, {
4669
3980
  });
4670
3981
 
4671
3982
  registerOAuthHandlers(wsRouter, {
4672
- startGeminiOAuth, exchangeGeminiOAuthCode,
4673
- geminiOAuthState: () => geminiOAuthState,
4674
- startCodexOAuth, exchangeCodexOAuthCode,
4675
- codexOAuthState: () => codexOAuthState,
3983
+ startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
3984
+ exchangeGeminiOAuthCode,
3985
+ geminiOAuthState: getGeminiOAuthState,
3986
+ startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
3987
+ exchangeCodexOAuthCode,
3988
+ codexOAuthState: getCodexOAuthState,
4676
3989
  });
4677
3990
 
4678
3991
  wsRouter.onLegacy((data, ws) => {