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 +2 -1
- package/server.js +292 -1
- package/static/js/agent-auth.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
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; }
|
package/static/js/agent-auth.js
CHANGED
|
@@ -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
|
}
|