agentgui 1.0.212 → 1.0.214

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.
@@ -208,7 +208,7 @@ class AgentRunner {
208
208
  } = config;
209
209
 
210
210
  const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
211
- const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : ['acp'];
211
+ const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : this.buildArgs(prompt, config);
212
212
  const args = [...baseArgs];
213
213
 
214
214
  const proc = spawn(cmd, args, { cwd });
@@ -351,6 +351,15 @@ class AgentRunner {
351
351
  return;
352
352
  }
353
353
 
354
+ if (message.id === promptId && message.error) {
355
+ originalHandler(message);
356
+ completed = true;
357
+ clearTimeout(timeoutHandle);
358
+ proc.kill();
359
+ reject(new Error(message.error.message || 'ACP prompt error'));
360
+ return;
361
+ }
362
+
354
363
  originalHandler(message);
355
364
  };
356
365
 
@@ -726,7 +735,7 @@ registry.register({
726
735
  protocol: 'acp',
727
736
  supportsStdin: false,
728
737
  supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
729
- buildArgs: () => ['acp'],
738
+ buildArgs: () => ['--experimental-acp', '--yolo'],
730
739
  protocolHandler: acpProtocolHandler
731
740
  });
732
741
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.212",
3
+ "version": "1.0.214",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -336,6 +336,67 @@ function geminiOAuthResultPage(title, message, success) {
336
336
  </div></body></html>`;
337
337
  }
338
338
 
339
+ function encodeOAuthState(csrfToken, relayUrl) {
340
+ const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
341
+ return Buffer.from(payload).toString('base64url');
342
+ }
343
+
344
+ function decodeOAuthState(stateStr) {
345
+ try {
346
+ const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
347
+ return { csrfToken: payload.t, relayUrl: payload.r };
348
+ } catch (_) {
349
+ return { csrfToken: stateStr, relayUrl: null };
350
+ }
351
+ }
352
+
353
+ function geminiOAuthRelayPage(code, state, error) {
354
+ const stateData = decodeOAuthState(state || '');
355
+ const relayUrl = stateData.relayUrl || '';
356
+ const escapedCode = (code || '').replace(/['"\\]/g, '');
357
+ const escapedState = (state || '').replace(/['"\\]/g, '');
358
+ const escapedError = (error || '').replace(/['"\\]/g, '');
359
+ const escapedRelay = relayUrl.replace(/['"\\]/g, '');
360
+ return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
361
+ <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;">
362
+ <div id="status" style="text-align:center;max-width:400px;padding:2rem;">
363
+ <div id="spinner" style="font-size:2rem;margin-bottom:1rem;">&#8987;</div>
364
+ <h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
365
+ <p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
366
+ </div>
367
+ <script>
368
+ (function() {
369
+ var code = '${escapedCode}';
370
+ var state = '${escapedState}';
371
+ var error = '${escapedError}';
372
+ var relayUrl = '${escapedRelay}';
373
+ function show(icon, title, msg, color) {
374
+ document.getElementById('spinner').textContent = icon;
375
+ document.getElementById('spinner').style.color = color;
376
+ document.getElementById('title').textContent = title;
377
+ document.getElementById('msg').textContent = msg;
378
+ }
379
+ if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
380
+ if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
381
+ if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
382
+ fetch(relayUrl, {
383
+ method: 'POST',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ code: code, state: state })
386
+ }).then(function(r) { return r.json(); }).then(function(data) {
387
+ if (data.success) {
388
+ show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
389
+ } else {
390
+ show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
391
+ }
392
+ }).catch(function(e) {
393
+ show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
394
+ });
395
+ })();
396
+ </script>
397
+ </body></html>`;
398
+ }
399
+
339
400
  function isRemoteRequest(req) {
340
401
  return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
341
402
  }
@@ -353,7 +414,10 @@ async function startGeminiOAuth(req) {
353
414
  redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
354
415
  }
355
416
 
