agent-tank 0.9.1 → 0.9.2
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/src/agents/base.js +34 -0
- package/src/agents/gemini.js +116 -8
- package/src/status-page.css +21 -0
- package/src/status-page.js +17 -3
package/package.json
CHANGED
package/src/agents/base.js
CHANGED
|
@@ -11,6 +11,7 @@ class BaseAgent {
|
|
|
11
11
|
this._metadataFetched = false;
|
|
12
12
|
this.lastUpdated = null;
|
|
13
13
|
this.error = null;
|
|
14
|
+
this.auth = null;
|
|
14
15
|
this.isRefreshing = false;
|
|
15
16
|
this.refreshInterval = null; // Per-agent override (seconds), null = use global
|
|
16
17
|
this.minRefreshInterval = null; // Per-agent minimum (seconds), null = no minimum
|
|
@@ -32,6 +33,7 @@ class BaseAgent {
|
|
|
32
33
|
metadata: this.metadata,
|
|
33
34
|
lastUpdated: this.lastUpdated,
|
|
34
35
|
error: this.error,
|
|
36
|
+
auth: this.auth,
|
|
35
37
|
isRefreshing: this.isRefreshing,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
@@ -45,6 +47,7 @@ class BaseAgent {
|
|
|
45
47
|
logger.agent(this.name, 'Starting refresh...');
|
|
46
48
|
this.isRefreshing = true;
|
|
47
49
|
this.error = null;
|
|
50
|
+
this.auth = null;
|
|
48
51
|
|
|
49
52
|
try {
|
|
50
53
|
// Fetch metadata once on first refresh (if agent supports it)
|
|
@@ -71,6 +74,15 @@ class BaseAgent {
|
|
|
71
74
|
// Check for rate limit and session errors before parsing
|
|
72
75
|
// Match actual error messages, not incidental mentions like "rate limits and credits"
|
|
73
76
|
const cleanOutput = this.stripAnsi(output);
|
|
77
|
+
const authState = this.detectAuthenticationState ? this.detectAuthenticationState(cleanOutput) : null;
|
|
78
|
+
if (authState) {
|
|
79
|
+
logger.agent(this.name, 'Authentication issue detected:', logger.json(authState));
|
|
80
|
+
this.auth = authState;
|
|
81
|
+
this.error = this.usage ? `${authState.message} — using cached data` : authState.message;
|
|
82
|
+
this.lastUpdated = new Date().toISOString();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
if (/rate.?limited|rate_limit_error/i.test(cleanOutput)) {
|
|
75
87
|
logger.agent(this.name, 'Rate limited, preserving last known usage data');
|
|
76
88
|
this.error = 'Rate limited — using cached data';
|
|
@@ -159,6 +171,20 @@ class BaseAgent {
|
|
|
159
171
|
this._handleAdditionalPrompts(this.shell, data, spawnOutput);
|
|
160
172
|
this._respondToTerminalQueries(data);
|
|
161
173
|
|
|
174
|
+
const authState = this.detectAuthenticationState ? this.detectAuthenticationState(spawnOutput) : null;
|
|
175
|
+
if (authState) {
|
|
176
|
+
logger.agent(this.name, 'Authentication issue detected during spawn');
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
spawnDataHandler.dispose();
|
|
179
|
+
spawnExitHandler.dispose();
|
|
180
|
+
this.processReady = true;
|
|
181
|
+
this.output = spawnOutput;
|
|
182
|
+
this._setupPersistentDataHandler();
|
|
183
|
+
this._setupPersistentExitHandler();
|
|
184
|
+
resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
if (Date.now() >= trustCooldownUntil && this.isReadyForCommands(spawnOutput)) {
|
|
163
189
|
logger.agent(this.name, 'Process ready for commands');
|
|
164
190
|
clearTimeout(timer);
|
|
@@ -183,6 +209,14 @@ class BaseAgent {
|
|
|
183
209
|
|
|
184
210
|
async sendCommandAndWait() {
|
|
185
211
|
return new Promise((resolve, reject) => {
|
|
212
|
+
const existingOutput = this.output;
|
|
213
|
+
const existingAuthState = this.detectAuthenticationState ? this.detectAuthenticationState(existingOutput) : null;
|
|
214
|
+
if (existingAuthState) {
|
|
215
|
+
logger.agent(this.name, 'Using existing authentication output without sending commands');
|
|
216
|
+
resolve(existingOutput);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
186
220
|
this.output = '';
|
|
187
221
|
this._commandInFlight = true;
|
|
188
222
|
|
package/src/agents/gemini.js
CHANGED
|
@@ -10,10 +10,24 @@ class GeminiAgent extends BaseAgent {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
getTimeout() {
|
|
13
|
-
return
|
|
13
|
+
return 35000; // 35 seconds - Gemini can take 10+ seconds to start
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getEnv() {
|
|
17
|
+
// Use xterm-256color for proper terminal support
|
|
18
|
+
// Gemini CLI needs good terminal emulation for its TUI
|
|
19
|
+
return {
|
|
20
|
+
...process.env,
|
|
21
|
+
TERM: 'xterm-256color',
|
|
22
|
+
COLORTERM: 'truecolor',
|
|
23
|
+
};
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
isReadyForCommands(output) {
|
|
27
|
+
// Don't consider ready if authentication is in progress
|
|
28
|
+
if (this._isAuthenticating(output)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
17
31
|
// v0.24: "Type your message" prompt
|
|
18
32
|
// v0.35+: "Ready" in terminal title + visible prompt area (no "Type your message")
|
|
19
33
|
// Also check for terminal title sequence containing "Ready"
|
|
@@ -22,6 +36,52 @@ class GeminiAgent extends BaseAgent {
|
|
|
22
36
|
|| /Ready\s*\(/.test(output);
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
_isAuthenticating(output) {
|
|
40
|
+
// Detect OAuth/authentication screens where Escape would cancel the flow
|
|
41
|
+
const clean = this.stripAnsi(output);
|
|
42
|
+
|
|
43
|
+
// If we see the ready prompt, auth is complete (even if auth text still in buffer)
|
|
44
|
+
if (clean.includes('Type your message') || clean.includes('? for shortcuts')) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Only consider authenticating if we see auth-specific text
|
|
49
|
+
return /waiting for authentication/i.test(clean)
|
|
50
|
+
|| /press esc.*to cancel/i.test(clean)
|
|
51
|
+
|| /authenticating\.\.\./i.test(clean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
detectAuthenticationState(output) {
|
|
55
|
+
const clean = this.stripAnsi(output);
|
|
56
|
+
if (!clean) return null;
|
|
57
|
+
|
|
58
|
+
const hasAuthMenu = /how would you like to authenticate for this project\?/i.test(clean)
|
|
59
|
+
|| /get started/i.test(clean);
|
|
60
|
+
const hasAuthFailure = /failed to sign in\.\s*message:\s*([^\n]+)/i.test(clean)
|
|
61
|
+
|| /please run gemini cli in an interactive terminal to authenticate/i.test(clean)
|
|
62
|
+
|| /authentication consent could not be obtained/i.test(clean);
|
|
63
|
+
|
|
64
|
+
if (!hasAuthMenu && !hasAuthFailure) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const failureMatch = clean.match(/failed to sign in\.\s*message:\s*([^\n]+)/i);
|
|
69
|
+
const detail = failureMatch
|
|
70
|
+
? `Failed to sign in: ${failureMatch[1].trim()}`
|
|
71
|
+
: 'Gemini CLI is waiting for authentication.';
|
|
72
|
+
const action = /NO_BROWSER=true/i.test(clean)
|
|
73
|
+
? 'Run Gemini CLI in an interactive terminal to authenticate, or use NO_BROWSER=true for manual authentication.'
|
|
74
|
+
: 'Run Gemini CLI in an interactive terminal to authenticate.';
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
authenticated: false,
|
|
78
|
+
status: 'unauthenticated',
|
|
79
|
+
message: 'Authentication required',
|
|
80
|
+
detail,
|
|
81
|
+
action,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
25
85
|
handleTrustPrompt(shell, output) {
|
|
26
86
|
// Check for trust prompt with various wordings
|
|
27
87
|
const trustPatterns = [
|
|
@@ -42,6 +102,7 @@ class GeminiAgent extends BaseAgent {
|
|
|
42
102
|
// v0.35+ does an internal restart after trust — send Escape after a delay
|
|
43
103
|
// to dismiss any transitional UI before the process becomes ready again
|
|
44
104
|
setTimeout(() => {
|
|
105
|
+
if (!shell) return;
|
|
45
106
|
logger.agent(this.name, 'Post-trust: sending Escape to clear restart UI...');
|
|
46
107
|
shell.write('\x1b');
|
|
47
108
|
}, 3000);
|
|
@@ -55,6 +116,7 @@ class GeminiAgent extends BaseAgent {
|
|
|
55
116
|
// Gemini shows "Usage limits" after the model usage table (v0.24.x)
|
|
56
117
|
if (output.includes('Usage limits span all sessions')) return true;
|
|
57
118
|
const clean = this.stripAnsi(output);
|
|
119
|
+
if (this.detectAuthenticationState(clean)) return true;
|
|
58
120
|
const hasModel = /gemini-[\w.-]+/i.test(clean);
|
|
59
121
|
const hasPercent = /\d+(?:\.\d+)?%/i.test(clean);
|
|
60
122
|
// v0.24: "Resets in 3h 26m", v0.35+: "(23h 21m)" after time
|
|
@@ -89,10 +151,16 @@ class GeminiAgent extends BaseAgent {
|
|
|
89
151
|
return totalSeconds > 0 ? totalSeconds : null;
|
|
90
152
|
}
|
|
91
153
|
|
|
92
|
-
sendCommands(shell,
|
|
154
|
+
sendCommands(shell, output) {
|
|
155
|
+
// Check if authentication is still in progress - don't send commands that could interfere
|
|
156
|
+
if (this._isAuthenticating(output) || this.detectAuthenticationState(output)) {
|
|
157
|
+
logger.agent(this.name, 'Authentication required or in progress, cannot send /stats command');
|
|
158
|
+
// Don't retry - let the timeout handle it. Sending Escape would cancel auth.
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
93
161
|
logger.agent(this.name, 'Sending /stats command...');
|
|
94
162
|
// v0.35+: typing /stats triggers autocomplete dropdown.
|
|
95
|
-
// Escape
|
|
163
|
+
// Escape clears any pending state, then send command, Escape again to dismiss autocomplete
|
|
96
164
|
setTimeout(() => shell.write('\x1b'), 50);
|
|
97
165
|
setTimeout(() => shell.write('/stats'), 500);
|
|
98
166
|
setTimeout(() => shell.write('\x1b'), 1000);
|
|
@@ -161,6 +229,10 @@ class GeminiAgent extends BaseAgent {
|
|
|
161
229
|
await this.spawnProcess();
|
|
162
230
|
}
|
|
163
231
|
|
|
232
|
+
if (this.detectAuthenticationState(this.output)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
164
236
|
return new Promise((resolve, _reject) => {
|
|
165
237
|
let aboutOutput = '';
|
|
166
238
|
let completed = false;
|
|
@@ -181,8 +253,16 @@ class GeminiAgent extends BaseAgent {
|
|
|
181
253
|
}, 10000); // 10 second timeout for /about
|
|
182
254
|
|
|
183
255
|
this._commandInFlight = true;
|
|
256
|
+
let trustHandled = false;
|
|
184
257
|
this._onDataCallback = () => {
|
|
185
258
|
aboutOutput = this.output;
|
|
259
|
+
// Handle trust prompts that appear during command execution
|
|
260
|
+
if (!trustHandled && this.handleTrustPrompt(this.shell, aboutOutput)) {
|
|
261
|
+
trustHandled = true;
|
|
262
|
+
aboutOutput = '';
|
|
263
|
+
this.output = '';
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
186
266
|
// Check if we have complete /about output
|
|
187
267
|
if (this._hasCompleteAboutOutput(aboutOutput)) {
|
|
188
268
|
logger.agent(this.name, 'Complete /about output detected');
|
|
@@ -192,11 +272,39 @@ class GeminiAgent extends BaseAgent {
|
|
|
192
272
|
|
|
193
273
|
logger.agent(this.name, 'Sending /about command for metadata...');
|
|
194
274
|
this.output = '';
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
|
|
276
|
+
// Wait for authentication to complete before sending commands
|
|
277
|
+
// Auth can start after spawn if token refresh is needed
|
|
278
|
+
// Gemini can take 10+ seconds to fully start up
|
|
279
|
+
let authWaitCount = 0;
|
|
280
|
+
const maxAuthWait = 15; // Max 15 seconds waiting for auth
|
|
281
|
+
|
|
282
|
+
const waitForAuth = () => {
|
|
283
|
+
if (this.detectAuthenticationState(this.output)) {
|
|
284
|
+
logger.agent(this.name, 'Authentication required, skipping /about metadata fetch');
|
|
285
|
+
finish(null);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (this._isAuthenticating(this.output)) {
|
|
289
|
+
authWaitCount++;
|
|
290
|
+
if (authWaitCount >= maxAuthWait) {
|
|
291
|
+
logger.agent(this.name, 'Authentication timeout - Gemini may be stuck in auth flow');
|
|
292
|
+
// Don't send commands that would interfere with stuck auth
|
|
293
|
+
// Let the overall timeout handle this
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
logger.agent(this.name, 'Authentication in progress, waiting...');
|
|
297
|
+
setTimeout(waitForAuth, 1000);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Auth complete (or not happening), send commands
|
|
301
|
+
// Guard against shell being nullified if process exits during setTimeout delays
|
|
302
|
+
// Don't send Escape first - it can cancel auth flows that started between checks
|
|
303
|
+
setTimeout(() => this.shell?.write('/about\r'), 500);
|
|
304
|
+
setTimeout(() => this.shell?.write('\r'), 1000);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
setTimeout(waitForAuth, 100);
|
|
200
308
|
});
|
|
201
309
|
}
|
|
202
310
|
|
package/src/status-page.css
CHANGED
|
@@ -516,6 +516,21 @@ body.light-mode .progress-bar {
|
|
|
516
516
|
font-size: 13px;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
.auth-notice {
|
|
520
|
+
font-size: 13px;
|
|
521
|
+
color: #f6ad55;
|
|
522
|
+
padding: 8px 10px;
|
|
523
|
+
margin-bottom: 10px;
|
|
524
|
+
background: rgba(246, 173, 85, 0.08);
|
|
525
|
+
border: 1px solid rgba(246, 173, 85, 0.2);
|
|
526
|
+
border-radius: 4px;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.auth-detail {
|
|
530
|
+
margin-top: 4px;
|
|
531
|
+
color: var(--text-secondary);
|
|
532
|
+
}
|
|
533
|
+
|
|
519
534
|
/* Version update notice below agent cards */
|
|
520
535
|
.version-update-notice {
|
|
521
536
|
font-family: var(--font-mono);
|
|
@@ -533,6 +548,12 @@ body.light-mode .version-update-notice {
|
|
|
533
548
|
border-color: rgba(183, 121, 31, 0.2);
|
|
534
549
|
}
|
|
535
550
|
|
|
551
|
+
body.light-mode .auth-notice {
|
|
552
|
+
color: #b7791f;
|
|
553
|
+
background: rgba(246, 173, 85, 0.12);
|
|
554
|
+
border-color: rgba(183, 121, 31, 0.2);
|
|
555
|
+
}
|
|
556
|
+
|
|
536
557
|
button {
|
|
537
558
|
width: 100%;
|
|
538
559
|
padding: 10px;
|
package/src/status-page.js
CHANGED
|
@@ -9,6 +9,15 @@ const { getStatusBadgeClass, getStatusText } = require('./public-status');
|
|
|
9
9
|
|
|
10
10
|
const styles = fs.readFileSync(path.join(__dirname, 'status-page.css'), 'utf8');
|
|
11
11
|
|
|
12
|
+
function escapeHtml(value) {
|
|
13
|
+
return String(value)
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/'/g, ''');
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function statusPage(status) {
|
|
13
22
|
const agents = Object.entries(status);
|
|
14
23
|
|
|
@@ -36,7 +45,7 @@ function statusPage(status) {
|
|
|
36
45
|
const publicStatus = data.publicStatus;
|
|
37
46
|
const badgeClass = publicStatus ? getStatusBadgeClass(publicStatus.status) : 'status-badge-grey';
|
|
38
47
|
const badgeText = publicStatus ? getStatusText(publicStatus.status) : 'Unknown';
|
|
39
|
-
const badgeTitle = publicStatus?.description || 'Unable to fetch status';
|
|
48
|
+
const badgeTitle = escapeHtml(publicStatus?.description || 'Unable to fetch status');
|
|
40
49
|
const statusBadgeHtml = `<span class="status-badge ${badgeClass}" title="${badgeTitle}">${badgeText}</span>`;
|
|
41
50
|
|
|
42
51
|
// Version update notice - check usage.version (Codex) or metadata.updateAvailable (Gemini)
|
|
@@ -45,10 +54,15 @@ function statusPage(status) {
|
|
|
45
54
|
? `<div class="version-update-notice">Update available: v${version.current} → v${version.latest}</div>`
|
|
46
55
|
: '';
|
|
47
56
|
|
|
57
|
+
const authHtml = data.auth
|
|
58
|
+
? `<div class="auth-notice">${escapeHtml(data.auth.action || 'Please log in via the CLI.')}</div>`
|
|
59
|
+
: '';
|
|
60
|
+
const errorHtml = data.error && !data.auth ? `<span class="error-msg">${escapeHtml(data.error)}</span>` : '';
|
|
61
|
+
|
|
48
62
|
// Only show card-footer if there's an error or update notice
|
|
49
|
-
const hasFooterContent =
|
|
63
|
+
const hasFooterContent = errorHtml || authHtml || updateHtml;
|
|
50
64
|
const footerHtml = hasFooterContent
|
|
51
|
-
? `<div class="card-footer">${
|
|
65
|
+
? `<div class="card-footer">${errorHtml}${authHtml}${updateHtml}</div>`
|
|
52
66
|
: '';
|
|
53
67
|
|
|
54
68
|
return `
|