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.
- package/package.json +1 -1
- package/server.js +116 -116
package/package.json
CHANGED
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
|
-
|
|
177
|
-
|
|
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 =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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]
|
|
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
|
|
249
|
-
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
377
|
-
geminiOAuthState = { status: 'success', error: null, email };
|
|
353
|
+
const { client, redirectUri, state: expectedState } = geminiOAuthPending;
|
|
378
354
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
});
|
|
372
|
+
try {
|
|
373
|
+
const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
|
|
374
|
+
client.setCredentials(tokens);
|
|
399
375
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
}
|
|
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() });
|