agentgui 1.0.203 → 1.0.206

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/server.js +91 -97
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.203",
3
+ "version": "1.0.206",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -27,7 +27,7 @@
27
27
  "better-sqlite3": "^12.6.2",
28
28
  "busboy": "^1.6.0",
29
29
  "express": "^5.2.1",
30
- "fsbrowse": "^0.2.17",
30
+ "fsbrowse": "^0.2.18",
31
31
  "google-auth-library": "^10.5.0",
32
32
  "onnxruntime-node": "^1.24.1",
33
33
  "webtalk": "github:AnEntrypoint/webtalk",
package/server.js CHANGED
@@ -3,7 +3,6 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import zlib from 'zlib';
6
- import net from 'net';
7
6
  import crypto from 'crypto';
8
7
  import { fileURLToPath } from 'url';
9
8
  import { WebSocketServer } from 'ws';
@@ -245,17 +244,19 @@ const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
245
244
  const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
246
245
 
247
246
  let geminiOAuthState = { status: 'idle', error: null, email: null };
248
- let geminiOAuthCallbackServer = null;
249
-
250
- function getAvailablePort() {
251
- return new Promise((resolve, reject) => {
252
- const srv = net.createServer();
253
- srv.listen(0, () => {
254
- const port = srv.address().port;
255
- srv.close(() => resolve(port));
256
- });
257
- srv.on('error', reject);
258
- });
247
+ let geminiOAuthPending = null;
248
+
249
+ function buildBaseUrl(req) {
250
+ const override = process.env.AGENTGUI_BASE_URL;
251
+ if (override) return override.replace(/\/+$/, '');
252
+ const fwdProto = req.headers['x-forwarded-proto'];
253
+ const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
254
+ if (fwdHost) {
255
+ const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
256
+ const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
257
+ return `${proto}://${cleanHost}`;
258
+ }
259
+ return `http://127.0.0.1:${PORT}`;
259
260
  }
260
261
 
261
262
  function saveGeminiCredentials(tokens, email) {
@@ -292,17 +293,11 @@ function geminiOAuthResultPage(title, message, success) {
292
293
  </div></body></html>`;
293
294
  }
294
295
 
295
- async function startGeminiOAuth() {
296
- if (geminiOAuthCallbackServer) {
297
- try { geminiOAuthCallbackServer.close(); } catch (_) {}
298
- geminiOAuthCallbackServer = null;
299
- }
300
-
296
+ async function startGeminiOAuth(baseUrl) {
301
297
  const creds = getGeminiOAuthCreds();
302
298
  if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
303
299
 
304
- const port = await getAvailablePort();
305
- const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
300
+ const redirectUri = `${baseUrl}${BASE_URL}/oauth2callback`;
306
301
  const state = crypto.randomBytes(32).toString('hex');
307
302
 
308
303
  const client = new OAuth2Client({
@@ -317,93 +312,87 @@ async function startGeminiOAuth() {
317
312
  state,
318
313
  });
319
314
 
315
+ geminiOAuthPending = { client, redirectUri, state };
320
316
  geminiOAuthState = { status: 'pending', error: null, email: null };
321
317
 
322
- return new Promise((resolve, reject) => {
323
- const cbServer = http.createServer(async (req, res) => {
324
- try {
325
- const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
326
- if (reqUrl.pathname !== '/oauth2callback') {
327
- res.writeHead(404);
328
- res.end('Not found');
329
- return;
330
- }
331
-
332
- const error = reqUrl.searchParams.get('error');
333
- if (error) {
334
- const desc = reqUrl.searchParams.get('error_description') || error;
335
- geminiOAuthState = { status: 'error', error: desc, email: null };
336
- res.writeHead(200, { 'Content-Type': 'text/html' });
337
- res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
338
- cbServer.close();
339
- return;
340
- }
318
+ setTimeout(() => {
319
+ if (geminiOAuthState.status === 'pending') {
320
+ geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
321
+ geminiOAuthPending = null;
322
+ }
323
+ }, 5 * 60 * 1000);
341
324
 
342
- if (reqUrl.searchParams.get('state') !== state) {
343
- geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
344
- res.writeHead(200, { 'Content-Type': 'text/html' });
345
- res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
346
- cbServer.close();
347
- return;
348
- }
325
+ return authUrl;
326
+ }
349
327
 
350
- const code = reqUrl.searchParams.get('code');
351
- if (!code) {
352
- geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
353
- res.writeHead(200, { 'Content-Type': 'text/html' });
354
- res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
355
- cbServer.close();
356
- return;
357
- }
328
+ async function handleGeminiOAuthCallback(req, res) {
329
+ const reqUrl = new URL(req.url, buildBaseUrl(req));
358
330
 
359
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
360
- client.setCredentials(tokens);
331
+ if (!geminiOAuthPending) {
332
+ res.writeHead(200, { 'Content-Type': 'text/html' });
333
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
334
+ return;
335
+ }
361
336
 
362
- let email = '';
363
- try {
364
- const { token } = await client.getAccessToken();
365
- if (token) {
366
- const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
367
- headers: { Authorization: `Bearer ${token}` }
368
- });
369
- if (resp.ok) {
370
- const info = await resp.json();
371
- email = info.email || '';
372
- }
373
- }
374
- } catch (_) {}
337
+ const error = reqUrl.searchParams.get('error');
338
+ if (error) {
339
+ const desc = reqUrl.searchParams.get('error_description') || error;
340
+ geminiOAuthState = { status: 'error', error: desc, email: null };
341
+ geminiOAuthPending = null;
342
+ res.writeHead(200, { 'Content-Type': 'text/html' });
343
+ res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
344
+ return;
345
+ }
375
346
 
376
- saveGeminiCredentials(tokens, email);
377
- geminiOAuthState = { status: 'success', error: null, email };
347
+ const { client, redirectUri, state: expectedState } = geminiOAuthPending;
378
348
 
379
- res.writeHead(200, { 'Content-Type': 'text/html' });
380
- res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
381
- cbServer.close();
382
- } catch (e) {
383
- geminiOAuthState = { status: 'error', error: e.message, email: null };
384
- res.writeHead(200, { 'Content-Type': 'text/html' });
385
- res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
386
- cbServer.close();
387
- }
388
- });
349
+ if (reqUrl.searchParams.get('state') !== expectedState) {
350
+ geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
351
+ geminiOAuthPending = null;
352
+ res.writeHead(200, { 'Content-Type': 'text/html' });
353
+ res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
354
+ return;
355
+ }
389
356
 
390
- cbServer.on('error', (err) => {
391
- geminiOAuthState = { status: 'error', error: err.message, email: null };
392
- reject(err);
393
- });
357
+ const code = reqUrl.searchParams.get('code');
358
+ if (!code) {
359
+ geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
360
+ geminiOAuthPending = null;
361
+ res.writeHead(200, { 'Content-Type': 'text/html' });
362
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
363
+ return;
364
+ }
394
365
 
395
- cbServer.listen(port, '127.0.0.1', () => {
396
- geminiOAuthCallbackServer = cbServer;
397
- resolve(authUrl);
398
- });
366
+ try {
367
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
368
+ client.setCredentials(tokens);
399
369
 
400
- setTimeout(() => {
401
- if (geminiOAuthState.status === 'pending') {
402
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
403
- try { cbServer.close(); } catch (_) {}
370
+ let email = '';
371
+ try {
372
+ const { token } = await client.getAccessToken();
373
+ if (token) {
374
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
375
+ headers: { Authorization: `Bearer ${token}` }
376
+ });
377
+ if (resp.ok) {
378
+ const info = await resp.json();
379
+ email = info.email || '';
380
+ }
404
381
  }
405
- }, 5 * 60 * 1000);
406
- });
382
+ } catch (_) {}
383
+
384
+ saveGeminiCredentials(tokens, email);
385
+ geminiOAuthState = { status: 'success', error: null, email };
386
+ geminiOAuthPending = null;
387
+
388
+ res.writeHead(200, { 'Content-Type': 'text/html' });
389
+ res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
390
+ } catch (e) {
391
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
392
+ geminiOAuthPending = null;
393
+ res.writeHead(200, { 'Content-Type': 'text/html' });
394
+ res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
395
+ }
407
396
  }
408
397
 
409
398
  function getGeminiOAuthStatus() {
@@ -620,6 +609,11 @@ const server = http.createServer(async (req, res) => {
620
609
  // Remove query parameters from routePath for matching
621
610
  const pathOnly = routePath.split('?')[0];
622
611
 
612
+ if (pathOnly === '/oauth2callback' && req.method === 'GET') {
613
+ await handleGeminiOAuthCallback(req, res);
614
+ return;
615
+ }
616
+
623
617
  if (pathOnly === '/api/conversations' && req.method === 'GET') {
624
618
  sendJSON(req, res, 200, { conversations: queries.getConversationsList() });
625
619
  return;
@@ -1093,7 +1087,7 @@ const server = http.createServer(async (req, res) => {
1093
1087
 
1094
1088
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1095
1089
  try {
1096
- const authUrl = await startGeminiOAuth();
1090
+ const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1097
1091
  sendJSON(req, res, 200, { authUrl });
1098
1092
  } catch (e) {
1099
1093
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -1115,7 +1109,7 @@ const server = http.createServer(async (req, res) => {
1115
1109
 
1116
1110
  if (agentId === 'gemini') {
1117
1111
  try {
1118
- const authUrl = await startGeminiOAuth();
1112
+ const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1119
1113
  const conversationId = '__agent_auth__';
1120
1114
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1121
1115
  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() });