agentgui 1.0.209 → 1.0.210

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.210",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -333,11 +333,11 @@ function geminiOAuthResultPage(title, message, success) {
333
333
  </div></body></html>`;
334
334
  }
335
335
 
336
- async function startGeminiOAuth(baseUrl) {
336
+ async function startGeminiOAuth() {
337
337
  const creds = getGeminiOAuthCreds();
338
338
  if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
339
339
 
340
- const redirectUri = `${baseUrl}${BASE_URL}/oauth2callback`;
340
+ const redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
341
341
  const state = crypto.randomBytes(32).toString('hex');
342
342
 
343
343
  const client = new OAuth2Client({
@@ -365,71 +365,74 @@ async function startGeminiOAuth(baseUrl) {
365
365
  return authUrl;
366
366
  }
367
367
 
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
- }
368
+ async function exchangeGeminiOAuthCode(code, state) {
369
+ if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
386
370
 
387
371
  const { client, redirectUri, state: expectedState } = geminiOAuthPending;
388
372
 
389
- if (reqUrl.searchParams.get('state') !== expectedState) {
373
+ if (state !== expectedState) {
390
374
  geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
391
375
  geminiOAuthPending = null;
392
- res.writeHead(200, { 'Content-Type': 'text/html' });
393
- res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
394
- return;
376
+ throw new Error('State mismatch - possible CSRF attack.');
395
377
  }
396
378
 
397
- const code = reqUrl.searchParams.get('code');
398
379
  if (!code) {
399
- geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
380
+ geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
400
381
  geminiOAuthPending = null;
382
+ throw new Error('No authorization code received.');
383
+ }
384
+
385
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
386
+ client.setCredentials(tokens);
387
+
388
+ let email = '';
389
+ try {
390
+ const { token } = await client.getAccessToken();
391
+ if (token) {
392
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
393
+ headers: { Authorization: `Bearer ${token}` }
394
+ });
395
+ if (resp.ok) {
396
+ const info = await resp.json();
397
+ email = info.email || '';
398
+ }
399
+ }
400
+ } catch (_) {}
401
+
402
+ saveGeminiCredentials(tokens, email);
403
+ geminiOAuthState = { status: 'success', error: null, email };
404
+ geminiOAuthPending = null;
405
+
406
+ return email;
407
+ }
408
+
409
+ async function handleGeminiOAuthCallback(req, res) {
410
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
411
+
412
+ if (!geminiOAuthPending) {
401
413
  res.writeHead(200, { 'Content-Type': 'text/html' });
402
- res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
414
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
403
415
  return;
404
416
  }
405
417
 
406
418
  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 (_) {}
419
+ const error = reqUrl.searchParams.get('error');
420
+ if (error) {
421
+ const desc = reqUrl.searchParams.get('error_description') || error;
422
+ geminiOAuthState = { status: 'error', error: desc, email: null };
423
+ geminiOAuthPending = null;
424
+ res.writeHead(200, { 'Content-Type': 'text/html' });
425
+ res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
426
+ return;
427
+ }
423
428
 
424
- saveGeminiCredentials(tokens, email);
425
- geminiOAuthState = { status: 'success', error: null, email };
426
- geminiOAuthPending = null;
429
+ const code = reqUrl.searchParams.get('code');
430
+ const state = reqUrl.searchParams.get('state');
431
+ const email = await exchangeGeminiOAuthCode(code, state);
427
432
 
428
433
  res.writeHead(200, { 'Content-Type': 'text/html' });
429
434
  res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
430
435
  } catch (e) {
431
- geminiOAuthState = { status: 'error', error: e.message, email: null };
432
- geminiOAuthPending = null;
433
436
  res.writeHead(200, { 'Content-Type': 'text/html' });
434
437
  res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
435
438
  }
@@ -1127,7 +1130,7 @@ const server = http.createServer(async (req, res) => {
1127
1130
 
1128
1131
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1129
1132
  try {
1130
- const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1133
+ const authUrl = await startGeminiOAuth();
1131
1134
  sendJSON(req, res, 200, { authUrl });
1132
1135
  } catch (e) {
1133
1136
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -1141,6 +1144,42 @@ const server = http.createServer(async (req, res) => {
1141
1144
  return;
1142
1145
  }
1143
1146
 
1147
+ if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
1148
+ try {
1149
+ const body = await parseBody(req);
1150
+ const pastedUrl = (body.url || '').trim();
1151
+ if (!pastedUrl) {
1152
+ sendJSON(req, res, 400, { error: 'No URL provided' });
1153
+ return;
1154
+ }
1155
+
1156
+ let parsed;
1157
+ try { parsed = new URL(pastedUrl); } catch (_) {
1158
+ sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
1159
+ return;
1160
+ }
1161
+
1162
+ const error = parsed.searchParams.get('error');
1163
+ if (error) {
1164
+ const desc = parsed.searchParams.get('error_description') || error;
1165
+ geminiOAuthState = { status: 'error', error: desc, email: null };
1166
+ geminiOAuthPending = null;
1167
+ sendJSON(req, res, 200, { error: desc });
1168
+ return;
1169
+ }
1170
+
1171
+ const code = parsed.searchParams.get('code');
1172
+ const state = parsed.searchParams.get('state');
1173
+ const email = await exchangeGeminiOAuthCode(code, state);
1174
+ sendJSON(req, res, 200, { success: true, email });
1175
+ } catch (e) {
1176
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
1177
+ geminiOAuthPending = null;
1178
+ sendJSON(req, res, 400, { error: e.message });
1179
+ }
1180
+ return;
1181
+ }
1182
+
1144
1183
  const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
1145
1184
  if (agentAuthMatch && req.method === 'POST') {
1146
1185
  const agentId = agentAuthMatch[1];
@@ -1149,7 +1188,7 @@ const server = http.createServer(async (req, res) => {
1149
1188
 
1150
1189
  if (agentId === 'gemini') {
1151
1190
  try {
1152
- const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1191
+ const authUrl = await startGeminiOAuth();
1153
1192
  const conversationId = '__agent_auth__';
1154
1193
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1155
1194
  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() });
@@ -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,28 @@
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
+ showOAuthPasteModal();
221
+ cleanupOAuthPolling();
222
+ oauthPollInterval = setInterval(function() {
223
+ fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
224
+ if (status.status === 'success') {
225
+ cleanupOAuthPolling();
226
+ authRunning = false;
227
+ removeOAuthPasteModal();
228
+ refresh();
229
+ } else if (status.status === 'error') {
230
+ cleanupOAuthPolling();
231
+ authRunning = false;
232
+ removeOAuthPasteModal();
233
+ }
234
+ }).catch(function() {});
235
+ }, 1500);
236
+ oauthPollTimeout = setTimeout(function() {
237
+ cleanupOAuthPolling();
238
+ if (authRunning) { authRunning = false; removeOAuthPasteModal(); }
239
+ }, 5 * 60 * 1000);
240
+ }
144
241
  }
145
242
  }
146
243
  }).catch(function() {});
@@ -159,6 +256,8 @@
159
256
  if (term) term.write(data.data);
160
257
  } else if (data.type === 'script_stopped') {
161
258
  authRunning = false;
259
+ removeOAuthPasteModal();
260
+ cleanupOAuthPolling();
162
261
  var term = getTerminal();
163
262
  var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
164
263
  if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');