agentgui 1.0.702 → 1.0.703

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
@@ -696,96 +696,6 @@ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
696
696
  let geminiOAuthState = { status: 'idle', error: null, email: null };
697
697
  let geminiOAuthPending = null;
698
698
 
699
- const CODEX_AUTH_FILE = path.join(os.homedir(), '.codex', 'auth.json');
700
- let codexDeviceAuthState = { status: 'idle', error: null, userCode: null, authUrl: null };
701
- let codexDeviceAuthProcess = null;
702
-
703
- function getCodexAuthStatus() {
704
- try {
705
- if (fs.existsSync(CODEX_AUTH_FILE)) {
706
- const creds = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf-8'));
707
- if (creds.auth_mode === 'oauth' || creds.access_token || creds.refresh_token) {
708
- return { authenticated: true, detail: creds.email || 'oauth' };
709
- }
710
- if (creds.auth_mode === 'apikey' && creds.OPENAI_API_KEY) {
711
- return { authenticated: true, detail: 'api-key' };
712
- }
713
- }
714
- } catch (_) {}
715
- return { authenticated: false, detail: 'no credentials' };
716
- }
717
-
718
- function startCodexDeviceAuth() {
719
- if (codexDeviceAuthProcess) {
720
- return { alreadyRunning: true };
721
- }
722
- const codexStatus = getCodexAuthStatus();
723
- if (codexStatus.authenticated) {
724
- codexDeviceAuthState = { status: 'success', error: null, userCode: null, authUrl: null };
725
- return { alreadyAuthenticated: true };
726
- }
727
- codexDeviceAuthState = { status: 'pending', error: null, userCode: null, authUrl: null };
728
- const child = spawn('npx', ['@openai/codex', 'login', '--device-auth'], {
729
- stdio: ['ignore', 'pipe', 'pipe'],
730
- env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
731
- shell: os.platform() === 'win32',
732
- });
733
- codexDeviceAuthProcess = child;
734
-
735
- const onData = (chunk) => {
736
- const text = chunk.toString().replace(/\x1b\[[0-9;]*m/g, '');
737
- const urlMatch = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s]*/);
738
- const codeMatch = text.match(/\b([0-9A-Z]{4}-[0-9A-Z]{5})\b/);
739
- if (urlMatch && !codexDeviceAuthState.authUrl) codexDeviceAuthState.authUrl = urlMatch[0];
740
- if (codeMatch && !codexDeviceAuthState.userCode) codexDeviceAuthState.userCode = codeMatch[1];
741
- };
742
- child.stdout.on('data', onData);
743
- child.stderr.on('data', onData);
744
-
745
- const authFileWatcher = fs.watchFile ? null : null;
746
- const pollInterval = setInterval(() => {
747
- const st = getCodexAuthStatus();
748
- if (st.authenticated) {
749
- clearInterval(pollInterval);
750
- clearTimeout(timeoutId);
751
- codexDeviceAuthState = { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl };
752
- if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
753
- }
754
- }, 1500);
755
-
756
- const timeoutId = setTimeout(() => {
757
- clearInterval(pollInterval);
758
- if (codexDeviceAuthState.status === 'pending') {
759
- codexDeviceAuthState = { status: 'error', error: 'Authentication timed out', userCode: null, authUrl: null };
760
- }
761
- if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
762
- }, 5 * 60 * 1000);
763
-
764
- child.on('error', (e) => {
765
- clearInterval(pollInterval);
766
- clearTimeout(timeoutId);
767
- codexDeviceAuthState = { status: 'error', error: e.message, userCode: null, authUrl: null };
768
- codexDeviceAuthProcess = null;
769
- });
770
- child.on('close', (code) => {
771
- clearInterval(pollInterval);
772
- clearTimeout(timeoutId);
773
- codexDeviceAuthProcess = null;
774
- if (codexDeviceAuthState.status === 'pending') {
775
- if (code === 0) {
776
- const st = getCodexAuthStatus();
777
- codexDeviceAuthState = st.authenticated
778
- ? { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl }
779
- : { status: 'error', error: 'Process exited without saving credentials', userCode: null, authUrl: null };
780
- } else {
781
- codexDeviceAuthState = { status: 'error', error: 'Authentication cancelled', userCode: null, authUrl: null };
782
- }
783
- }
784
- });
785
-
786
- return { started: true };
787
- }
788
-
789
699
  function buildBaseUrl(req) {
790
700
  const override = process.env.AGENTGUI_BASE_URL;
791
701
  if (override) return override.replace(/\/+$/, '');
@@ -1039,6 +949,238 @@ function getGeminiOAuthStatus() {
1039
949
  return null;
1040
950
  }
1041
951
 
952
+ const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
953
+ const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
954
+ const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
955
+ const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
956
+ const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
957
+ const CODEX_OAUTH_PORT = 1455;
958
+
959
+ let codexOAuthState = { status: 'idle', error: null, email: null };
960
+ let codexOAuthPending = null;
961
+
962
+ function generatePkce() {
963
+ const verifierBytes = crypto.randomBytes(64);
964
+ const codeVerifier = verifierBytes.toString('base64url');
965
+ const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
966
+ const codeChallenge = challengeBytes.toString('base64url');
967
+ return { codeVerifier, codeChallenge };
968
+ }
969
+
970
+ function parseJwtEmail(jwt) {
971
+ try {
972
+ const parts = jwt.split('.');
973
+ if (parts.length < 2) return '';
974
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
975
+ return payload.email || payload['https://api.openai.com/profile']?.email || '';
976
+ } catch (_) { return ''; }
977
+ }
978
+
979
+ function saveCodexCredentials(tokens) {
980
+ if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
981
+ const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
982
+ fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
983
+ try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
984
+ }
985
+
986
+ function getCodexOAuthStatus() {
987
+ try {
988
+ if (fs.existsSync(CODEX_AUTH_FILE)) {
989
+ const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
990
+ if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
991
+ const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
992
+ return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
993
+ }
994
+ }
995
+ } catch (_) {}
996
+ return null;
997
+ }
998
+
999
+ async function startCodexOAuth(req) {
1000
+ const remote = isRemoteRequest(req);
1001
+ const redirectUri = remote
1002
+ ? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
1003
+ : `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
1004
+
1005
+ const pkce = generatePkce();
1006
+ const csrfToken = crypto.randomBytes(32).toString('hex');
1007
+ const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
1008
+ const state = encodeOAuthState(csrfToken, relayUrl);
1009
+
1010
+ const params = new URLSearchParams({
1011
+ response_type: 'code',
1012
+ client_id: CODEX_CLIENT_ID,
1013
+ redirect_uri: redirectUri,
1014
+ scope: CODEX_SCOPES,
1015
+ code_challenge: pkce.codeChallenge,
1016
+ code_challenge_method: 'S256',
1017
+ id_token_add_organizations: 'true',
1018
+ codex_cli_simplified_flow: 'true',
1019
+ state,
1020
+ });
1021
+
1022
+ const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
1023
+ const mode = remote ? 'remote' : 'local';
1024
+
1025
+ codexOAuthPending = { pkce, redirectUri, state: csrfToken };
1026
+ codexOAuthState = { status: 'pending', error: null, email: null };
1027
+
1028
+ setTimeout(() => {
1029
+ if (codexOAuthState.status === 'pending') {
1030
+ codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
1031
+ codexOAuthPending = null;
1032
+ }
1033
+ }, 5 * 60 * 1000);
1034
+
1035
+ return { authUrl, mode };
1036
+ }
1037
+
1038
+ async function exchangeCodexOAuthCode(code, stateParam) {
1039
+ if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
1040
+
1041
+ const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
1042
+ const { csrfToken } = decodeOAuthState(stateParam);
1043
+
1044
+ if (csrfToken !== expectedCsrf) {
1045
+ codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
1046
+ codexOAuthPending = null;
1047
+ throw new Error('State mismatch - possible CSRF attack.');
1048
+ }
1049
+
1050
+ if (!code) {
1051
+ codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
1052
+ codexOAuthPending = null;
1053
+ throw new Error('No authorization code received.');
1054
+ }
1055
+
1056
+ const body = new URLSearchParams({
1057
+ grant_type: 'authorization_code',
1058
+ code,
1059
+ redirect_uri: redirectUri,
1060
+ client_id: CODEX_CLIENT_ID,
1061
+ code_verifier: pkce.codeVerifier,
1062
+ });
1063
+
1064
+ const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
1065
+ method: 'POST',
1066
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1067
+ body: body.toString(),
1068
+ });
1069
+
1070
+ if (!resp.ok) {
1071
+ const text = await resp.text();
1072
+ codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
1073
+ codexOAuthPending = null;
1074
+ throw new Error(`Token exchange failed (${resp.status}): ${text}`);
1075
+ }
1076
+
1077
+ const tokens = await resp.json();
1078
+ const email = parseJwtEmail(tokens.id_token || '');
1079
+
1080
+ saveCodexCredentials(tokens);
1081
+ codexOAuthState = { status: 'success', error: null, email };
1082
+ codexOAuthPending = null;
1083
+
1084
+ return email;
1085
+ }
1086
+
1087
+ function codexOAuthResultPage(title, message, success) {
1088
+ const color = success ? '#10b981' : '#ef4444';
1089
+ const icon = success ? '&#10003;' : '&#10007;';
1090
+ return `<!DOCTYPE html><html><head><title>${title}</title></head>
1091
+ <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;">
1092
+ <div style="text-align:center;max-width:400px;padding:2rem;">
1093
+ <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
1094
+ <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
1095
+ <p style="color:#9ca3af;">${message}</p>
1096
+ <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
1097
+ </div></body></html>`;
1098
+ }
1099
+
1100
+ function codexOAuthRelayPage(code, state, error) {
1101
+ const stateData = decodeOAuthState(state || '');
1102
+ const relayUrl = stateData.relayUrl || '';
1103
+ const escapedCode = (code || '').replace(/['"\\]/g, '');
1104
+ const escapedState = (state || '').replace(/['"\\]/g, '');
1105
+ const escapedError = (error || '').replace(/['"\\]/g, '');
1106
+ const escapedRelay = relayUrl.replace(/['"\\]/g, '');
1107
+ return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
1108
+ <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;">
1109
+ <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
1110
+ <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
1111
+ <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
1112
+ <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
1113
+ </div>
1114
+ <script>
1115
+ (function() {
1116
+ var code = '${escapedCode}';
1117
+ var state = '${escapedState}';
1118
+ var error = '${escapedError}';
1119
+ var relayUrl = '${escapedRelay}';
1120
+ function show(icon, title, msg, color) {
1121
+ document.getElementById('spinner').textContent = icon;
1122
+ document.getElementById('spinner').style.color = color;
1123
+ document.getElementById('title').textContent = title;
1124
+ document.getElementById('msg').textContent = msg;
1125
+ }
1126
+ if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
1127
+ if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
1128
+ if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
1129
+ fetch(relayUrl, {
1130
+ method: 'POST',
1131
+ headers: { 'Content-Type': 'application/json' },
1132
+ body: JSON.stringify({ code: code, state: state })
1133
+ }).then(function(r) { return r.json(); }).then(function(data) {
1134
+ if (data.success) {
1135
+ show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
1136
+ } else {
1137
+ show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
1138
+ }
1139
+ }).catch(function(e) {
1140
+ show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
1141
+ });
1142
+ })();
1143
+ </script>
1144
+ </body></html>`;
1145
+ }
1146
+
1147
+ async function handleCodexOAuthCallback(req, res) {
1148
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
1149
+ const code = reqUrl.searchParams.get('code');
1150
+ const state = reqUrl.searchParams.get('state');
1151
+ const error = reqUrl.searchParams.get('error');
1152
+ const errorDesc = reqUrl.searchParams.get('error_description');
1153
+
1154
+ if (error) {
1155
+ const desc = errorDesc || error;
1156
+ codexOAuthState = { status: 'error', error: desc, email: null };
1157
+ codexOAuthPending = null;
1158
+ }
1159
+
1160
+ const stateData = decodeOAuthState(state || '');
1161
+ if (stateData.relayUrl) {
1162
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1163
+ res.end(codexOAuthRelayPage(code, state, errorDesc || error));
1164
+ return;
1165
+ }
1166
+
1167
+ if (!codexOAuthPending) {
1168
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1169
+ res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
1170
+ return;
1171
+ }
1172
+
1173
+ try {
1174
+ if (error) throw new Error(errorDesc || error);
1175
+ const email = await exchangeCodexOAuthCode(code, state);
1176
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1177
+ res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
1178
+ } catch (e) {
1179
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1180
+ res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
1181
+ }
1182
+ }
1183
+
1042
1184
  const PROVIDER_CONFIGS = {
1043
1185
  'anthropic': {
1044
1186
  name: 'Anthropic', configPaths: [
@@ -1127,6 +1269,13 @@ function getProviderConfigs() {
1127
1269
  continue;
1128
1270
  }
1129
1271
  }
1272
+ if (providerId === 'codex') {
1273
+ const oauthStatus = getCodexOAuthStatus();
1274
+ if (oauthStatus) {
1275
+ configs[providerId] = { name: config.name, ...oauthStatus };
1276
+ continue;
1277
+ }
1278
+ }
1130
1279
  for (const configPath of config.configPaths) {
1131
1280
  try {
1132
1281
  if (fs.existsSync(configPath)) {
@@ -1257,6 +1406,11 @@ const server = http.createServer(async (req, res) => {
1257
1406
  return;
1258
1407
  }
1259
1408
 
1409
+ if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
1410
+ await handleCodexOAuthCallback(req, res);
1411
+ return;
1412
+ }
1413
+
1260
1414
  if (pathOnly === '/api/conversations' && req.method === 'GET') {
1261
1415
  const conversations = queries.getConversationsList();
1262
1416
  // Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
@@ -2267,10 +2421,6 @@ const server = http.createServer(async (req, res) => {
2267
2421
  } else {
2268
2422
  status.detail = 'no credentials';
2269
2423
  }
2270
- } else if (agent.id === 'codex') {
2271
- const codexSt = getCodexAuthStatus();
2272
- status.authenticated = codexSt.authenticated;
2273
- status.detail = codexSt.detail;
2274
2424
  } else {
2275
2425
  status.detail = 'unknown';
2276
2426
  }
@@ -2716,12 +2866,110 @@ const server = http.createServer(async (req, res) => {
2716
2866
  return;
2717
2867
  }
2718
2868
 
2869
+ if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
2870
+ try {
2871
+ const result = await startCodexOAuth(req);
2872
+ sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
2873
+ } catch (e) {
2874
+ console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
2875
+ sendJSON(req, res, 500, { error: e.message });
2876
+ }
2877
+ return;
2878
+ }
2879
+
2880
+ if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
2881
+ sendJSON(req, res, 200, codexOAuthState);
2882
+ return;
2883
+ }
2884
+
2885
+ if (pathOnly === '/api/codex-oauth/relay' && req.method === 'POST') {
2886
+ try {
2887
+ const body = await parseBody(req);
2888
+ const { code, state: stateParam } = body;
2889
+ if (!code || !stateParam) {
2890
+ sendJSON(req, res, 400, { error: 'Missing code or state' });
2891
+ return;
2892
+ }
2893
+ const email = await exchangeCodexOAuthCode(code, stateParam);
2894
+ sendJSON(req, res, 200, { success: true, email });
2895
+ } catch (e) {
2896
+ codexOAuthState = { status: 'error', error: e.message, email: null };
2897
+ codexOAuthPending = null;
2898
+ sendJSON(req, res, 400, { error: e.message });
2899
+ }
2900
+ return;
2901
+ }
2902
+
2903
+ if (pathOnly === '/api/codex-oauth/complete' && req.method === 'POST') {
2904
+ try {
2905
+ const body = await parseBody(req);
2906
+ const pastedUrl = (body.url || '').trim();
2907
+ if (!pastedUrl) {
2908
+ sendJSON(req, res, 400, { error: 'No URL provided' });
2909
+ return;
2910
+ }
2911
+ let parsed;
2912
+ try { parsed = new URL(pastedUrl); } catch (_) {
2913
+ sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
2914
+ return;
2915
+ }
2916
+ const error = parsed.searchParams.get('error');
2917
+ if (error) {
2918
+ const desc = parsed.searchParams.get('error_description') || error;
2919
+ codexOAuthState = { status: 'error', error: desc, email: null };
2920
+ codexOAuthPending = null;
2921
+ sendJSON(req, res, 200, { error: desc });
2922
+ return;
2923
+ }
2924
+ const code = parsed.searchParams.get('code');
2925
+ const state = parsed.searchParams.get('state');
2926
+ const email = await exchangeCodexOAuthCode(code, state);
2927
+ sendJSON(req, res, 200, { success: true, email });
2928
+ } catch (e) {
2929
+ codexOAuthState = { status: 'error', error: e.message, email: null };
2930
+ codexOAuthPending = null;
2931
+ sendJSON(req, res, 400, { error: e.message });
2932
+ }
2933
+ return;
2934
+ }
2935
+
2719
2936
  const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
2720
2937
  if (agentAuthMatch && req.method === 'POST') {
2721
2938
  const agentId = agentAuthMatch[1];
2722
2939
  const agent = discoveredAgents.find(a => a.id === agentId);
2723
2940
  if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
2724
2941
 
2942
+ if (agentId === 'codex' || agentId === 'cli-codex') {
2943
+ try {
2944
+ const result = await startCodexOAuth(req);
2945
+ const conversationId = '__agent_auth__';
2946
+ broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
2947
+ 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() });
2948
+
2949
+ const pollId = setInterval(() => {
2950
+ if (codexOAuthState.status === 'success') {
2951
+ clearInterval(pollId);
2952
+ const email = codexOAuthState.email || '';
2953
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
2954
+ broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2955
+ } else if (codexOAuthState.status === 'error') {
2956
+ clearInterval(pollId);
2957
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2958
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexOAuthState.error, timestamp: Date.now() });
2959
+ }
2960
+ }, 1000);
2961
+
2962
+ setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
2963
+
2964
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
2965
+ return;
2966
+ } catch (e) {
2967
+ console.error('[codex-oauth] /api/agents/codex/auth failed:', e);
2968
+ sendJSON(req, res, 500, { error: e.message });
2969
+ return;
2970
+ }
2971
+ }
2972
+
2725
2973
  if (agentId === 'gemini') {
2726
2974
  try {
2727
2975
  const result = await startGeminiOAuth(req);
@@ -2753,55 +3001,6 @@ const server = http.createServer(async (req, res) => {
2753
3001
  }
2754
3002
  }
2755
3003
 
2756
- if (agentId === 'codex') {
2757
- const conversationId = '__agent_auth__';
2758
- const result = startCodexDeviceAuth();
2759
- if (result.alreadyRunning) {
2760
- sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: 'pending' });
2761
- return;
2762
- }
2763
- if (result.alreadyAuthenticated) {
2764
- sendJSON(req, res, 200, { ok: true, agentId, status: 'success' });
2765
- return;
2766
- }
2767
- broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
2768
- broadcastSync({ type: 'script_output', conversationId, data: '\x1b[36mStarting Codex device authorization...\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
2769
-
2770
- const waitForCode = (tries) => {
2771
- if (tries <= 0) return;
2772
- if (codexDeviceAuthState.authUrl && codexDeviceAuthState.userCode) {
2773
- const msg = `\r\nVisit: ${codexDeviceAuthState.authUrl}\r\nEnter code: ${codexDeviceAuthState.userCode}\r\n`;
2774
- broadcastSync({ type: 'script_output', conversationId, data: msg, stream: 'stdout', timestamp: Date.now() });
2775
- return;
2776
- }
2777
- setTimeout(() => waitForCode(tries - 1), 500);
2778
- };
2779
- waitForCode(10);
2780
-
2781
- const pollId = setInterval(() => {
2782
- if (codexDeviceAuthState.status === 'success') {
2783
- clearInterval(pollId);
2784
- broadcastSync({ type: 'script_output', conversationId, data: '\r\n\x1b[32mCodex authentication successful\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
2785
- broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2786
- } else if (codexDeviceAuthState.status === 'error') {
2787
- clearInterval(pollId);
2788
- broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexDeviceAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2789
- broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexDeviceAuthState.error, timestamp: Date.now() });
2790
- }
2791
- }, 1500);
2792
- setTimeout(() => clearInterval(pollId), 5 * 60 * 1000 + 5000);
2793
-
2794
- const waitForInfo = (tries) => {
2795
- if (codexDeviceAuthState.authUrl || tries <= 0) {
2796
- sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: codexDeviceAuthState.status });
2797
- } else {
2798
- setTimeout(() => waitForInfo(tries - 1), 400);
2799
- }
2800
- };
2801
- waitForInfo(12);
2802
- return;
2803
- }
2804
-
2805
3004
  const authCommands = {
2806
3005
  'claude-code': { cmd: 'claude', args: ['setup-token'] },
2807
3006
  'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
@@ -4333,7 +4532,8 @@ registerUtilHandlers(wsRouter, {
4333
4532
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
4334
4533
  startGeminiOAuth, exchangeGeminiOAuthCode,
4335
4534
  geminiOAuthState: () => geminiOAuthState,
4336
- startCodexDeviceAuth, codexDeviceAuthState: () => codexDeviceAuthState,
4535
+ startCodexOAuth, exchangeCodexOAuthCode,
4536
+ codexOAuthState: () => codexOAuthState,
4337
4537
  STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
4338
4538
  });
4339
4539
 
@@ -128,7 +128,6 @@
128
128
  function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
129
129
 
130
130
  var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
131
- var codexPollInterval = null, codexPollTimeout = null;
132
131
 
133
132
  function cleanupOAuthPolling() {
134
133
  if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
@@ -136,96 +135,6 @@
136
135
  if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
137
136
  }
138
137
 
139
- function cleanupCodexPolling() {
140
- if (codexPollInterval) { clearInterval(codexPollInterval); codexPollInterval = null; }
141
- if (codexPollTimeout) { clearTimeout(codexPollTimeout); codexPollTimeout = null; }
142
- }
143
-
144
- function removeCodexModal() {
145
- var el = document.getElementById('codexDeviceModal');
146
- if (el) el.remove();
147
- }
148
-
149
- function showCodexDeviceModal(authUrl, userCode) {
150
- removeCodexModal();
151
- var overlay = document.createElement('div');
152
- overlay.id = 'codexDeviceModal';
153
- overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
154
- var displayUrl = authUrl || 'https://auth.openai.com/codex/device';
155
- var displayCode = userCode || '';
156
- overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:30rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
157
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;">' +
158
- '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Codex Sign-In</h2>' +
159
- '<button id="codexModalClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
160
- '<div id="codexModalContent" style="text-align:center;">' +
161
- '<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">&#9203;</div>' +
162
- '<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 1rem;">Follow these steps to sign in with your OpenAI account:</p>' +
163
- '<div style="margin-bottom:1rem;">' +
164
- '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">1. Open this link in your browser:</p>' +
165
- '<div style="display:flex;gap:0.5rem;align-items:center;">' +
166
- '<a href="' + esc(displayUrl) + '" target="_blank" style="flex:1;padding:0.5rem 0.75rem;background:var(--color-primary,#3b82f6);color:white;text-decoration:none;border-radius:0.375rem;font-size:0.8rem;font-weight:600;text-align:center;">Open Sign-In Page</a>' +
167
- '</div></div>' +
168
- '<div style="margin-bottom:1.25rem;">' +
169
- '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">2. Enter this one-time code:</p>' +
170
- '<div style="display:flex;gap:0.5rem;align-items:center;">' +
171
- '<div id="codexUserCode" style="flex:1;padding:0.625rem;background:var(--color-bg-primary,#111827);border:2px solid var(--color-primary,#3b82f6);border-radius:0.5rem;font-family:monospace;font-size:1.25rem;font-weight:700;letter-spacing:0.15em;text-align:center;">' + esc(displayCode) + '</div>' +
172
- '<button id="codexCopyBtn" style="padding:0.5rem 0.75rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.375rem;color:var(--color-text-primary,white);font-size:0.75rem;cursor:pointer;flex-shrink:0;">Copy</button>' +
173
- '</div></div>' +
174
- '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">This dialog will close automatically when sign-in completes.</p>' +
175
- '</div>' +
176
- '<div id="codexModalSuccess" style="display:none;text-align:center;padding:1rem 0;">' +
177
- '<div style="font-size:3rem;color:#10b981;margin-bottom:0.75rem;">&#10003;</div>' +
178
- '<p style="font-weight:600;margin:0 0 0.25rem;">Authentication Successful</p>' +
179
- '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0;">Codex CLI is now authenticated.</p>' +
180
- '</div>' +
181
- '<div style="margin-top:1.25rem;">' +
182
- '<button id="codexModalCancel" style="width:100%;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button></div>' +
183
- '<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
184
- document.body.appendChild(overlay);
185
-
186
- var dismiss = function() { cleanupCodexPolling(); authRunning = false; removeCodexModal(); };
187
- document.getElementById('codexModalClose').addEventListener('click', dismiss);
188
- document.getElementById('codexModalCancel').addEventListener('click', dismiss);
189
-
190
- var copyBtn = document.getElementById('codexCopyBtn');
191
- if (copyBtn && displayCode) {
192
- copyBtn.addEventListener('click', function(e) {
193
- e.stopPropagation();
194
- navigator.clipboard.writeText(displayCode).then(function() {
195
- copyBtn.textContent = 'Copied!';
196
- setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
197
- }).catch(function() {});
198
- });
199
- }
200
-
201
- cleanupCodexPolling();
202
- codexPollInterval = setInterval(function() {
203
- window.wsClient.rpc('codex.status')
204
- .then(function(st) {
205
- if (st.status === 'success') {
206
- cleanupCodexPolling();
207
- authRunning = false;
208
- var content = document.getElementById('codexModalContent');
209
- var success = document.getElementById('codexModalSuccess');
210
- var cancel = document.getElementById('codexModalCancel');
211
- if (content) content.style.display = 'none';
212
- if (success) success.style.display = 'block';
213
- if (cancel) cancel.textContent = 'Close';
214
- setTimeout(function() { removeCodexModal(); refresh(); }, 2500);
215
- } else if (st.status === 'error') {
216
- cleanupCodexPolling();
217
- authRunning = false;
218
- removeCodexModal();
219
- refresh();
220
- }
221
- }).catch(function() {});
222
- }, 1500);
223
- codexPollTimeout = setTimeout(function() {
224
- cleanupCodexPolling();
225
- if (authRunning) { authRunning = false; removeCodexModal(); }
226
- }, 5 * 60 * 1000);
227
- }
228
-
229
138
  function showOAuthWaitingModal() {
230
139
  removeOAuthModal();
231
140
  var overlay = document.createElement('div');
@@ -308,19 +217,6 @@
308
217
 
309
218
  function triggerAuth(agentId) {
310
219
  if (authRunning) return;
311
- if (agentId === 'codex') {
312
- authRunning = true;
313
- window.wsClient.rpc('codex.start')
314
- .then(function(data) {
315
- if (data.status === 'success') {
316
- authRunning = false;
317
- refresh();
318
- return;
319
- }
320
- showCodexDeviceModal(data.authUrl, data.userCode);
321
- }).catch(function() { authRunning = false; });
322
- return;
323
- }
324
220
  window.wsClient.rpc('agent.auth', { id: agentId })
325
221
  .then(function(data) {
326
222
  if (data.ok) {
@@ -375,8 +271,6 @@
375
271
  authRunning = false;
376
272
  removeOAuthModal();
377
273
  cleanupOAuthPolling();
378
- cleanupCodexPolling();
379
- removeCodexModal();
380
274
  var term = getTerminal();
381
275
  var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
382
276
  if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');