agentgui 1.0.200 → 1.0.202

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.200",
3
+ "version": "1.0.202",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -28,6 +28,7 @@
28
28
  "busboy": "^1.6.0",
29
29
  "express": "^5.2.1",
30
30
  "fsbrowse": "^0.2.13",
31
+ "google-auth-library": "^10.5.0",
31
32
  "onnxruntime-node": "^1.24.1",
32
33
  "webtalk": "github:AnEntrypoint/webtalk",
33
34
  "ws": "^8.14.2"
package/server.js CHANGED
@@ -3,10 +3,13 @@ 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
+ import crypto from 'crypto';
6
8
  import { fileURLToPath } from 'url';
7
9
  import { WebSocketServer } from 'ws';
8
10
  import { execSync, spawn } from 'child_process';
9
11
  import { createRequire } from 'module';
12
+ import { OAuth2Client } from 'google-auth-library';
10
13
  import { queries } from './database.js';
11
14
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
12
15
  let speechModule = null;
@@ -180,6 +183,248 @@ function discoverAgents() {
180
183
 
181
184
  const discoveredAgents = discoverAgents();
182
185
 
186
+ const GEMINI_SCOPES = [
187
+ 'https://www.googleapis.com/auth/cloud-platform',
188
+ 'https://www.googleapis.com/auth/userinfo.email',
189
+ 'https://www.googleapis.com/auth/userinfo.profile',
190
+ ];
191
+
192
+ function extractOAuthFromFile(oauth2Path) {
193
+ try {
194
+ const src = fs.readFileSync(oauth2Path, 'utf8');
195
+ const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
196
+ const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
197
+ if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
198
+ } catch {}
199
+ return null;
200
+ }
201
+
202
+ function getGeminiOAuthCreds() {
203
+ const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
204
+ 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;
210
+ } catch (e) {
211
+ console.error('[gemini-oauth] which gemini lookup failed:', e.message);
212
+ }
213
+ try {
214
+ const npmCacheDirs = new Set();
215
+ const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
216
+ addDir(path.join(os.homedir(), '.npm'));
217
+ addDir(path.join(os.homedir(), '.cache', '.npm'));
218
+ if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
219
+ if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
220
+ try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
221
+ for (const cacheDir of npmCacheDirs) {
222
+ if (!fs.existsSync(cacheDir)) continue;
223
+ for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
224
+ const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
225
+ if (result) return result;
226
+ }
227
+ }
228
+ } catch (e) {
229
+ console.error('[gemini-oauth] npm cache scan failed:', e.message);
230
+ }
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
+ console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
241
+ return null;
242
+ }
243
+ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
244
+ const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
245
+ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
246
+
247
+ 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
+ });
259
+ }
260
+
261
+ function saveGeminiCredentials(tokens, email) {
262
+ if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
263
+ fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
264
+ try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
265
+
266
+ let accounts = { active: null, old: [] };
267
+ try {
268
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
269
+ accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
270
+ }
271
+ } catch (_) {}
272
+
273
+ if (email) {
274
+ if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
275
+ accounts.old.push(accounts.active);
276
+ }
277
+ accounts.active = email;
278
+ }
279
+ fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
280
+ }
281
+
282
+ function geminiOAuthResultPage(title, message, success) {
283
+ const color = success ? '#10b981' : '#ef4444';
284
+ const icon = success ? '✓' : '✗';
285
+ return `<!DOCTYPE html><html><head><title>${title}</title></head>
286
+ <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;">
287
+ <div style="text-align:center;max-width:400px;padding:2rem;">
288
+ <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
289
+ <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
290
+ <p style="color:#9ca3af;">${message}</p>
291
+ <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
292
+ </div></body></html>`;
293
+ }
294
+
295
+ async function startGeminiOAuth() {
296
+ if (geminiOAuthCallbackServer) {
297
+ try { geminiOAuthCallbackServer.close(); } catch (_) {}
298
+ geminiOAuthCallbackServer = null;
299
+ }
300
+
301
+ const creds = getGeminiOAuthCreds();
302
+ if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
303
+
304
+ const port = await getAvailablePort();
305
+ const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
306
+ const state = crypto.randomBytes(32).toString('hex');
307
+
308
+ const client = new OAuth2Client({
309
+ clientId: creds.clientId,
310
+ clientSecret: creds.clientSecret,
311
+ });
312
+
313
+ const authUrl = client.generateAuthUrl({
314
+ redirect_uri: redirectUri,
315
+ access_type: 'offline',
316
+ scope: GEMINI_SCOPES,
317
+ state,
318
+ });
319
+
320
+ geminiOAuthState = { status: 'pending', error: null, email: null };
321
+
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
+ }
341
+
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
+ }
349
+
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
+ }
358
+
359
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
360
+ client.setCredentials(tokens);
361
+
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 (_) {}
375
+
376
+ saveGeminiCredentials(tokens, email);
377
+ geminiOAuthState = { status: 'success', error: null, email };
378
+
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
+ });
389
+
390
+ cbServer.on('error', (err) => {
391
+ geminiOAuthState = { status: 'error', error: err.message, email: null };
392
+ reject(err);
393
+ });
394
+
395
+ cbServer.listen(port, '127.0.0.1', () => {
396
+ geminiOAuthCallbackServer = cbServer;
397
+ resolve(authUrl);
398
+ });
399
+
400
+ setTimeout(() => {
401
+ if (geminiOAuthState.status === 'pending') {
402
+ geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
403
+ try { cbServer.close(); } catch (_) {}
404
+ }
405
+ }, 5 * 60 * 1000);
406
+ });
407
+ }
408
+
409
+ function getGeminiOAuthStatus() {
410
+ try {
411
+ if (fs.existsSync(GEMINI_OAUTH_FILE)) {
412
+ const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
413
+ if (creds.refresh_token || creds.access_token) {
414
+ let email = '';
415
+ try {
416
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
417
+ const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
418
+ email = accts.active || '';
419
+ }
420
+ } catch (_) {}
421
+ return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
422
+ }
423
+ }
424
+ } catch (_) {}
425
+ return null;
426
+ }
427
+
183
428
  const PROVIDER_CONFIGS = {
184
429
  'anthropic': {
185
430
  name: 'Anthropic', configPaths: [
@@ -255,6 +500,13 @@ function maskKey(key) {
255
500
  function getProviderConfigs() {
256
501
  const configs = {};
257
502
  for (const [providerId, config] of Object.entries(PROVIDER_CONFIGS)) {
503
+ if (providerId === 'google') {
504
+ const oauthStatus = getGeminiOAuthStatus();
505
+ if (oauthStatus) {
506
+ configs[providerId] = { name: config.name, ...oauthStatus };
507
+ continue;
508
+ }
509
+ }
258
510
  for (const configPath of config.configPaths) {
259
511
  try {
260
512
  if (fs.existsSync(configPath)) {
@@ -792,15 +1044,29 @@ const server = http.createServer(async (req, res) => {
792
1044
  status.detail = 'no credentials';
793
1045
  }
794
1046
  } else if (agent.id === 'gemini') {
1047
+ const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
795
1048
  const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
1049
+ let hasOAuth = false;
1050
+ if (fs.existsSync(oauthFile)) {
1051
+ try {
1052
+ const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
1053
+ if (creds.refresh_token || creds.access_token) hasOAuth = true;
1054
+ } catch (_) {}
1055
+ }
796
1056
  if (fs.existsSync(acctFile)) {
797
1057
  const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
798
1058
  if (accts.active) {
799
1059
  status.authenticated = true;
800
1060
  status.detail = accts.active;
1061
+ } else if (hasOAuth) {
1062
+ status.authenticated = true;
1063
+ status.detail = 'oauth';
801
1064
  } else {
802
1065
  status.detail = 'logged out';
803
1066
  }
1067
+ } else if (hasOAuth) {
1068
+ status.authenticated = true;
1069
+ status.detail = 'oauth';
804
1070
  } else {
805
1071
  status.detail = 'no credentials';
806
1072
  }
@@ -825,16 +1091,62 @@ const server = http.createServer(async (req, res) => {
825
1091
  return;
826
1092
  }
827
1093
 
1094
+ if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1095
+ try {
1096
+ const authUrl = await startGeminiOAuth();
1097
+ sendJSON(req, res, 200, { authUrl });
1098
+ } catch (e) {
1099
+ console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
1100
+ sendJSON(req, res, 500, { error: e.message });
1101
+ }
1102
+ return;
1103
+ }
1104
+
1105
+ if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
1106
+ sendJSON(req, res, 200, geminiOAuthState);
1107
+ return;
1108
+ }
1109
+
828
1110
  const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
829
1111
  if (agentAuthMatch && req.method === 'POST') {
830
1112
  const agentId = agentAuthMatch[1];
831
1113
  const agent = discoveredAgents.find(a => a.id === agentId);
832
1114
  if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
833
1115
 
1116
+ if (agentId === 'gemini') {
1117
+ try {
1118
+ const authUrl = await startGeminiOAuth();
1119
+ const conversationId = '__agent_auth__';
1120
+ broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
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() });
1122
+
1123
+ const pollId = setInterval(() => {
1124
+ if (geminiOAuthState.status === 'success') {
1125
+ clearInterval(pollId);
1126
+ const email = geminiOAuthState.email || '';
1127
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
1128
+ broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
1129
+ } else if (geminiOAuthState.status === 'error') {
1130
+ clearInterval(pollId);
1131
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${geminiOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
1132
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: geminiOAuthState.error, timestamp: Date.now() });
1133
+ }
1134
+ }, 1000);
1135
+
1136
+ setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
1137
+
1138
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl });
1139
+ return;
1140
+ } catch (e) {
1141
+ console.error('[gemini-oauth] /api/agents/gemini/auth failed:', e);
1142
+ sendJSON(req, res, 500, { error: e.message });
1143
+ return;
1144
+ }
1145
+ }
1146
+
834
1147
  const authCommands = {
835
1148
  'claude-code': { cmd: 'claude', args: ['setup-token'] },
836
1149
  'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
837
- 'gemini': { cmd: 'gemini', args: [] }
838
1150
  };
839
1151
  const authCmd = authCommands[agentId];
840
1152
  if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
@@ -139,6 +139,9 @@
139
139
  authRunning = true; showTerminalTab(); switchToTerminalView();
140
140
  var term = getTerminal();
141
141
  if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
142
+ if (data.authUrl) {
143
+ window.open(data.authUrl, '_blank');
144
+ }
142
145
  }
143
146
  }).catch(function() {});
144
147
  }