agentgui 1.0.199 → 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 +433 -1
- package/static/index.html +7 -2
- package/static/js/agent-auth.js +115 -78
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,350 @@ 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
|
+
|
|
409
|
+
const PROVIDER_CONFIGS = {
|
|
410
|
+
'anthropic': {
|
|
411
|
+
name: 'Anthropic', configPaths: [
|
|
412
|
+
path.join(os.homedir(), '.claude.json'),
|
|
413
|
+
path.join(os.homedir(), '.config', 'claude', 'settings.json'),
|
|
414
|
+
path.join(os.homedir(), '.anthropic.json')
|
|
415
|
+
],
|
|
416
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
417
|
+
},
|
|
418
|
+
'openai': {
|
|
419
|
+
name: 'OpenAI', configPaths: [
|
|
420
|
+
path.join(os.homedir(), '.openai.json'),
|
|
421
|
+
path.join(os.homedir(), '.config', 'openai', 'api-key')
|
|
422
|
+
],
|
|
423
|
+
configFormat: (apiKey, model) => ({ apiKey, defaultModel: model })
|
|
424
|
+
},
|
|
425
|
+
'google': {
|
|
426
|
+
name: 'Google Gemini', configPaths: [
|
|
427
|
+
path.join(os.homedir(), '.gemini.json'),
|
|
428
|
+
path.join(os.homedir(), '.config', 'gemini', 'credentials.json')
|
|
429
|
+
],
|
|
430
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
431
|
+
},
|
|
432
|
+
'openrouter': {
|
|
433
|
+
name: 'OpenRouter', configPaths: [
|
|
434
|
+
path.join(os.homedir(), '.openrouter.json'),
|
|
435
|
+
path.join(os.homedir(), '.config', 'openrouter', 'config.json')
|
|
436
|
+
],
|
|
437
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
438
|
+
},
|
|
439
|
+
'github': {
|
|
440
|
+
name: 'GitHub Models', configPaths: [
|
|
441
|
+
path.join(os.homedir(), '.github.json'),
|
|
442
|
+
path.join(os.homedir(), '.config', 'github-copilot.json')
|
|
443
|
+
],
|
|
444
|
+
configFormat: (apiKey, model) => ({ github_token: apiKey, default_model: model })
|
|
445
|
+
},
|
|
446
|
+
'azure': {
|
|
447
|
+
name: 'Azure OpenAI', configPaths: [
|
|
448
|
+
path.join(os.homedir(), '.azure.json'),
|
|
449
|
+
path.join(os.homedir(), '.config', 'azure-openai', 'config.json')
|
|
450
|
+
],
|
|
451
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, endpoint: '', default_model: model })
|
|
452
|
+
},
|
|
453
|
+
'anthropic-claude-code': {
|
|
454
|
+
name: 'Claude Code Max', configPaths: [
|
|
455
|
+
path.join(os.homedir(), '.claude', 'max.json'),
|
|
456
|
+
path.join(os.homedir(), '.config', 'claude-code', 'max.json')
|
|
457
|
+
],
|
|
458
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, plan: 'max', default_model: model })
|
|
459
|
+
},
|
|
460
|
+
'opencode': {
|
|
461
|
+
name: 'OpenCode', configPaths: [
|
|
462
|
+
path.join(os.homedir(), '.opencode', 'config.json'),
|
|
463
|
+
path.join(os.homedir(), '.config', 'opencode', 'config.json')
|
|
464
|
+
],
|
|
465
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model, providers: ['anthropic', 'openai', 'google'] })
|
|
466
|
+
},
|
|
467
|
+
'proxypilot': {
|
|
468
|
+
name: 'ProxyPilot', configPaths: [
|
|
469
|
+
path.join(os.homedir(), '.proxypilot', 'config.json'),
|
|
470
|
+
path.join(os.homedir(), '.config', 'proxypilot', 'config.json')
|
|
471
|
+
],
|
|
472
|
+
configFormat: (apiKey, model) => ({ api_key: apiKey, default_model: model })
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
function maskKey(key) {
|
|
477
|
+
if (!key || key.length < 8) return '****';
|
|
478
|
+
return '****' + key.slice(-4);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getProviderConfigs() {
|
|
482
|
+
const configs = {};
|
|
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
|
+
}
|
|
491
|
+
for (const configPath of config.configPaths) {
|
|
492
|
+
try {
|
|
493
|
+
if (fs.existsSync(configPath)) {
|
|
494
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
495
|
+
const parsed = JSON.parse(content);
|
|
496
|
+
const rawKey = parsed.api_key || parsed.apiKey || parsed.github_token || '';
|
|
497
|
+
configs[providerId] = {
|
|
498
|
+
name: config.name,
|
|
499
|
+
apiKey: maskKey(rawKey),
|
|
500
|
+
hasKey: !!rawKey,
|
|
501
|
+
defaultModel: parsed.default_model || parsed.defaultModel || '',
|
|
502
|
+
path: configPath
|
|
503
|
+
};
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
} catch (_) {}
|
|
507
|
+
}
|
|
508
|
+
if (!configs[providerId]) {
|
|
509
|
+
configs[providerId] = { name: config.name, apiKey: '', hasKey: false, defaultModel: '', path: '' };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return configs;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function saveProviderConfig(providerId, apiKey, defaultModel) {
|
|
516
|
+
const config = PROVIDER_CONFIGS[providerId];
|
|
517
|
+
if (!config) throw new Error('Unknown provider: ' + providerId);
|
|
518
|
+
const configPath = config.configPaths[0];
|
|
519
|
+
const configDir = path.dirname(configPath);
|
|
520
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
521
|
+
let existing = {};
|
|
522
|
+
try {
|
|
523
|
+
if (fs.existsSync(configPath)) existing = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
524
|
+
} catch (_) {}
|
|
525
|
+
const merged = { ...existing, ...config.configFormat(apiKey, defaultModel) };
|
|
526
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
527
|
+
return configPath;
|
|
528
|
+
}
|
|
529
|
+
|
|
183
530
|
function parseBody(req) {
|
|
184
531
|
return new Promise((resolve, reject) => {
|
|
185
532
|
let body = '';
|
|
@@ -678,15 +1025,29 @@ const server = http.createServer(async (req, res) => {
|
|
|
678
1025
|
status.detail = 'no credentials';
|
|
679
1026
|
}
|
|
680
1027
|
} else if (agent.id === 'gemini') {
|
|
1028
|
+
const oauthFile = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
681
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
|
+
}
|
|
682
1037
|
if (fs.existsSync(acctFile)) {
|
|
683
1038
|
const accts = JSON.parse(fs.readFileSync(acctFile, 'utf-8'));
|
|
684
1039
|
if (accts.active) {
|
|
685
1040
|
status.authenticated = true;
|
|
686
1041
|
status.detail = accts.active;
|
|
1042
|
+
} else if (hasOAuth) {
|
|
1043
|
+
status.authenticated = true;
|
|
1044
|
+
status.detail = 'oauth';
|
|
687
1045
|
} else {
|
|
688
1046
|
status.detail = 'logged out';
|
|
689
1047
|
}
|
|
1048
|
+
} else if (hasOAuth) {
|
|
1049
|
+
status.authenticated = true;
|
|
1050
|
+
status.detail = 'oauth';
|
|
690
1051
|
} else {
|
|
691
1052
|
status.detail = 'no credentials';
|
|
692
1053
|
}
|
|
@@ -711,16 +1072,60 @@ const server = http.createServer(async (req, res) => {
|
|
|
711
1072
|
return;
|
|
712
1073
|
}
|
|
713
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
|
+
|
|
714
1090
|
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
715
1091
|
if (agentAuthMatch && req.method === 'POST') {
|
|
716
1092
|
const agentId = agentAuthMatch[1];
|
|
717
1093
|
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
718
1094
|
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
719
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
|
+
|
|
720
1126
|
const authCommands = {
|
|
721
1127
|
'claude-code': { cmd: 'claude', args: ['setup-token'] },
|
|
722
1128
|
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
|
|
723
|
-
'gemini': { cmd: 'gemini', args: [] }
|
|
724
1129
|
};
|
|
725
1130
|
const authCmd = authCommands[agentId];
|
|
726
1131
|
if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
|
|
@@ -755,6 +1160,33 @@ const server = http.createServer(async (req, res) => {
|
|
|
755
1160
|
return;
|
|
756
1161
|
}
|
|
757
1162
|
|
|
1163
|
+
if (pathOnly === '/api/auth/configs' && req.method === 'GET') {
|
|
1164
|
+
const configs = getProviderConfigs();
|
|
1165
|
+
sendJSON(req, res, 200, configs);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (pathOnly === '/api/auth/save-config' && req.method === 'POST') {
|
|
1170
|
+
try {
|
|
1171
|
+
const body = await parseBody(req);
|
|
1172
|
+
const { providerId, apiKey, defaultModel } = body || {};
|
|
1173
|
+
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) {
|
|
1174
|
+
sendJSON(req, res, 400, { error: 'Invalid providerId' }); return;
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) {
|
|
1177
|
+
sendJSON(req, res, 400, { error: 'Invalid apiKey' }); return;
|
|
1178
|
+
}
|
|
1179
|
+
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) {
|
|
1180
|
+
sendJSON(req, res, 400, { error: 'Invalid defaultModel' }); return;
|
|
1181
|
+
}
|
|
1182
|
+
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
|
|
1183
|
+
sendJSON(req, res, 200, { success: true, path: configPath });
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
sendJSON(req, res, 400, { error: err.message });
|
|
1186
|
+
}
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
758
1190
|
if (pathOnly === '/api/import/claude-code' && req.method === 'GET') {
|
|
759
1191
|
const result = queries.importClaudeCodeConversations();
|
|
760
1192
|
sendJSON(req, res, 200, { imported: result });
|
package/static/index.html
CHANGED
|
@@ -452,7 +452,7 @@
|
|
|
452
452
|
transition: background-color 0.15s, color 0.15s;
|
|
453
453
|
}
|
|
454
454
|
.header-icon-btn:hover { background-color: var(--color-bg-primary); color: var(--color-text-primary); }
|
|
455
|
-
.header-icon-btn svg { width:
|
|
455
|
+
.header-icon-btn svg { width: 20px; height: 20px; }
|
|
456
456
|
#scriptStartBtn { color: var(--color-success); }
|
|
457
457
|
#scriptStartBtn:hover { background-color: rgba(16,185,129,0.1); color: var(--color-success); }
|
|
458
458
|
.script-dev-btn { color: var(--color-info); }
|
|
@@ -466,7 +466,7 @@
|
|
|
466
466
|
.agent-auth-btn:hover { background-color: var(--color-bg-primary); }
|
|
467
467
|
.agent-auth-dropdown {
|
|
468
468
|
position: absolute; top: 100%; right: 0; z-index: 100;
|
|
469
|
-
min-width:
|
|
469
|
+
min-width: 260px; padding: 0.25rem 0;
|
|
470
470
|
background: var(--color-bg-secondary); border: 1px solid var(--color-border);
|
|
471
471
|
border-radius: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
472
472
|
display: none;
|
|
@@ -485,6 +485,11 @@
|
|
|
485
485
|
.agent-auth-dot.ok { background: var(--color-success); }
|
|
486
486
|
.agent-auth-dot.missing { background: var(--color-warning); }
|
|
487
487
|
.agent-auth-dot.unknown { background: var(--color-text-secondary); }
|
|
488
|
+
.agent-auth-section-header {
|
|
489
|
+
padding: 0.375rem 0.75rem; font-size: 0.6875rem; font-weight: 600;
|
|
490
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
491
|
+
color: var(--color-text-secondary); user-select: none;
|
|
492
|
+
}
|
|
488
493
|
|
|
489
494
|
.terminal-container {
|
|
490
495
|
flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #1e1e1e;
|
package/static/js/agent-auth.js
CHANGED
|
@@ -2,103 +2,157 @@
|
|
|
2
2
|
var BASE = window.__BASE_URL || '';
|
|
3
3
|
var btn = document.getElementById('agentAuthBtn');
|
|
4
4
|
var dropdown = document.getElementById('agentAuthDropdown');
|
|
5
|
-
var agents = [];
|
|
6
|
-
var authRunning = false;
|
|
5
|
+
var agents = [], providers = {}, authRunning = false, editingProvider = null;
|
|
7
6
|
var AUTH_CONV_ID = '__agent_auth__';
|
|
8
7
|
|
|
9
8
|
function init() {
|
|
10
9
|
if (!btn || !dropdown) return;
|
|
10
|
+
btn.style.display = 'flex';
|
|
11
11
|
btn.addEventListener('click', toggleDropdown);
|
|
12
12
|
document.addEventListener('click', function(e) {
|
|
13
|
-
if (!btn.contains(e.target)) closeDropdown();
|
|
13
|
+
if (!btn.contains(e.target) && !dropdown.contains(e.target)) closeDropdown();
|
|
14
14
|
});
|
|
15
|
-
window.addEventListener('conversation-selected', function() {
|
|
15
|
+
window.addEventListener('conversation-selected', function() { refresh(); });
|
|
16
16
|
window.addEventListener('ws-message', onWsMessage);
|
|
17
|
-
|
|
17
|
+
refresh();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function refresh() { fetchAuthStatus(); fetchProviderConfigs(); }
|
|
21
|
+
|
|
20
22
|
function fetchAuthStatus() {
|
|
21
|
-
fetch(BASE + '/api/agents/auth-status')
|
|
22
|
-
.then(function(
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
fetch(BASE + '/api/agents/auth-status').then(function(r) { return r.json(); })
|
|
24
|
+
.then(function(data) { agents = data.agents || []; updateButton(); renderDropdown(); })
|
|
25
|
+
.catch(function() {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fetchProviderConfigs() {
|
|
29
|
+
fetch(BASE + '/api/auth/configs').then(function(r) { return r.json(); })
|
|
30
|
+
.then(function(data) { providers = data || {}; updateButton(); renderDropdown(); })
|
|
28
31
|
.catch(function() {});
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function updateButton() {
|
|
32
|
-
if (agents.length === 0) { btn.style.display = 'none'; return; }
|
|
33
35
|
btn.style.display = 'flex';
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
var agentOk = agents.length === 0 || agents.every(function(a) { return a.authenticated; });
|
|
37
|
+
var pkeys = Object.keys(providers);
|
|
38
|
+
var provOk = pkeys.length === 0 || pkeys.some(function(k) { return providers[k].hasKey; });
|
|
39
|
+
var anyWarn = agents.some(function(a) { return !a.authenticated; }) ||
|
|
40
|
+
pkeys.some(function(k) { return !providers[k].hasKey; });
|
|
41
|
+
btn.classList.toggle('auth-ok', agentOk && provOk && (agents.length > 0 || pkeys.length > 0));
|
|
42
|
+
btn.classList.toggle('auth-warn', anyWarn);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
function renderDropdown() {
|
|
41
46
|
dropdown.innerHTML = '';
|
|
42
|
-
agents.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
if (agents.length > 0) {
|
|
48
|
+
appendHeader('Agent CLI Auth');
|
|
49
|
+
agents.forEach(function(agent) {
|
|
50
|
+
var dotClass = agent.authenticated ? 'ok' : (agent.detail === 'unknown' ? 'unknown' : 'missing');
|
|
51
|
+
var item = makeItem(dotClass, agent.name, agent.detail);
|
|
52
|
+
item.addEventListener('click', function(e) { e.stopPropagation(); closeDropdown(); triggerAuth(agent.id); });
|
|
53
|
+
dropdown.appendChild(item);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
var pkeys = Object.keys(providers);
|
|
57
|
+
if (pkeys.length > 0) {
|
|
58
|
+
if (agents.length > 0) appendSep();
|
|
59
|
+
appendHeader('Provider Keys');
|
|
60
|
+
pkeys.forEach(function(pid) {
|
|
61
|
+
var p = providers[pid];
|
|
62
|
+
var item = makeItem(p.hasKey ? 'ok' : 'missing', p.name || pid, p.hasKey ? p.apiKey : 'not set');
|
|
63
|
+
item.style.flexWrap = 'wrap';
|
|
64
|
+
item.addEventListener('click', function(e) { e.stopPropagation(); toggleEdit(pid); });
|
|
65
|
+
dropdown.appendChild(item);
|
|
66
|
+
if (editingProvider === pid) dropdown.appendChild(makeEditForm(pid));
|
|
53
67
|
});
|
|
54
|
-
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function appendHeader(text) {
|
|
72
|
+
var h = document.createElement('div');
|
|
73
|
+
h.className = 'agent-auth-section-header';
|
|
74
|
+
h.textContent = text;
|
|
75
|
+
dropdown.appendChild(h);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function appendSep() {
|
|
79
|
+
var s = document.createElement('div');
|
|
80
|
+
s.style.cssText = 'height:1px;background:var(--color-border);margin:0.25rem 0;';
|
|
81
|
+
dropdown.appendChild(s);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeItem(dotClass, name, detail) {
|
|
85
|
+
var el = document.createElement('button');
|
|
86
|
+
el.className = 'agent-auth-item';
|
|
87
|
+
el.innerHTML = '<span class="agent-auth-dot ' + dotClass + '"></span><span>' + esc(name) +
|
|
88
|
+
'</span><span style="margin-left:auto;font-size:0.7rem;color:var(--color-text-secondary)">' + esc(detail) + '</span>';
|
|
89
|
+
return el;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeEditForm(pid) {
|
|
93
|
+
var form = document.createElement('div');
|
|
94
|
+
form.style.cssText = 'width:100%;padding:0.375rem 0.75rem;display:flex;gap:0.375rem;';
|
|
95
|
+
var input = document.createElement('input');
|
|
96
|
+
input.type = 'password'; input.placeholder = 'API key';
|
|
97
|
+
input.style.cssText = 'flex:1;min-width:0;padding:0.25rem 0.5rem;font-size:0.75rem;border:1px solid var(--color-border);border-radius:0.25rem;background:var(--color-bg-primary);color:var(--color-text-primary);outline:none;';
|
|
98
|
+
input.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
99
|
+
var saveBtn = document.createElement('button');
|
|
100
|
+
saveBtn.textContent = 'Save';
|
|
101
|
+
saveBtn.style.cssText = 'padding:0.25rem 0.5rem;font-size:0.7rem;font-weight:600;background:var(--color-primary);color:white;border:none;border-radius:0.25rem;cursor:pointer;flex-shrink:0;';
|
|
102
|
+
saveBtn.addEventListener('click', function(e) {
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
var key = input.value.trim();
|
|
105
|
+
if (!key) return;
|
|
106
|
+
saveBtn.disabled = true; saveBtn.textContent = '...';
|
|
107
|
+
saveProviderKey(pid, key);
|
|
55
108
|
});
|
|
109
|
+
form.appendChild(input); form.appendChild(saveBtn);
|
|
110
|
+
setTimeout(function() { input.focus(); }, 50);
|
|
111
|
+
return form;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toggleEdit(pid) { editingProvider = editingProvider === pid ? null : pid; renderDropdown(); }
|
|
115
|
+
|
|
116
|
+
function saveProviderKey(providerId, apiKey) {
|
|
117
|
+
fetch(BASE + '/api/auth/save-config', {
|
|
118
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ providerId: providerId, apiKey: apiKey, defaultModel: '' })
|
|
120
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
121
|
+
if (data.success) { editingProvider = null; fetchProviderConfigs(); }
|
|
122
|
+
}).catch(function() { editingProvider = null; renderDropdown(); });
|
|
56
123
|
}
|
|
57
124
|
|
|
58
125
|
function toggleDropdown(e) {
|
|
59
126
|
e.stopPropagation();
|
|
127
|
+
if (!dropdown.classList.contains('open')) { editingProvider = null; refresh(); }
|
|
60
128
|
dropdown.classList.toggle('open');
|
|
61
129
|
}
|
|
62
130
|
|
|
63
|
-
function closeDropdown() {
|
|
64
|
-
dropdown.classList.remove('open');
|
|
65
|
-
}
|
|
131
|
+
function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
|
|
66
132
|
|
|
67
133
|
function triggerAuth(agentId) {
|
|
68
134
|
if (authRunning) return;
|
|
69
135
|
fetch(BASE + '/api/agents/' + agentId + '/auth', {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
|
|
72
|
-
body: '{}'
|
|
73
|
-
})
|
|
74
|
-
.then(function(r) { return r.json(); })
|
|
75
|
-
.then(function(data) {
|
|
136
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
137
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
76
138
|
if (data.ok) {
|
|
77
|
-
authRunning = true;
|
|
78
|
-
showTerminalTab();
|
|
79
|
-
switchToTerminalView();
|
|
139
|
+
authRunning = true; showTerminalTab(); switchToTerminalView();
|
|
80
140
|
var term = getTerminal();
|
|
81
|
-
if (term) {
|
|
82
|
-
|
|
83
|
-
|
|
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');
|
|
84
144
|
}
|
|
85
145
|
}
|
|
86
|
-
})
|
|
87
|
-
.catch(function() {});
|
|
146
|
+
}).catch(function() {});
|
|
88
147
|
}
|
|
89
148
|
|
|
90
149
|
function onWsMessage(e) {
|
|
91
150
|
var data = e.detail;
|
|
92
151
|
if (!data || data.conversationId !== AUTH_CONV_ID) return;
|
|
93
152
|
if (data.type === 'script_started') {
|
|
94
|
-
authRunning = true;
|
|
95
|
-
showTerminalTab();
|
|
96
|
-
switchToTerminalView();
|
|
153
|
+
authRunning = true; showTerminalTab(); switchToTerminalView();
|
|
97
154
|
var term = getTerminal();
|
|
98
|
-
if (term) {
|
|
99
|
-
term.clear();
|
|
100
|
-
term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n');
|
|
101
|
-
}
|
|
155
|
+
if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n'); }
|
|
102
156
|
} else if (data.type === 'script_output') {
|
|
103
157
|
showTerminalTab();
|
|
104
158
|
var term = getTerminal();
|
|
@@ -108,37 +162,20 @@
|
|
|
108
162
|
var term = getTerminal();
|
|
109
163
|
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|
|
110
164
|
if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
|
|
111
|
-
setTimeout(
|
|
165
|
+
setTimeout(refresh, 1000);
|
|
112
166
|
}
|
|
113
167
|
}
|
|
114
168
|
|
|
115
|
-
function showTerminalTab() {
|
|
116
|
-
var tabBtn = document.getElementById('terminalTabBtn');
|
|
117
|
-
if (tabBtn) tabBtn.style.display = '';
|
|
118
|
-
}
|
|
119
|
-
|
|
169
|
+
function showTerminalTab() { var t = document.getElementById('terminalTabBtn'); if (t) t.style.display = ''; }
|
|
120
170
|
function switchToTerminalView() {
|
|
121
171
|
var bar = document.getElementById('viewToggleBar');
|
|
122
172
|
if (!bar) return;
|
|
123
|
-
var
|
|
124
|
-
if (termBtn) termBtn.click();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function getTerminal() {
|
|
128
|
-
return window.scriptRunner ? window.scriptRunner.getTerminal() : null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function escapeHtml(s) {
|
|
132
|
-
var d = document.createElement('div');
|
|
133
|
-
d.textContent = s;
|
|
134
|
-
return d.innerHTML;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (document.readyState === 'loading') {
|
|
138
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
139
|
-
} else {
|
|
140
|
-
init();
|
|
173
|
+
var t = bar.querySelector('[data-view="terminal"]'); if (t) t.click();
|
|
141
174
|
}
|
|
175
|
+
function getTerminal() { return window.scriptRunner ? window.scriptRunner.getTerminal() : null; }
|
|
176
|
+
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
142
177
|
|
|
143
|
-
|
|
178
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
179
|
+
else init();
|
|
180
|
+
window.agentAuth = { refresh: refresh };
|
|
144
181
|
})();
|