agentgui 1.0.200 → 1.0.201

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.201",
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,229 @@ 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 getGeminiOAuthCreds() {
193
+ try {
194
+ const geminiPath = execSync('which gemini', { encoding: 'utf8' }).trim();
195
+ const realPath = fs.realpathSync(geminiPath);
196
+ const pkgRoot = path.resolve(path.dirname(realPath), '..');
197
+ const oauth2Path = path.join(pkgRoot, 'node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
198
+ const src = fs.readFileSync(oauth2Path, 'utf8');
199
+ const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
200
+ const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
201
+ if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
202
+ } catch {}
203
+ try {
204
+ const npmCacheDirs = [
205
+ path.join(os.homedir(), '.npm', '_npx'),
206
+ path.join(os.homedir(), '.cache', '.npm', '_npx'),
207
+ process.env.NPM_CACHE ? path.join(process.env.NPM_CACHE, '_npx') : null,
208
+ ].filter(Boolean);
209
+ for (const cacheDir of npmCacheDirs) {
210
+ if (!fs.existsSync(cacheDir)) continue;
211
+ for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
212
+ const oauth2Path = path.join(cacheDir, d, 'node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
213
+ if (fs.existsSync(oauth2Path)) {
214
+ const src = fs.readFileSync(oauth2Path, 'utf8');
215
+ const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
216
+ const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
217
+ if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
218
+ }
219
+ }
220
+ }
221
+ } catch {}
222
+ return null;
223
+ }
224
+ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
225
+ const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
226
+ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
227
+
228
+ let geminiOAuthState = { status: 'idle', error: null, email: null };
229
+ let geminiOAuthCallbackServer = null;
230
+
231
+ function getAvailablePort() {
232
+ return new Promise((resolve, reject) => {
233
+ const srv = net.createServer();
234
+ srv.listen(0, () => {
235
+ const port = srv.address().port;
236
+ srv.close(() => resolve(port));
237
+ });
238
+ srv.on('error', reject);
239
+ });
240
+ }
241
+
242
+ function saveGeminiCredentials(tokens, email) {
243
+ if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
244
+ fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
245
+ try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
246
+
247
+ let accounts = { active: null, old: [] };
248
+ try {
249
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
250
+ accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
251
+ }
252
+ } catch (_) {}
253
+
254
+ if (email) {
255
+ if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
256
+ accounts.old.push(accounts.active);
257
+ }
258
+ accounts.active = email;
259
+ }
260
+ fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
261
+ }
262
+
263
+ function geminiOAuthResultPage(title, message, success) {
264
+ const color = success ? '#10b981' : '#ef4444';
265
+ const icon = success ? '✓' : '✗';
266
+ return `<!DOCTYPE html><html><head><title>${title}</title></head>
267
+ <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;">
268
+ <div style="text-align:center;max-width:400px;padding:2rem;">
269
+ <div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
270
+ <h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
271
+ <p style="color:#9ca3af;">${message}</p>
272
+ <p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
273
+ </div></body></html>`;
274
+ }
275
+
276
+ async function startGeminiOAuth() {
277
+ if (geminiOAuthCallbackServer) {
278
+ try { geminiOAuthCallbackServer.close(); } catch (_) {}
279
+ geminiOAuthCallbackServer = null;
280
+ }
281
+
282
+ const creds = getGeminiOAuthCreds();
283
+ if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
284
+
285
+ const port = await getAvailablePort();
286
+ const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
287
+ const state = crypto.randomBytes(32).toString('hex');
288
+
289
+ const client = new OAuth2Client({
290
+ clientId: creds.clientId,
291
+ clientSecret: creds.clientSecret,
292
+ });
293
+
294
+ const authUrl = client.generateAuthUrl({
295
+ redirect_uri: redirectUri,
296
+ access_type: 'offline',
297
+ scope: GEMINI_SCOPES,
298
+ state,
299
+ });
300
+
301
+ geminiOAuthState = { status: 'pending', error: null, email: null };
302
+
303
+ return new Promise((resolve, reject) => {
304
+ const cbServer = http.createServer(async (req, res) => {
305
+ try {
306
+ const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
307
+ if (reqUrl.pathname !== '/oauth2callback') {
308
+ res.writeHead(404);
309
+ res.end('Not found');
310
+ return;
311
+ }
312
+
313
+ const error = reqUrl.searchParams.get('error');
314
+ if (error) {
315
+ const desc = reqUrl.searchParams.get('error_description') || error;
316
+ geminiOAuthState = { status: 'error', error: desc, email: null };
317
+ res.writeHead(200, { 'Content-Type': 'text/html' });
318
+ res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
319
+ cbServer.close();
320
+ return;
321
+ }
322
+
323
+ if (reqUrl.searchParams.get('state') !== state) {
324
+ geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
325
+ res.writeHead(200, { 'Content-Type': 'text/html' });
326
+ res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
327
+ cbServer.close();
328
+ return;
329
+ }
330
+
331
+ const code = reqUrl.searchParams.get('code');
332
+ if (!code) {
333
+ geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
334
+ res.writeHead(200, { 'Content-Type': 'text/html' });
335
+ res.end(geminiOAuthResultPage('Authentication Failed', 'No authorization code received.', false));
336
+ cbServer.close();
337
+ return;
338
+ }
339
+
340
+ const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
341
+ client.setCredentials(tokens);
342
+
343
+ let email = '';
344
+ try {
345
+ const { token } = await client.getAccessToken();
346
+ if (token) {
347
+ const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
348
+ headers: { Authorization: `Bearer ${token}` }
349
+ });
350
+ if (resp.ok) {
351
+ const info = await resp.json();
352
+ email = info.email || '';
353
+ }
354
+ }
355
+ } catch (_) {}
356
+
357
+ saveGeminiCredentials(tokens, email);
358
+ geminiOAuthState = { status: 'success', error: null, email };
359
+
360
+ res.writeHead(200, { 'Content-Type': 'text/html' });
361
+ res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
362
+ cbServer.close();
363
+ } catch (e) {
364
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
365
+ res.writeHead(200, { 'Content-Type': 'text/html' });
366
+ res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
367
+ cbServer.close();
368
+ }
369
+ });
370
+
371
+ cbServer.on('error', (err) => {
372
+ geminiOAuthState = { status: 'error', error: err.message, email: null };
373
+ reject(err);
374
+ });
375
+
376
+ cbServer.listen(port, '127.0.0.1', () => {
377
+ geminiOAuthCallbackServer = cbServer;
378
+ resolve(authUrl);
379
+ });
380
+
381
+ setTimeout(() => {
382
+ if (geminiOAuthState.status === 'pending') {
383
+ geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
384
+ try { cbServer.close(); } catch (_) {}
385
+ }
386
+ }, 5 * 60 * 1000);
387
+ });
388
+ }
389
+
390
+ function getGeminiOAuthStatus() {
391
+ try {
392
+ if (fs.existsSync(GEMINI_OAUTH_FILE)) {
393
+ const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
394
+ if (creds.refresh_token || creds.access_token) {
395
+ let email = '';
396
+ try {
397
+ if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
398
+ const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
399
+ email = accts.active || '';
400
+ }
401
+ } catch (_) {}
402
+ return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
403
+ }
404
+ }
405
+ } catch (_) {}
406
+ return null;
407
+ }
408
+
183
409
  const PROVIDER_CONFIGS = {
184
410
  'anthropic': {
185
411
  name: 'Anthropic', configPaths: [
@@ -255,6 +481,13 @@ function maskKey(key) {
255
481
  function getProviderConfigs() {
256
482
  const configs = {};
257
483
  for (const [providerId, config] of Object.entries(PROVIDER_CONFIGS)) {
484
+ if (providerId === 'google') {
485
+ const oauthStatus = getGeminiOAuthStatus();
486
+ if (oauthStatus) {
487
+ configs[providerId] = { name: config.name, ...oauthStatus };
488
+ continue;
489
+ }
490
+ }
258
491
  for (const configPath of config.configPaths) {
259
492
  try {
260
493
  if (fs.existsSync(configPath)) {
@@ -792,15 +1025,29 @@ const server = http.createServer(async (req, res) => {
792
1025
  status.detail = 'no credentials';
793
1026
  }
794
1027
  } else if (agent.id === 'gemini') {
1028
+ const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
795
1029
  const acctFile = path.join(os.homedir(), '.gemini', 'google_accounts.json');
1030
+ let hasOAuth = false;
1031
+ if (fs.existsSync(oauthFile)) {
1032
+ try {
1033
+ const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
1034
+ if (creds.refresh_token || creds.access_token) hasOAuth = true;
1035
+ } catch (_) {}
1036
+ }
796
1037
  if (fs.existsSync(acctFile)) {
797
1038
  const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
798
1039
  if (accts.active) {
799
1040
  status.authenticated = true;
800
1041
  status.detail = accts.active;
1042
+ } else if (hasOAuth) {
1043
+ status.authenticated = true;
1044
+ status.detail = 'oauth';
801
1045
  } else {
802
1046
  status.detail = 'logged out';
803
1047
  }
1048
+ } else if (hasOAuth) {
1049
+ status.authenticated = true;
1050
+ status.detail = 'oauth';
804
1051
  } else {
805
1052
  status.detail = 'no credentials';
806
1053
  }
@@ -825,16 +1072,60 @@ const server = http.createServer(async (req, res) => {
825
1072
  return;
826
1073
  }
827
1074
 
1075
+ if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
1076
+ try {
1077
+ const authUrl = await startGeminiOAuth();
1078
+ sendJSON(req, res, 200, { authUrl });
1079
+ } catch (e) {
1080
+ sendJSON(req, res, 500, { error: e.message });
1081
+ }
1082
+ return;
1083
+ }
1084
+
1085
+ if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
1086
+ sendJSON(req, res, 200, geminiOAuthState);
1087
+ return;
1088
+ }
1089
+
828
1090
  const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
829
1091
  if (agentAuthMatch && req.method === 'POST') {
830
1092
  const agentId = agentAuthMatch[1];
831
1093
  const agent = discoveredAgents.find(a => a.id === agentId);
832
1094
  if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
833
1095
 
1096
+ if (agentId === 'gemini') {
1097
+ try {
1098
+ const authUrl = await startGeminiOAuth();
1099
+ const conversationId = '__agent_auth__';
1100
+ broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
1101
+ 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() });
1102
+
1103
+ const pollId = setInterval(() => {
1104
+ if (geminiOAuthState.status === 'success') {
1105
+ clearInterval(pollId);
1106
+ const email = geminiOAuthState.email || '';
1107
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
1108
+ broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
1109
+ } else if (geminiOAuthState.status === 'error') {
1110
+ clearInterval(pollId);
1111
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${geminiOAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
1112
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: geminiOAuthState.error, timestamp: Date.now() });
1113
+ }
1114
+ }, 1000);
1115
+
1116
+ setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
1117
+
1118
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl });
1119
+ return;
1120
+ } catch (e) {
1121
+ sendJSON(req, res, 500, { error: e.message });
1122
+ return;
1123
+ }
1124
+ }
1125
+
834
1126
  const authCommands = {
835
1127
  'claude-code': { cmd: 'claude', args: ['setup-token'] },
836
1128
  'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
837
- 'gemini': { cmd: 'gemini', args: [] }
838
1129
  };
839
1130
  const authCmd = authCommands[agentId];
840
1131
  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
  }