agentgui 1.0.203 → 1.0.207

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 +1 -1
  2. package/server.js +116 -116
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.203",
3
+ "version": "1.0.207",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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';
@@ -155,6 +154,21 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
155
154
  router(req, res, next);
156
155
  });
157
156
 
157
+ function findCommand(cmd) {
158
+ const isWindows = os.platform() === 'win32';
159
+ try {
160
+ if (isWindows) {
161
+ const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
162
+ return result.split('\n')[0].trim();
163
+ } else {
164
+ const result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
165
+ return result;
166
+ }
167
+ } catch (_) {
168
+ return null;
169
+ }
170
+ }
171
+
158
172
  function discoverAgents() {
159
173
  const agents = [];
160
174
  const binaries = [
@@ -173,10 +187,8 @@ function discoverAgents() {
173
187
  { cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F' },
174
188
  ];
175
189
  for (const bin of binaries) {
176
- try {
177
- const result = execSync(`which ${bin.cmd} 2>/dev/null`, { encoding: 'utf-8' }).trim();
178
- if (result) agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result });
179
- } catch (_) {}
190
+ const result = findCommand(bin.cmd);
191
+ if (result) agents.push({ id: bin.id, name: bin.name, icon: bin.icon, path: result });
180
192
  }
181
193
  return agents;
182
194
  }
@@ -202,13 +214,15 @@ function extractOAuthFromFile(oauth2Path) {
202
214
  function getGeminiOAuthCreds() {
203
215
  const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
204
216
  try {
205
- const geminiPath = execSync('which gemini', { encoding: 'utf8' }).trim();
206
- const realPath = fs.realpathSync(geminiPath);
207
- const pkgRoot = path.resolve(path.dirname(realPath), '..');
208
- const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
209
- if (result) return result;
217
+ const geminiPath = findCommand('gemini');
218
+ if (geminiPath) {
219
+ const realPath = fs.realpathSync(geminiPath);
220
+ const pkgRoot = path.resolve(path.dirname(realPath), '..');
221
+ const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
222
+ if (result) return result;
223
+ }
210
224
  } catch (e) {
211
- console.error('[gemini-oauth] which gemini lookup failed:', e.message);
225
+ console.error('[gemini-oauth] gemini lookup failed:', e.message);
212
226
  }
213
227
  try {
214
228
  const npmCacheDirs = new Set();
@@ -228,15 +242,6 @@ function getGeminiOAuthCreds() {
228
242
  } catch (e) {
229
243
  console.error('[gemini-oauth] npm cache scan failed:', e.message);
230
244
  }
231
- try {
232
- const found = execSync('find / -path "*/gemini-cli-core/dist/src/code_assist/oauth2.js" -maxdepth 10 2>/dev/null | head -1', { encoding: 'utf8', timeout: 10000 }).trim();
233
- if (found) {
234
- const result = extractOAuthFromFile(found);
235
- if (result) return result;
236
- }
237
- } catch (e) {
238
- console.error('[gemini-oauth] filesystem search failed:', e.message);
239
- }
240
245
  console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
241
246
  return null;
242
247
  }
@@ -245,17 +250,19 @@ const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
245
250
  const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
246
251
 
247
252
  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
- });
253
+ let geminiOAuthPending = null;
254
+
255
+ function buildBaseUrl(req) {
256
+ const override = process.env.AGENTGUI_BASE_URL;
257
+ if (override) return override.replace(/\/+$/, '');
258
+ const fwdProto = req.headers['x-forwarded-proto'];
259
+ const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
260
+ if (fwdHost) {
261
+ const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
262
+ const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
263
+ return `${proto}://${cleanHost}`;
264
+ }
265
+ return `http://127.0.0.1:${PORT}`;
259
266
  }
260
267
 