356
- const state = crypto.randomBytes(32).toString('hex');
417
+ const csrfToken = crypto.randomBytes(32).toString('hex');
418
+ const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
419
+ const state = encodeOAuthState(csrfToken, relayUrl);
420
+
357
421
  const client = new OAuth2Client({
358
422
  clientId: creds.clientId,
359
423
  clientSecret: creds.clientSecret,
@@ -367,7 +431,7 @@ async function startGeminiOAuth(req) {
367
431
  });
368
432
 
369
433
  const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
370
- geminiOAuthPending = { client, redirectUri, state };
434
+ geminiOAuthPending = { client, redirectUri, state: csrfToken };
371
435
  geminiOAuthState = { status: 'pending', error: null, email: null };
372
436
 
373
437
  setTimeout(() => {
@@ -380,12 +444,13 @@ async function startGeminiOAuth(req) {
380
444
  return { authUrl, mode };
381
445
  }
382
446
 
383
- async function exchangeGeminiOAuthCode(code, state) {
447
+ async function exchangeGeminiOAuthCode(code, stateParam) {
384
448
  if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
385
449
 
386
- const { client, redirectUri, state: expectedState } = geminiOAuthPending;
450
+ const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
451
+ const { csrfToken } = decodeOAuthState(stateParam);
387
452
 
388
- if (state !== expectedState) {
453
+ if (csrfToken !== expectedCsrf) {
389
454
  geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
390
455
  geminiOAuthPending = null;
391
456
  throw new Error('State mismatch - possible CSRF attack.');
@@ -423,6 +488,23 @@ async function exchangeGeminiOAuthCode(code, state) {
423
488
 
424
489
  async function handleGeminiOAuthCallback(req, res) {
425
490
  const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
491
+ const code = reqUrl.searchParams.get('code');
492
+ const state = reqUrl.searchParams.get('state');
493
+ const error = reqUrl.searchParams.get('error');
494
+ const errorDesc = reqUrl.searchParams.get('error_description');
495
+
496
+ if (error) {
497
+ const desc = errorDesc || error;
498
+ geminiOAuthState = { status: 'error', error: desc, email: null };
499
+ geminiOAuthPending = null;
500
+ }
501
+
502
+ const stateData = decodeOAuthState(state || '');
503
+ if (stateData.relayUrl) {
504
+ res.writeHead(200, { 'Content-Type': 'text/html' });
505
+ res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
506
+ return;
507
+ }
426
508
 
427
509
  if (!geminiOAuthPending) {
428
510
  res.writeHead(200, { 'Content-Type': 'text/html' });
@@ -431,20 +513,8 @@ async function handleGeminiOAuthCallback(req, res) {
431
513
  }
432
514
 
433
515
  try {
434
- const error = reqUrl.searchParams.get('error');
435
- if (error) {
436
- const desc = reqUrl.searchParams.get('error_description') || error;
437
- geminiOAuthState = { status: 'error', error: desc, email: null };
438
- geminiOAuthPending = null;
439
- res.writeHead(200, { 'Content-Type': 'text/html' });
440
- res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
441
- return;
442
- }
443
-
444
- const code = reqUrl.searchParams.get('code');
445
- const state = reqUrl.searchParams.get('state');
516
+ if (error) throw new Error(errorDesc || error);
446
517
  const email = await exchangeGeminiOAuthCode(code, state);
447
-
448
518
  res.writeHead(200, { 'Content-Type': 'text/html' });
449
519
  res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
450
520
  } catch (e) {
@@ -1165,6 +1235,24 @@ const server = http.createServer(async (req, res) => {
1165
1235
  return;
1166
1236
  }
1167
1237
 
1238
+ if (pathOnly === '/api/gemini-oauth/relay' && req.method === 'POST') {
1239
+ try {
1240
+ const body = await parseBody(req);
1241
+ const { code, state: stateParam } = body;
1242
+ if (!code || !stateParam) {
1243
+ sendJSON(req, res, 400, { error: 'Missing code or state' });
1244
+ return;
1245
+ }
1246
+ const email = await exchangeGeminiOAuthCode(code, stateParam);
1247
+ sendJSON(req, res, 200, { success: true, email });
1248
+ } catch (e) {
1249
+ geminiOAuthState = { status: 'error', error: e.message, email: null };
1250
+ geminiOAuthPending = null;
1251
+ sendJSON(req, res, 400, { error: e.message });
1252
+ }
1253
+ return;
1254
+ }
1255
+
1168
1256
  if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
1169
1257
  try {
1170
1258
  const body = await parseBody(req);
@@ -130,46 +130,61 @@
130
130
 
131
131
  function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
132
132
 
133
- var oauthPollInterval = null, oauthPollTimeout = null;
133
+ var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
134
134
 
135
135
  function cleanupOAuthPolling() {
136
136
  if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
137
137
  if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
138
+ if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
138
139
  }
139
140
 
140
- function showOAuthPasteModal() {
141
- removeOAuthPasteModal();
141
+ function showOAuthWaitingModal() {
142
+ removeOAuthModal();
142
143
  var overlay = document.createElement('div');
143
- overlay.id = 'oauthPasteModal';
144
+ overlay.id = 'oauthWaitingModal';
144
145
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
145
- var s = function(c) { return 'font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 ' + (c ? '0' : '0.5rem') + ';'; };
146
146
  overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
147
147
  '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
148
- '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Complete Google Sign-In</h2>' +
149
- '<button id="oauthPasteClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
148
+ '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Google Sign-In</h2>' +
149
+ '<button id="oauthWaitingClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
150
+ '<div id="oauthWaitingContent" style="text-align:center;padding:1.5rem 0;">' +
151
+ '<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">&#9203;</div>' +
152
+ '<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">Waiting for Google sign-in to complete...</p>' +
153
+ '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">Complete the sign-in in the tab that just opened.</p>' +
154
+ '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0.25rem 0 0;">This dialog will close automatically when done.</p></div>' +
155
+ '<div id="oauthPasteFallback" style="display:none;">' +
150
156
  '<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
151
- '<p style="' + s() + '">1. A Google sign-in page has opened in a new tab.</p>' +
152
- '<p style="' + s() + '">2. Complete the sign-in process with Google.</p>' +
153
- '<p style="' + s() + '">3. After signing in, you will be redirected to a page that <span style="color:#facc15;font-weight:600;">may not load</span> (this is expected).</p>' +
154
- '<p style="' + s(1) + '">4. Copy the <span style="color:white;font-weight:600;">entire URL</span> from the address bar and paste it below.</p></div>' +
155
- '<label style="display:block;font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin-bottom:0.5rem;">Paste the redirect URL here:</label>' +
157
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">The automatic relay did not complete. This can happen when accessing the server remotely.</p>' +
158
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0;">Copy the <span style="color:white;font-weight:600;">entire URL</span> from the sign-in tab and paste it below.</p></div>' +
156
159
  '<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
157
- '<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p>' +
160
+ '<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p></div>' +
158
161
  '<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
159
- '<button id="oauthPasteCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
160
- '<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;">Complete Sign-In</button></div>' +
161
- '<p style="font-size:0.7rem;color:var(--color-text-secondary,#6b7280);margin-top:1rem;text-align:center;">If the redirect page loaded successfully, this dialog will close automatically.</p></div>';
162
+ '<button id="oauthWaitingCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
163
+ '<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;display:none;">Complete Sign-In</button></div>' +
164
+ '<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
162
165
  document.body.appendChild(overlay);
163
- var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthPasteModal(); };
164
- document.getElementById('oauthPasteClose').addEventListener('click', dismiss);
165
- document.getElementById('oauthPasteCancel').addEventListener('click', dismiss);
166
+ var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthModal(); };
167
+ document.getElementById('oauthWaitingClose').addEventListener('click', dismiss);
168
+ document.getElementById('oauthWaitingCancel').addEventListener('click', dismiss);
166
169
  document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
167
- document.getElementById('oauthPasteInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
168
- setTimeout(function() { var i = document.getElementById('oauthPasteInput'); if (i) i.focus(); }, 100);
169
170
  }
170
171
 
171
- function removeOAuthPasteModal() {
172
- var el = document.getElementById('oauthPasteModal');
172
+ function showOAuthPasteFallback() {
173
+ var fallback = document.getElementById('oauthPasteFallback');
174
+ var waitContent = document.getElementById('oauthWaitingContent');
175
+ var submitBtn = document.getElementById('oauthPasteSubmit');
176
+ if (fallback) fallback.style.display = 'block';
177
+ if (waitContent) waitContent.style.display = 'none';
178
+ if (submitBtn) submitBtn.style.display = 'block';
179
+ var input = document.getElementById('oauthPasteInput');
180
+ if (input) {
181
+ input.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
182
+ setTimeout(function() { input.focus(); }, 100);
183
+ }
184
+ }
185
+
186
+ function removeOAuthModal() {
187
+ var el = document.getElementById('oauthWaitingModal');
173
188
  if (el) el.remove();
174
189
  }
175
190
 
@@ -193,7 +208,7 @@
193
208
  if (data.success) {
194
209
  cleanupOAuthPolling();
195
210
  authRunning = false;
196
- removeOAuthPasteModal();
211
+ removeOAuthModal();
197
212
  refresh();
198
213
  } else {
199
214
  if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
@@ -217,26 +232,28 @@
217
232
  if (data.authUrl) {
218
233
  window.open(data.authUrl, '_blank');
219
234
  if (agentId === 'gemini') {
220
- var needsPaste = data.mode === 'cli-remote';
221
- if (needsPaste) showOAuthPasteModal();
235
+ showOAuthWaitingModal();
222
236
  cleanupOAuthPolling();
223
237
  oauthPollInterval = setInterval(function() {
224
238
  fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
225
239
  if (status.status === 'success') {
226
240
  cleanupOAuthPolling();
227
241
  authRunning = false;
228
- removeOAuthPasteModal();
242
+ removeOAuthModal();
229
243
  refresh();
230
244
  } else if (status.status === 'error') {
231
245
  cleanupOAuthPolling();
232
246
  authRunning = false;
233
- removeOAuthPasteModal();
247
+ removeOAuthModal();
234
248
  }
235
249
  }).catch(function() {});
236
250
  }, 1500);
251
+ oauthFallbackTimer = setTimeout(function() {
252
+ if (authRunning) showOAuthPasteFallback();
253
+ }, 30000);
237
254
  oauthPollTimeout = setTimeout(function() {
238
255
  cleanupOAuthPolling();
239
- if (authRunning) { authRunning = false; removeOAuthPasteModal(); }
256
+ if (authRunning) { authRunning = false; removeOAuthModal(); }
240
257
  }, 5 * 60 * 1000);
241
258
  }
242
259
  }
@@ -257,7 +274,7 @@
257
274
  if (term) term.write(data.data);
258
275
  } else if (data.type === 'script_stopped') {
259
276
  authRunning = false;
260
- removeOAuthPasteModal();
277
+ removeOAuthModal();
261
278
  cleanupOAuthPolling();
262
279
  var term = getTerminal();
263
280
  var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));