agentgui 1.0.209 → 1.0.211

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.209",
3
+ "version": "1.0.211",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -246,6 +246,9 @@ function extractOAuthFromFile(oauth2Path) {
246
246
  }
247
247
 
248
248
  function getGeminiOAuthCreds() {
249
+ if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
250
+ return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
251
+ }
249
252
  const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
250
253
  try {
251
254
  const geminiPath = findCommand('gemini');
@@ -333,13 +336,24 @@ function geminiOAuthResultPage(title, message, success) {
333
336
  </div></body></html>`;
334
337
  }
335
338
 
336
- async function startGeminiOAuth(baseUrl) {
339
+ function isRemoteRequest(req) {
340
+ return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
341
+ }
342
+
343
+ async function startGeminiOAuth(req) {
337
344
  const creds = getGeminiOAuthCreds();
338
345
  if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
339
346
 
340
- const redirectUri = `${baseUrl}${BASE_URL}/oauth2callback`;
341
- const state = crypto.randomBytes(32).toString('hex');
347
+ const useCustomClient = !!creds.custom;
348
+ const remote = isRemoteRequest(req);
349
+ let redirectUri;
350
+ if (useCustomClient && req) {
351
+ redirectUri = `${buildBaseUrl(req)}${BASE_URL}/oauth2callback`;
352
+ } else {
353
+ redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
354
+ }
342
355
 
356
+ const state = crypto.randomBytes(32).toString('hex');
343
357
  const client = new OAuth2Client({
344
358
  clientId: creds.clientId,
345
359
  clientSecret: creds.clientSecret,
@@ -352,6 +366,7 @@ async function startGeminiOAuth(baseUrl) {
352
366
  state,
353
367
  });
354
368
 
369
+ const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
355
370
  geminiOAuthPending = { client, redirectUri, state };
356
371
  geminiOAuthState = { status: 'pending', error: null, email: null };
357
372
 
@@ -362,74 +377,77 @@ async function startGeminiOAuth(baseUrl) {
362
377
  }
363
378
  }, 5 * 60 * 1000);
364
379
 
365
- return authUrl;
380
+ return { authUrl, mode };
366
381
  }
367
382
 
368
- async function handleGeminiOAuthCallback(req, res) {
369
- const reqUrl = new URL(req.url, buildBaseUrl(req));
370
-
371
- if (!geminiOAuthPending) {
372
- res.writeHead(200, { 'Content-Type': 'text/html' });
373
- res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
374
- return;
375
- }
376
-
377
- const error = reqUrl.searchParams.get('error');
378
- if (error) {
379
- const desc = reqUrl.searchParams.get('error_description') || error;
380
- geminiOAuthState = { status: 'error', error: desc, email: null };
381
- geminiOAuthPending = null;
382
- res.writeHead(200, { 'Content-Type': 'text/html' });
383
- res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
384
- return;
385
- }
383
+ async function exchangeGeminiOAuthCode(code, state) {
384
+ if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
386
385
 
387
386
  const { client, redirectUri, state: expectedState } = geminiOAuthPending;
388
387
 
389
- if (reqUrl.searchParams.get('state') !== expectedState) {
388
+ if (state !== expectedState) {
390
389
  geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
391
390
  geminiOAuthPending = null;
392
- res.writeHead(200, { 'Content-Type': 'text/html' });
393
- res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
394
- return;
391
+ throw new Error('State mismatch - possible CSRF attack.');
395
392
  }
396
393
 
397
- const code = reqUrl.searchParams.get('code');
398
394
  if (!code) {
399
- geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
395
+ geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
400
396
  geminiOAuthPending = null;
397
+ throw new Error('No authorization code received.');
398
+ }
399
+
400
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
401
+ client.setCredentials(tokens);
402
+
403
+ let email = '';
404
+ try {
405
+ const { token } = await client.getAccessToken();
406
+ if (token) {
407
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
408
+ headers: { Authorization: `Bearer ${token}` }
409
+ });
410
+ if (resp.ok) {
411
+ const info = await resp.json();
412
+ email = info.email || '';
413
+ }
414
+ }
415
+ } catch (_) {}
416
+
417
+ saveGeminiCredentials(tokens, email);
418
+ geminiOAuthState = { status: 'success', error: null, email };
419
+ geminiOAuthPending = null;
420
+
421
+ return email;
422
+ }
423
+
424
+ async function handleGeminiOAuthCallback(req, res) {
425
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
426
+
427
+ if (!geminiOAuthPending) {
401
428
  res.writeHead(200, { 'Content-Type': 'text/html' });
402
- res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
429
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
403
430
  return;
404
431
  }
405
432
 
406
433
  try {
407
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
408
- client.setCredentials(tokens);
409
-
410
- let email = '';
411
- try {
412
- const { token } = await client.getAccessToken();
413
- if (token) {
414
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
415
- headers: { Authorization: `Bearer ${token}` }
416
- });
417
- if (resp.ok) {
418
- const info = await resp.json();
419
- email = info.email || '';
420
- }
421
- }
422
- } catch (_) {}
434
+ const error = reqUrl.searchParams.get('error');
435
+ if (error) {
436
+ const desc = reqUrl.searchParams.get('error_description') || error;
437
+ geminiOAuthState = { status: 'error', error: desc, email: null };
438
+ geminiOAuthPending = null;
439
+ res.writeHead(200, { 'Content-Type': 'text/html' });
440
+ res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
441
+ return;
442
+ }
423
443
 
424
- saveGeminiCredentials(tokens, email);
425
- geminiOAuthState = { status: 'success', error: null, email };
426
- geminiOAuthPending = null;
444
+ const code = reqUrl.searchParams.get('code');
445
+ const state = reqUrl.searchParams.get('state');
446
+ const email = await exchangeGeminiOAuthCode(code, state);
427
447
 
428
448
  res.writeHead(200, { 'Content-Type': 'text/html' });
429
449
  res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
430
450
  } catch (e) {
431
- geminiOAuthState = { status: 'error', error: e.message, email: null };
432
- geminiOAuthPending = null;
433
451
  res.writeHead(200, { 'Content-Type': 'text/html' });
434
452
  res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
435
453
  }
@@ -1127,8 +1145,8 @@ const server = http.createServer(async (req, res) => {
1127
1145
 
1128
1146
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1129
1147
  try {
1130
- const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1131
- sendJSON(req, res, 200, { authUrl });
1148
+ const result = await startGeminiOAuth(req);
1149
+ sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
1132
1150
  } catch (e) {
1133
1151
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
1134
1152
  sendJSON(req, res, 500, { error: e.message });
@@ -1141,6 +1159,42 @@ const server = http.createServer(async (req, res) => {
1141
1159
  return;
1142
1160
  }
1143
1161
 
1162
+ if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
1163
+ try {
1164
+ const body = await parseBody(req);
1165
+ const pastedUrl = (body.url || '').trim();
1166
+ if (!pastedUrl) {
1167
+ sendJSON(req, res, 400, { error: 'No URL provided' });
1168
+ return;
1169
+ }
1170
+
1171
+ let parsed;
1172
+ try { parsed = new URL(pastedUrl); } catch (_) {
1173
+ sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
1174
+ return;
1175
+ }
1176
+
1177
+ const error = parsed.searchParams.get('error');
1178
+ if (error) {
1179
+ const desc = parsed.searchParams.get('error_description') || error;
1180
+ geminiOAuthState = { status: 'error', error: desc, email: null };
1181
+ geminiOAuthPending = null;
1182
+ sendJSON(req, res, 200, { error: desc });
1183
+ return;
1184
+ }
1185
+
1186
+ const code = parsed.searchParams.get('code');
1187
+ const state = parsed.searchParams.get('state');
1188
+ const email = await exchangeGeminiOAuthCode(code, state);
1189
+ sendJSON(req, res, 200, { success: true, email });
1190
+ } catch (e) {
1191
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
1192
+ geminiOAuthPending = null;
1193
+ sendJSON(req, res, 400, { error: e.message });
1194
+ }
1195
+ return;
1196
+ }
1197
+
1144
1198
  const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
1145
1199
  if (agentAuthMatch && req.method === 'POST') {
1146
1200
  const agentId = agentAuthMatch[1];
@@ -1149,10 +1203,10 @@ const server = http.createServer(async (req, res) => {
1149
1203
 
1150
1204
  if (agentId === 'gemini') {
1151
1205
  try {
1152
- const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1206
+ const result = await startGeminiOAuth(req);
1153
1207
  const conversationId = '__agent_auth__';
1154
1208
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1155
- 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${authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
1209
+ 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() });
1156
1210
 
1157
1211
  const pollId = setInterval(() => {
1158
1212
  if (geminiOAuthState.status === 'success') {
@@ -1169,7 +1223,7 @@ const server = http.createServer(async (req, res) => {
1169
1223
 
1170
1224
  setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
1171
1225
 
1172
- sendJSON(req, res, 200, { ok: true, agentId, authUrl });
1226
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
1173
1227
  return;
1174
1228
  } catch (e) {
1175
1229
  console.error('[gemini-oauth] /api/agents/gemini/auth failed:', e);
@@ -130,6 +130,81 @@
130
130
 
131
131
  function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
132
132
 
133
+ var oauthPollInterval = null, oauthPollTimeout = null;
134
+
135
+ function cleanupOAuthPolling() {
136
+ if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
137
+ if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
138
+ }
139
+
140
+ function showOAuthPasteModal() {
141
+ removeOAuthPasteModal();
142
+ var overlay = document.createElement('div');
143
+ overlay.id = 'oauthPasteModal';
144
+ 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;';
145
+ var s = function(c) { return 'font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 ' + (c ? '0' : '0.5rem') + ';'; };
146
+ overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;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()">' +
147
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
148
+ '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Complete Google Sign-In</h2>' +
149
+ '<button id="oauthPasteClose" 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>' +
150
+ '<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
151
+ '<p style="' + s() + '">1. A Google sign-in page has opened in a new tab.</p>' +
152
+ '<p style="' + s() + '">2. Complete the sign-in process with Google.</p>' +
153
+ '<p style="' + s() + '">3. After signing in, you will be redirected to a page that <span style="color:#facc15;font-weight:600;">may not load</span> (this is expected).</p>' +
154
+ '<p style="' + s(1) + '">4. Copy the <span style="color:white;font-weight:600;">entire URL</span> from the address bar and paste it below.</p></div>' +
155
+ '<label style="display:block;font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin-bottom:0.5rem;">Paste the redirect URL here:</label>' +
156
+ '<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
157
+ '<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p>' +
158
+ '<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
159
+ '<button id="oauthPasteCancel" style="flex:1;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>' +
160
+ '<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;">Complete Sign-In</button></div>' +
161
+ '<p style="font-size:0.7rem;color:var(--color-text-secondary,#6b7280);margin-top:1rem;text-align:center;">If the redirect page loaded successfully, this dialog will close automatically.</p></div>';
162
+ document.body.appendChild(overlay);
163
+ var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthPasteModal(); };
164
+ document.getElementById('oauthPasteClose').addEventListener('click', dismiss);
165
+ document.getElementById('oauthPasteCancel').addEventListener('click', dismiss);
166
+ document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
167
+ document.getElementById('oauthPasteInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
168
+ setTimeout(function() { var i = document.getElementById('oauthPasteInput'); if (i) i.focus(); }, 100);
169
+ }
170
+
171
+ function removeOAuthPasteModal() {
172
+ var el = document.getElementById('oauthPasteModal');
173
+ if (el) el.remove();
174
+ }
175
+
176
+ function submitOAuthPasteUrl() {
177
+ var input = document.getElementById('oauthPasteInput');
178
+ var errorEl = document.getElementById('oauthPasteError');
179
+ var submitBtn = document.getElementById('oauthPasteSubmit');
180
+ if (!input) return;
181
+ var url = input.value.trim();
182
+ if (!url) {
183
+ if (errorEl) { errorEl.textContent = 'Please paste the URL from the redirected page.'; errorEl.style.display = 'block'; }
184
+ return;
185
+ }
186
+ if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; }
187
+ if (errorEl) errorEl.style.display = 'none';
188
+
189
+ fetch(BASE + '/api/gemini-oauth/complete', {
190
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify({ url: url })
192
+ }).then(function(r) { return r.json(); }).then(function(data) {
193
+ if (data.success) {
194
+ cleanupOAuthPolling();
195
+ authRunning = false;
196
+ removeOAuthPasteModal();
197
+ refresh();
198
+ } else {
199
+ if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
200
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
201
+ }
202
+ }).catch(function(e) {
203
+ if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
204
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
205
+ });
206
+ }
207
+
133
208
  function triggerAuth(agentId) {
134
209
  if (authRunning) return;
135
210
  fetch(BASE + '/api/agents/' + agentId + '/auth', {
@@ -141,6 +216,29 @@
141
216
  if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
142
217
  if (data.authUrl) {
143
218
  window.open(data.authUrl, '_blank');
219
+ if (agentId === 'gemini') {
220
+ var needsPaste = data.mode === 'cli-remote';
221
+ if (needsPaste) showOAuthPasteModal();
222
+ cleanupOAuthPolling();
223
+ oauthPollInterval = setInterval(function() {
224
+ fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
225
+ if (status.status === 'success') {
226
+ cleanupOAuthPolling();
227
+ authRunning = false;
228
+ removeOAuthPasteModal();
229
+ refresh();
230
+ } else if (status.status === 'error') {
231
+ cleanupOAuthPolling();
232
+ authRunning = false;
233
+ removeOAuthPasteModal();
234
+ }
235
+ }).catch(function() {});
236
+ }, 1500);
237
+ oauthPollTimeout = setTimeout(function() {
238
+ cleanupOAuthPolling();
239
+ if (authRunning) { authRunning = false; removeOAuthPasteModal(); }
240
+ }, 5 * 60 * 1000);
241
+ }
144
242
  }
145
243
  }
146
244
  }).catch(function() {});
@@ -159,6 +257,8 @@
159
257
  if (term) term.write(data.data);
160
258
  } else if (data.type === 'script_stopped') {
161
259
  authRunning = false;
260
+ removeOAuthPasteModal();
261
+ cleanupOAuthPolling();
162
262
  var term = getTerminal();
163
263
  var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
164
264
  if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');