261
268
  function saveGeminiCredentials(tokens, email) {
@@ -292,17 +299,11 @@ function geminiOAuthResultPage(title, message, success) {
292
299
  </div></body></html>`;
293
300
  }
294
301
 
295
- async function startGeminiOAuth() {
296
- if (geminiOAuthCallbackServer) {
297
- try { geminiOAuthCallbackServer.close(); } catch (_) {}
298
- geminiOAuthCallbackServer = null;
299
- }
300
-
302
+ async function startGeminiOAuth(baseUrl) {
301
303
  const creds = getGeminiOAuthCreds();
302
304
  if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
303
305
 
304
- const port = await getAvailablePort();
305
- const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
306
+ const redirectUri = `${baseUrl}${BASE_URL}/oauth2callback`;
306
307
  const state = crypto.randomBytes(32).toString('hex');
307
308
 
308
309
  const client = new OAuth2Client({
@@ -317,93 +318,87 @@ async function startGeminiOAuth() {
317
318
  state,
318
319
  });
319
320
 
321
+ geminiOAuthPending = { client, redirectUri, state };
320
322
  geminiOAuthState = { status: 'pending', error: null, email: null };
321
323
 
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
- }
324
+ setTimeout(() => {
325
+ if (geminiOAuthState.status === 'pending') {
326
+ geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
327
+ geminiOAuthPending = null;
328
+ }
329
+ }, 5 * 60 * 1000);
341
330
 
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
- }
331
+ return authUrl;
332
+ }
349
333
 
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
- }
334
+ async function handleGeminiOAuthCallback(req, res) {
335
+ const reqUrl = new URL(req.url, buildBaseUrl(req));
358
336
 
359
- const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
360
- client.setCredentials(tokens);
337
+ if (!geminiOAuthPending) {
338
+ res.writeHead(200, { 'Content-Type': 'text/html' });
339
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
340
+ return;
341
+ }
361
342
 
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 (_) {}
343
+ const error = reqUrl.searchParams.get('error');
344
+ if (error) {
345
+ const desc = reqUrl.searchParams.get('error_description') || error;
346
+ geminiOAuthState = { status: 'error', error: desc, email: null };
347
+ geminiOAuthPending = null;
348
+ res.writeHead(200, { 'Content-Type': 'text/html' });
349
+ res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
350
+ return;
351
+ }
375
352
 
376
- saveGeminiCredentials(tokens, email);
377
- geminiOAuthState = { status: 'success', error: null, email };
353
+ const { client, redirectUri, state: expectedState } = geminiOAuthPending;
378
354
 
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
- });
355
+ if (reqUrl.searchParams.get('state') !== expectedState) {
356
+ geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
357
+ geminiOAuthPending = null;
358
+ res.writeHead(200, { 'Content-Type': 'text/html' });
359
+ res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
360
+ return;
361
+ }
389
362
 
390
- cbServer.on('error', (err) => {
391
- geminiOAuthState = { status: 'error', error: err.message, email: null };
392
- reject(err);
393
- });
363
+ const code = reqUrl.searchParams.get('code');
364
+ if (!code) {
365
+ geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
366
+ geminiOAuthPending = null;
367
+ res.writeHead(200, { 'Content-Type': 'text/html' });
368
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
369
+ return;
370
+ }
394
371
 
395
- cbServer.listen(port, '127.0.0.1', () => {
396
- geminiOAuthCallbackServer = cbServer;
397
- resolve(authUrl);
398
- });
372
+ try {
373
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
374
+ client.setCredentials(tokens);
399
375
 
400
- setTimeout(() => {
401
- if (geminiOAuthState.status === 'pending') {
402
- geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
403
- try { cbServer.close(); } catch (_) {}
376
+ let email = '';
377
+ try {
378
+ const { token } = await client.getAccessToken();
379
+ if (token) {
380
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
381
+ headers: { Authorization: `Bearer ${token}` }
382
+ });
383
+ if (resp.ok) {
384
+ const info = await resp.json();
385
+ email = info.email || '';
386
+ }
404
387
  }
405
- }, 5 * 60 * 1000);
406
- });
388
+ } catch (_) {}
389
+
390
+ saveGeminiCredentials(tokens, email);
391
+ geminiOAuthState = { status: 'success', error: null, email };
392
+ geminiOAuthPending = null;
393
+
394
+ res.writeHead(200, { 'Content-Type': 'text/html' });
395
+ res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
396
+ } catch (e) {
397
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
398
+ geminiOAuthPending = null;
399
+ res.writeHead(200, { 'Content-Type': 'text/html' });
400
+ res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
401
+ }
407
402
  }
408
403
 
409
404
  function getGeminiOAuthStatus() {
@@ -620,6 +615,11 @@ const server = http.createServer(async (req, res) => {
620
615
  // Remove query parameters from routePath for matching
621
616
  const pathOnly = routePath.split('?')[0];
622
617
 
618
+ if (pathOnly === '/oauth2callback' && req.method === 'GET') {
619
+ await handleGeminiOAuthCallback(req, res);
620
+ return;
621
+ }
622
+
623
623
  if (pathOnly === '/api/conversations' && req.method === 'GET') {
624
624
  sendJSON(req, res, 200, { conversations: queries.getConversationsList() });
625
625
  return;
@@ -1093,7 +1093,7 @@ const server = http.createServer(async (req, res) => {
1093
1093
 
1094
1094
  if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1095
1095
  try {
1096
- const authUrl = await startGeminiOAuth();
1096
+ const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1097
1097
  sendJSON(req, res, 200, { authUrl });
1098
1098
  } catch (e) {
1099
1099
  console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
@@ -1115,7 +1115,7 @@ const server = http.createServer(async (req, res) => {
1115
1115
 
1116
1116
  if (agentId === 'gemini') {
1117
1117
  try {
1118
- const authUrl = await startGeminiOAuth();
1118
+ const authUrl = await startGeminiOAuth(buildBaseUrl(req));
1119
1119
  const conversationId = '__agent_auth__';
1120
1120
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1121
1121
  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() });